From 3740bef74622f714fc29bb40a42b21928c004352 Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:45:24 +0800 Subject: [PATCH 01/12] docs: design agent runtime image publishing --- .../2026-06-16-agent-runtime-images-design.md | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 docs/plans/2026-06-16-agent-runtime-images-design.md diff --git a/docs/plans/2026-06-16-agent-runtime-images-design.md b/docs/plans/2026-06-16-agent-runtime-images-design.md new file mode 100644 index 000000000..6d7b708fb --- /dev/null +++ b/docs/plans/2026-06-16-agent-runtime-images-design.md @@ -0,0 +1,91 @@ +# Agent Runtime Images Design + +## Goal + +Publish the three BoxLite agent runtime images from source-controlled Dockerfiles through GitHub Actions, using new GHCR package names and version tags. Keep the existing `boxlite-agent-base`, `boxlite-agent-python`, and `boxlite-agent-node` packages untouched. + +## Context + +The historical Dockerfiles were introduced in commit `fc88aa0b` and also appear in `bdab4823`: + +- `images/agent-runtime/base.Dockerfile` +- `images/agent-runtime/python.Dockerfile` +- `images/agent-runtime/node.Dockerfile` +- `images/agent-runtime/start-agent-runtime.sh` + +The historical build script was `scripts/images/build-agent-runtime.sh`. It built a Linux daemon binary into `apps/dist/apps/daemon-runtime/boxlite-daemon`, then used the repository root as the Docker build context. + +Current `origin/main` still has references to those paths in `apps/scripts/local-dex-env.mjs`, but the Dockerfiles and build script are absent. Current `.dockerignore` excludes `apps/dist`, which would break the Dockerfile `COPY apps/dist/apps/daemon-runtime/boxlite-daemon ...` step unless fixed. + +## Naming And Versioning + +Use new package names: + +- `ghcr.io/boxlite-ai/boxlite-agent-base-v2` +- `ghcr.io/boxlite-ai/boxlite-agent-python-v2` +- `ghcr.io/boxlite-ai/boxlite-agent-node-v2` + +Use version tags derived from the root `Cargo.toml` version. With the current version, the generated tag is `v0.9.5`. + +Do not delete, retag, or overwrite the existing `boxlite-agent-base`, `boxlite-agent-python`, or `boxlite-agent-node` packages. + +## Architecture + +Restore the historical Dockerfiles in `images/agent-runtime/` so local development and CI share one source of truth. Add a publish workflow that builds and pushes the three images as multi-architecture GHCR images for `linux/amd64` and `linux/arm64`. + +The workflow reads the version from root `Cargo.toml` by default and supports a manual override through `workflow_dispatch`. A shell build script remains available for local dry runs and for CI reuse where useful. + +## Data Flow + +1. Developer updates an agent runtime Dockerfile or daemon code. +2. GitHub Actions builds `boxlite-daemon` for Linux. +3. Buildx builds each runtime image for `linux/amd64` and `linux/arm64`. +4. GHCR receives three new package names with the same version tag. +5. API allowlist, infra fallback env, and dashboard image picker point at the new refs. +6. Dashboard creates boxes using the new refs, and API rejects refs outside the curated set. + +## Files To Change + +- Restore `images/agent-runtime/base.Dockerfile` +- Restore `images/agent-runtime/python.Dockerfile` +- Restore `images/agent-runtime/node.Dockerfile` +- Restore `images/agent-runtime/start-agent-runtime.sh` +- Restore and upgrade `scripts/images/build-agent-runtime.sh` +- Add `.github/workflows/publish-agent-runtime-images.yml` +- Modify `.dockerignore` +- Modify `apps/api/src/box/constants/curated-images.constant.ts` +- Modify `apps/api/src/box/constants/curated-images.constant.spec.ts` +- Modify `apps/infra/sst.config.ts` +- Modify `apps/dashboard/src/components/Box/CreateBoxSheet.tsx` +- Add or update dashboard tests for the supported image refs. + +## Error Handling + +The build script should fail fast when: + +- `TAG` is empty or malformed. +- `PLATFORMS` contains anything outside `linux/amd64` and `linux/arm64`. +- A required Dockerfile is missing. +- The daemon binary cannot be built. + +The workflow should not overwrite old package names. It should only push the `*-v2` packages. + +## Testing + +Use test-first changes for user-visible behavior: + +- API allowlist test should expect the three `*-v2:v0.9.5` refs and fail before implementation. +- Dashboard image picker test should expect the three `*-v2:v0.9.5` refs and fail before implementation. + +Then verify: + +- `make test:apps` for API/dashboard unit coverage. +- A local dry-run build for one platform where Docker is available. +- The workflow YAML is syntactically valid by inspection and uses `packages: write`. + +## Out Of Scope + +- Deleting old GHCR packages. +- Migrating existing boxes that already reference old images. +- Redesigning the image picker to fetch dynamic image refs from the API. +- Changing runner image pull authentication. From 99a3df4c583ccc2e94440d87acd02df8cd910b05 Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:46:29 +0800 Subject: [PATCH 02/12] docs: plan agent runtime image publishing --- docs/plans/2026-06-16-agent-runtime-images.md | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 docs/plans/2026-06-16-agent-runtime-images.md diff --git a/docs/plans/2026-06-16-agent-runtime-images.md b/docs/plans/2026-06-16-agent-runtime-images.md new file mode 100644 index 000000000..3f1029db7 --- /dev/null +++ b/docs/plans/2026-06-16-agent-runtime-images.md @@ -0,0 +1,263 @@ +# Agent Runtime Images Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Publish BoxLite agent runtime Docker images from source-controlled Dockerfiles using new GHCR package names and version tags. + +**Architecture:** Restore the historical `images/agent-runtime` Dockerfiles and startup script. Add a reusable local build script and a GitHub Actions workflow that derives `vX.Y.Z` from root `Cargo.toml`, then pushes `linux/amd64` and `linux/arm64` images to new `*-v2` GHCR packages. Update the API allowlist, infra fallbacks, and dashboard picker to use the new package names and version tag. + +**Tech Stack:** Docker Buildx, GitHub Actions, GHCR, Go daemon build, TypeScript/Jest API tests, React/Vitest dashboard tests, Makefile verification. + +--- + +### Task 1: Restore Agent Runtime Sources + +**Files:** +- Create: `images/agent-runtime/base.Dockerfile` +- Create: `images/agent-runtime/python.Dockerfile` +- Create: `images/agent-runtime/node.Dockerfile` +- Create: `images/agent-runtime/start-agent-runtime.sh` +- Modify: `.dockerignore` + +**Step 1: Restore the historical files** + +Use commit `fc88aa0b` as the source for the three Dockerfiles and `start-agent-runtime.sh`. + +**Step 2: Fix Docker build context** + +Modify `.dockerignore` so `apps/dist/apps/daemon-runtime/boxlite-daemon` can be copied into the Docker build context while unrelated app build output remains ignored. + +**Step 3: Verify Dockerfile sources exist** + +Run: + +```bash +test -f images/agent-runtime/base.Dockerfile +test -f images/agent-runtime/python.Dockerfile +test -f images/agent-runtime/node.Dockerfile +test -f images/agent-runtime/start-agent-runtime.sh +``` + +Expected: all commands exit 0. + +**Step 4: Commit** + +```bash +git add .dockerignore images/agent-runtime +git commit -m "build: restore agent runtime Dockerfiles" +``` + +### Task 2: Add Versioned Build Script + +**Files:** +- Create: `scripts/images/build-agent-runtime.sh` + +**Step 1: Write script behavior** + +Create a script that: + +- Reads `TAG` from env or derives `v$(Cargo.toml package.version)`. +- Uses `REGISTRY=ghcr.io/boxlite-ai` by default. +- Uses package names `boxlite-agent-base-v2`, `boxlite-agent-python-v2`, and `boxlite-agent-node-v2`. +- Accepts `PLATFORMS=linux/amd64,linux/arm64` by default. +- Accepts `PUSH=0` for local dry-run and `PUSH=1` for registry publishing. +- Fails on unsupported platforms. +- Builds the Linux daemon binary before each single-platform local build. +- Uses Buildx `--platform "$PLATFORMS"` for pushes. + +**Step 2: Run script help or dry validation** + +Run: + +```bash +bash -n scripts/images/build-agent-runtime.sh +``` + +Expected: exit 0. + +**Step 3: Commit** + +```bash +git add scripts/images/build-agent-runtime.sh +git commit -m "build: add versioned agent runtime image script" +``` + +### Task 3: Add Publish Workflow + +**Files:** +- Create: `.github/workflows/publish-agent-runtime-images.yml` + +**Step 1: Create workflow** + +Add a workflow with: + +- `name: Publish Agent Runtime Images` +- `push` trigger on `main` for `images/agent-runtime/**`, `apps/daemon/**`, `scripts/images/build-agent-runtime.sh`, and the workflow file. +- `workflow_dispatch` input `version` for manual override. +- `permissions: contents: read, packages: write`. +- `docker/setup-qemu-action`, `docker/setup-buildx-action`, and `docker/login-action`. +- Version extraction from root `Cargo.toml` when no manual version is provided. +- `TAG=v PUSH=1 PLATFORMS=linux/amd64,linux/arm64 bash scripts/images/build-agent-runtime.sh`. + +**Step 2: Validate workflow syntax structurally** + +Run: + +```bash +ruby -e "require 'yaml'; YAML.load_file('.github/workflows/publish-agent-runtime-images.yml'); puts 'ok'" +``` + +Expected: prints `ok`. + +**Step 3: Commit** + +```bash +git add .github/workflows/publish-agent-runtime-images.yml +git commit -m "ci: publish agent runtime images" +``` + +### Task 4: Update API And Infra Refs Test-First + +**Files:** +- Modify: `apps/api/src/box/constants/curated-images.constant.spec.ts` +- Modify: `apps/api/src/box/constants/curated-images.constant.ts` +- Modify: `apps/infra/sst.config.ts` + +**Step 1: Write failing API expectation** + +Update the API allowlist spec to expect: + +```text +ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5 +ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5 +ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5 +``` + +**Step 2: Run test to verify it fails** + +Run: + +```bash +cd apps && yarn test api/src/box/constants/curated-images.constant.spec.ts +``` + +Expected: FAIL because production code still returns old `boxlite-agent-*` refs. + +**Step 3: Update production refs** + +Update `curated-images.constant.ts` and `sst.config.ts` to use the `*-v2:v0.9.5` refs. + +**Step 4: Run test to verify it passes** + +Run: + +```bash +cd apps && yarn test api/src/box/constants/curated-images.constant.spec.ts +``` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add apps/api/src/box/constants/curated-images.constant.ts apps/api/src/box/constants/curated-images.constant.spec.ts apps/infra/sst.config.ts +git commit -m "feat: switch curated API refs to agent runtime v2" +``` + +### Task 5: Update Dashboard Picker Test-First + +**Files:** +- Create: `apps/dashboard/src/components/Box/supportedBoxImages.ts` +- Create: `apps/dashboard/src/components/Box/supportedBoxImages.test.ts` +- Modify: `apps/dashboard/src/components/Box/CreateBoxSheet.tsx` + +**Step 1: Extract desired refs into a test** + +Create a dashboard test that imports `SUPPORTED_BOX_IMAGES` from `supportedBoxImages.ts` and expects the three `*-v2:v0.9.5` refs, base first and default. + +**Step 2: Run test to verify it fails** + +Run: + +```bash +cd apps && yarn vitest run dashboard/src/components/Box/supportedBoxImages.test.ts +``` + +Expected: FAIL while the module is missing or still old. + +**Step 3: Extract production constant** + +Move the `SUPPORTED_BOX_IMAGES` array out of `CreateBoxSheet.tsx` into `supportedBoxImages.ts`, update refs to `*-v2:v0.9.5`, and import it from the sheet. + +**Step 4: Run test to verify it passes** + +Run: + +```bash +cd apps && yarn vitest run dashboard/src/components/Box/supportedBoxImages.test.ts +``` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add apps/dashboard/src/components/Box/CreateBoxSheet.tsx apps/dashboard/src/components/Box/supportedBoxImages.ts apps/dashboard/src/components/Box/supportedBoxImages.test.ts +git commit -m "feat: switch dashboard image picker to agent runtime v2" +``` + +### Task 6: Final Verification + +**Files:** +- All changed files. + +**Step 1: Run focused syntax checks** + +Run: + +```bash +bash -n scripts/images/build-agent-runtime.sh +ruby -e "require 'yaml'; YAML.load_file('.github/workflows/publish-agent-runtime-images.yml'); puts 'ok'" +``` + +Expected: both pass. + +**Step 2: Run app tests** + +Run: + +```bash +make test:apps +``` + +Expected: PASS. If the suite is too broad or blocked by environment, run the focused API and dashboard tests and report the blocker. + +**Step 3: Optional Docker dry-run** + +If Docker is available: + +```bash +PLATFORMS=linux/amd64 PUSH=0 bash scripts/images/build-agent-runtime.sh +``` + +Expected: local build succeeds for all three images. If Docker is unavailable, report that this was not run. + +**Step 4: Inspect final diff** + +Run: + +```bash +git status --short +git diff --stat origin/main...HEAD +``` + +Expected: clean working tree and scoped diff. + +**Step 5: Push and open PR** + +```bash +git push -u origin codex/agent-runtime-images-v2 +gh pr create --base main --head codex/agent-runtime-images-v2 --title "Publish agent runtime images from Dockerfiles" --body-file /tmp/agent-runtime-images-pr.md +``` + +Expected: PR created for review. From 5c0a7d2b36d662f14bae21977b3a385ab750e7f0 Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:48:05 +0800 Subject: [PATCH 03/12] docs: correct multi-arch daemon image plan --- docs/plans/2026-06-16-agent-runtime-images-design.md | 8 +++++--- docs/plans/2026-06-16-agent-runtime-images.md | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/plans/2026-06-16-agent-runtime-images-design.md b/docs/plans/2026-06-16-agent-runtime-images-design.md index 6d7b708fb..b5b1e7969 100644 --- a/docs/plans/2026-06-16-agent-runtime-images-design.md +++ b/docs/plans/2026-06-16-agent-runtime-images-design.md @@ -15,7 +15,7 @@ The historical Dockerfiles were introduced in commit `fc88aa0b` and also appear The historical build script was `scripts/images/build-agent-runtime.sh`. It built a Linux daemon binary into `apps/dist/apps/daemon-runtime/boxlite-daemon`, then used the repository root as the Docker build context. -Current `origin/main` still has references to those paths in `apps/scripts/local-dex-env.mjs`, but the Dockerfiles and build script are absent. Current `.dockerignore` excludes `apps/dist`, which would break the Dockerfile `COPY apps/dist/apps/daemon-runtime/boxlite-daemon ...` step unless fixed. +Current `origin/main` still has references to those paths in `apps/scripts/local-dex-env.mjs`, but the Dockerfiles and build script are absent. Current `.dockerignore` excludes `apps/dist`, which would break Dockerfile copies from `apps/dist/apps/daemon-runtime/` unless fixed. ## Naming And Versioning @@ -35,10 +35,12 @@ Restore the historical Dockerfiles in `images/agent-runtime/` so local developme The workflow reads the version from root `Cargo.toml` by default and supports a manual override through `workflow_dispatch`. A shell build script remains available for local dry runs and for CI reuse where useful. +Because these images embed `boxlite-daemon`, multi-architecture publishing must not copy one shared daemon binary into both platforms. The script builds `apps/dist/apps/daemon-runtime/boxlite-daemon-amd64` and `apps/dist/apps/daemon-runtime/boxlite-daemon-arm64`. The Dockerfiles use BuildKit's `TARGETARCH` argument to copy `boxlite-daemon-${TARGETARCH}` into `/boxlite/bin/boxlite-daemon`. + ## Data Flow 1. Developer updates an agent runtime Dockerfile or daemon code. -2. GitHub Actions builds `boxlite-daemon` for Linux. +2. GitHub Actions builds `boxlite-daemon-amd64` and `boxlite-daemon-arm64` for Linux. 3. Buildx builds each runtime image for `linux/amd64` and `linux/arm64`. 4. GHCR receives three new package names with the same version tag. 5. API allowlist, infra fallback env, and dashboard image picker point at the new refs. @@ -66,7 +68,7 @@ The build script should fail fast when: - `TAG` is empty or malformed. - `PLATFORMS` contains anything outside `linux/amd64` and `linux/arm64`. - A required Dockerfile is missing. -- The daemon binary cannot be built. +- A required architecture-specific daemon binary cannot be built. The workflow should not overwrite old package names. It should only push the `*-v2` packages. diff --git a/docs/plans/2026-06-16-agent-runtime-images.md b/docs/plans/2026-06-16-agent-runtime-images.md index 3f1029db7..8d441bbfb 100644 --- a/docs/plans/2026-06-16-agent-runtime-images.md +++ b/docs/plans/2026-06-16-agent-runtime-images.md @@ -4,7 +4,7 @@ **Goal:** Publish BoxLite agent runtime Docker images from source-controlled Dockerfiles using new GHCR package names and version tags. -**Architecture:** Restore the historical `images/agent-runtime` Dockerfiles and startup script. Add a reusable local build script and a GitHub Actions workflow that derives `vX.Y.Z` from root `Cargo.toml`, then pushes `linux/amd64` and `linux/arm64` images to new `*-v2` GHCR packages. Update the API allowlist, infra fallbacks, and dashboard picker to use the new package names and version tag. +**Architecture:** Restore the historical `images/agent-runtime` Dockerfiles and startup script, with one multi-arch correction: Dockerfiles copy `boxlite-daemon-${TARGETARCH}` so each platform gets the matching daemon binary. Add a reusable local build script and a GitHub Actions workflow that derives `vX.Y.Z` from root `Cargo.toml`, then pushes `linux/amd64` and `linux/arm64` images to new `*-v2` GHCR packages. Update the API allowlist, infra fallbacks, and dashboard picker to use the new package names and version tag. **Tech Stack:** Docker Buildx, GitHub Actions, GHCR, Go daemon build, TypeScript/Jest API tests, React/Vitest dashboard tests, Makefile verification. @@ -25,7 +25,7 @@ Use commit `fc88aa0b` as the source for the three Dockerfiles and `start-agent-r **Step 2: Fix Docker build context** -Modify `.dockerignore` so `apps/dist/apps/daemon-runtime/boxlite-daemon` can be copied into the Docker build context while unrelated app build output remains ignored. +Modify `.dockerignore` so `apps/dist/apps/daemon-runtime/boxlite-daemon-amd64` and `apps/dist/apps/daemon-runtime/boxlite-daemon-arm64` can be copied into the Docker build context while unrelated app build output remains ignored. **Step 3: Verify Dockerfile sources exist** @@ -62,7 +62,7 @@ Create a script that: - Accepts `PLATFORMS=linux/amd64,linux/arm64` by default. - Accepts `PUSH=0` for local dry-run and `PUSH=1` for registry publishing. - Fails on unsupported platforms. -- Builds the Linux daemon binary before each single-platform local build. +- Builds `boxlite-daemon-amd64` and/or `boxlite-daemon-arm64` before Docker builds. - Uses Buildx `--platform "$PLATFORMS"` for pushes. **Step 2: Run script help or dry validation** From 36246000c654f3fdf9a5f31a1b993e2a00b468a9 Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:49:10 +0800 Subject: [PATCH 04/12] build: restore agent runtime Dockerfiles --- .dockerignore | 8 +++- images/agent-runtime/base.Dockerfile | 40 +++++++++++++++++++ images/agent-runtime/node.Dockerfile | 41 ++++++++++++++++++++ images/agent-runtime/python.Dockerfile | 43 +++++++++++++++++++++ images/agent-runtime/start-agent-runtime.sh | 11 ++++++ 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 images/agent-runtime/base.Dockerfile create mode 100644 images/agent-runtime/node.Dockerfile create mode 100644 images/agent-runtime/python.Dockerfile create mode 100644 images/agent-runtime/start-agent-runtime.sh diff --git a/.dockerignore b/.dockerignore index 942fcb6f4..9249b7685 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,7 +13,13 @@ apps/.nx # Build outputs target dist -apps/dist +apps/dist/* +!apps/dist/apps +apps/dist/apps/* +!apps/dist/apps/daemon-runtime +apps/dist/apps/daemon-runtime/* +!apps/dist/apps/daemon-runtime/boxlite-daemon-amd64 +!apps/dist/apps/daemon-runtime/boxlite-daemon-arm64 coverage apps/coverage *.log diff --git a/images/agent-runtime/base.Dockerfile b/images/agent-runtime/base.Dockerfile new file mode 100644 index 000000000..1b5fec1ca --- /dev/null +++ b/images/agent-runtime/base.Dockerfile @@ -0,0 +1,40 @@ +FROM debian:bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive \ + TZ=Etc/UTC \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_BREAK_SYSTEM_PACKAGES=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + curl \ + git \ + jq \ + less \ + openssh-client \ + procps \ + python3 \ + python3-pip \ + python3-venv \ + sudo \ + tzdata \ + unzip \ + wget \ + zip \ + && ln -fs /usr/share/zoneinfo/$TZ /etc/localtime \ + && dpkg-reconfigure -f noninteractive tzdata \ + && echo 'ALL ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/boxlite \ + && chmod 0440 /etc/sudoers.d/boxlite \ + && rm -rf /var/lib/apt/lists/* + +ARG TARGETARCH +COPY apps/dist/apps/daemon-runtime/boxlite-daemon-${TARGETARCH} /boxlite/bin/boxlite-daemon +COPY images/agent-runtime/start-agent-runtime.sh /boxlite/bin/start-agent-runtime +RUN chmod 0755 /boxlite/bin/boxlite-daemon /boxlite/bin/start-agent-runtime \ + && mkdir -p /workspace + +WORKDIR /workspace +ENTRYPOINT ["/boxlite/bin/start-agent-runtime"] +CMD ["sleep", "infinity"] diff --git a/images/agent-runtime/node.Dockerfile b/images/agent-runtime/node.Dockerfile new file mode 100644 index 000000000..1d48df82f --- /dev/null +++ b/images/agent-runtime/node.Dockerfile @@ -0,0 +1,41 @@ +FROM node:22-bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive \ + TZ=Etc/UTC \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_BREAK_SYSTEM_PACKAGES=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + curl \ + git \ + jq \ + less \ + openssh-client \ + procps \ + python3 \ + python3-pip \ + python3-venv \ + sudo \ + tzdata \ + unzip \ + wget \ + zip \ + && ln -fs /usr/share/zoneinfo/$TZ /etc/localtime \ + && dpkg-reconfigure -f noninteractive tzdata \ + && corepack enable \ + && echo 'ALL ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/boxlite \ + && chmod 0440 /etc/sudoers.d/boxlite \ + && rm -rf /var/lib/apt/lists/* + +ARG TARGETARCH +COPY apps/dist/apps/daemon-runtime/boxlite-daemon-${TARGETARCH} /boxlite/bin/boxlite-daemon +COPY images/agent-runtime/start-agent-runtime.sh /boxlite/bin/start-agent-runtime +RUN chmod 0755 /boxlite/bin/boxlite-daemon /boxlite/bin/start-agent-runtime \ + && mkdir -p /workspace + +WORKDIR /workspace +ENTRYPOINT ["/boxlite/bin/start-agent-runtime"] +CMD ["sleep", "infinity"] diff --git a/images/agent-runtime/python.Dockerfile b/images/agent-runtime/python.Dockerfile new file mode 100644 index 000000000..ff882b453 --- /dev/null +++ b/images/agent-runtime/python.Dockerfile @@ -0,0 +1,43 @@ +FROM python:3.12-slim-bookworm + +ENV DEBIAN_FRONTEND=noninteractive \ + TZ=Etc/UTC \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_BREAK_SYSTEM_PACKAGES=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + build-essential \ + ca-certificates \ + curl \ + git \ + jq \ + less \ + openssh-client \ + pkg-config \ + procps \ + python3 \ + python3-pip \ + python3-venv \ + sudo \ + tzdata \ + unzip \ + wget \ + zip \ + && ln -fs /usr/share/zoneinfo/$TZ /etc/localtime \ + && dpkg-reconfigure -f noninteractive tzdata \ + && python -m pip install --upgrade pip setuptools wheel \ + && echo 'ALL ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/boxlite \ + && chmod 0440 /etc/sudoers.d/boxlite \ + && rm -rf /var/lib/apt/lists/* + +ARG TARGETARCH +COPY apps/dist/apps/daemon-runtime/boxlite-daemon-${TARGETARCH} /boxlite/bin/boxlite-daemon +COPY images/agent-runtime/start-agent-runtime.sh /boxlite/bin/start-agent-runtime +RUN chmod 0755 /boxlite/bin/boxlite-daemon /boxlite/bin/start-agent-runtime \ + && mkdir -p /workspace + +WORKDIR /workspace +ENTRYPOINT ["/boxlite/bin/start-agent-runtime"] +CMD ["sleep", "infinity"] diff --git a/images/agent-runtime/start-agent-runtime.sh b/images/agent-runtime/start-agent-runtime.sh new file mode 100644 index 000000000..1c8a135f4 --- /dev/null +++ b/images/agent-runtime/start-agent-runtime.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu + +mkdir -p /workspace + +if [ -z "${BOXLITE_SANDBOX_ID:-}" ]; then + BOXLITE_SANDBOX_ID="$(hostname)" + export BOXLITE_SANDBOX_ID +fi + +exec /boxlite/bin/boxlite-daemon "$@" From 5041b20da0ee20f77846ca452d356fbe6491c967 Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:50:29 +0800 Subject: [PATCH 05/12] build: add versioned agent runtime image script --- scripts/images/build-agent-runtime.sh | 120 ++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100755 scripts/images/build-agent-runtime.sh diff --git a/scripts/images/build-agent-runtime.sh b/scripts/images/build-agent-runtime.sh new file mode 100755 index 000000000..16d345c0d --- /dev/null +++ b/scripts/images/build-agent-runtime.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +APPS_DIR="$ROOT_DIR/apps" +DAEMON_OUT_DIR="$APPS_DIR/dist/apps/daemon-runtime" + +REGISTRY="${REGISTRY:-ghcr.io/boxlite-ai}" +PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}" +PUSH="${PUSH:-0}" + +read_cargo_version() { + sed -n 's/^version = "\([^"]*\)".*/\1/p' "$ROOT_DIR/Cargo.toml" | head -1 +} + +normalize_tag() { + local version tag + + if [[ -n "${TAG:-}" ]]; then + tag="$TAG" + else + version="${VERSION:-$(read_cargo_version)}" + if [[ -z "$version" ]]; then + echo "Unable to derive version from Cargo.toml; set TAG or VERSION" >&2 + exit 1 + fi + tag="v${version#v}" + fi + + if [[ "$tag" != v* ]]; then + tag="v$tag" + fi + + if [[ ! "$tag" =~ ^v[0-9]+[.][0-9]+[.][0-9]+([-+][0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid TAG=$tag; expected vMAJOR.MINOR.PATCH" >&2 + exit 1 + fi + + printf '%s\n' "$tag" +} + +platform_to_arch() { + case "$1" in + linux/amd64) printf 'amd64\n' ;; + linux/arm64) printf 'arm64\n' ;; + *) + echo "Unsupported platform '$1'; expected linux/amd64 or linux/arm64" >&2 + exit 1 + ;; + esac +} + +split_platforms() { + local raw="$1" + local -a out=() + IFS=',' read -ra out <<< "$raw" + for platform in "${out[@]}"; do + if [[ -z "$platform" ]]; then + echo "Invalid empty platform in PLATFORMS=$raw" >&2 + exit 1 + fi + platform_to_arch "$platform" >/dev/null + done + printf '%s\n' "${out[@]}" +} + +build_daemon() { + local platform="$1" + local arch + arch="$(platform_to_arch "$platform")" + + mkdir -p "$DAEMON_OUT_DIR" + + echo "==> Building daemon runtime binary for $platform" + ( + cd "$APPS_DIR" + GOOS=linux GOARCH="$arch" CGO_ENABLED=0 \ + go build -o "$DAEMON_OUT_DIR/boxlite-daemon-$arch" ./daemon/cmd/daemon/ + ) + + file "$DAEMON_OUT_DIR/boxlite-daemon-$arch" +} + +build_image() { + local image="$1" + local tag="$2" + local dockerfile="$ROOT_DIR/images/agent-runtime/${image}.Dockerfile" + local target="$REGISTRY/boxlite-agent-${image}-v2:$tag" + local -a build_args=(buildx build --platform "$PLATFORMS" -f "$dockerfile" -t "$target") + + if [[ ! -f "$dockerfile" ]]; then + echo "Missing Dockerfile: $dockerfile" >&2 + exit 1 + fi + + if [[ "$PUSH" == "1" || "$PUSH" == "true" ]]; then + build_args+=(--push) + elif [[ "${#REQUESTED_PLATFORMS[@]}" -eq 1 ]]; then + build_args+=(--load) + else + build_args+=(--output=type=cacheonly) + fi + + echo "==> Building $target from $dockerfile for $PLATFORMS" + docker "${build_args[@]}" "$ROOT_DIR" +} + +TAG="$(normalize_tag)" +REQUESTED_PLATFORMS=() +while IFS= read -r platform; do + REQUESTED_PLATFORMS+=("$platform") +done < <(split_platforms "$PLATFORMS") + +for platform in "${REQUESTED_PLATFORMS[@]}"; do + build_daemon "$platform" +done + +for image in base python node; do + build_image "$image" "$TAG" +done From a108b13395b84d23d89db55ebd20a53e3eed4cbe Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:51:31 +0800 Subject: [PATCH 06/12] ci: publish agent runtime images --- .../publish-agent-runtime-images.yml | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .github/workflows/publish-agent-runtime-images.yml diff --git a/.github/workflows/publish-agent-runtime-images.yml b/.github/workflows/publish-agent-runtime-images.yml new file mode 100644 index 000000000..ba9ff1e9d --- /dev/null +++ b/.github/workflows/publish-agent-runtime-images.yml @@ -0,0 +1,79 @@ +name: Publish Agent Runtime Images + +on: + push: + branches: [main] + paths: + - 'Cargo.toml' + - 'apps/daemon/**' + - 'images/agent-runtime/**' + - 'scripts/images/build-agent-runtime.sh' + - '.github/workflows/publish-agent-runtime-images.yml' + workflow_dispatch: + inputs: + version: + description: 'Version tag to publish, with or without leading v. Defaults to Cargo.toml version.' + required: false + type: string + +permissions: + contents: read + packages: write + +concurrency: + group: publish-agent-runtime-images-${{ github.ref }} + cancel-in-progress: false + +jobs: + publish: + name: Publish agent runtime images + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: apps/go.work + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Determine image version + id: version + env: + INPUT_VERSION: ${{ github.event.inputs.version }} + run: | + set -euo pipefail + + if [ -n "${INPUT_VERSION:-}" ]; then + version="${INPUT_VERSION#v}" + else + version="$(sed -n 's/^version = "\([^"]*\)".*/\1/p' Cargo.toml | head -1)" + fi + + if ! echo "$version" | grep -Eq '^[0-9]+[.][0-9]+[.][0-9]+([-+][0-9A-Za-z.-]+)?$'; then + echo "Invalid version '$version'; expected MAJOR.MINOR.PATCH" >&2 + exit 1 + fi + + echo "tag=v$version" >> "$GITHUB_OUTPUT" + + - name: Publish images + env: + TAG: ${{ steps.version.outputs.tag }} + PUSH: '1' + PLATFORMS: linux/amd64,linux/arm64 + run: bash scripts/images/build-agent-runtime.sh From d9285a87f7c27a861eca7915173b799896b809e6 Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:07:52 +0800 Subject: [PATCH 07/12] chore: sync runtime image refs to v2 packages --- .dockerignore | 4 ++-- .../constants/curated-images.constant.spec.ts | 6 ++--- .../box/constants/curated-images.constant.ts | 6 ++--- .../src/components/Box/CreateBoxSheet.tsx | 22 +------------------ .../components/Box/supportedBoxImages.test.ts | 13 +++++++++++ .../src/components/Box/supportedBoxImages.ts | 20 +++++++++++++++++ apps/infra/sst.config.ts | 10 ++++----- 7 files changed, 47 insertions(+), 34 deletions(-) create mode 100644 apps/dashboard/src/components/Box/supportedBoxImages.test.ts create mode 100644 apps/dashboard/src/components/Box/supportedBoxImages.ts diff --git a/.dockerignore b/.dockerignore index 9249b7685..8ce2ff222 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,9 +14,9 @@ apps/.nx target dist apps/dist/* -!apps/dist/apps +!apps/dist/apps/ apps/dist/apps/* -!apps/dist/apps/daemon-runtime +!apps/dist/apps/daemon-runtime/ apps/dist/apps/daemon-runtime/* !apps/dist/apps/daemon-runtime/boxlite-daemon-amd64 !apps/dist/apps/daemon-runtime/boxlite-daemon-arm64 diff --git a/apps/api/src/box/constants/curated-images.constant.spec.ts b/apps/api/src/box/constants/curated-images.constant.spec.ts index 2fda47f59..4f7a3edc5 100644 --- a/apps/api/src/box/constants/curated-images.constant.spec.ts +++ b/apps/api/src/box/constants/curated-images.constant.spec.ts @@ -29,9 +29,9 @@ describe('supported image allowlist', () => { it('exposes the three curated ghcr refs, base first (the default)', () => { const supported = supportedImages() expect(supported).toEqual([ - 'ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3', - 'ghcr.io/boxlite-ai/boxlite-agent-python:20260605-p0-r3', - 'ghcr.io/boxlite-ai/boxlite-agent-node:20260605-p0-r3', + 'ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5', + 'ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5', + 'ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5', ]) }) diff --git a/apps/api/src/box/constants/curated-images.constant.ts b/apps/api/src/box/constants/curated-images.constant.ts index f70a15208..e7cd20614 100644 --- a/apps/api/src/box/constants/curated-images.constant.ts +++ b/apps/api/src/box/constants/curated-images.constant.ts @@ -25,15 +25,15 @@ type SupportedImageSource = { const SUPPORTED_IMAGE_SOURCES: SupportedImageSource[] = [ { envVar: 'BOXLITE_SYSTEM_BASE_IMAGE', - fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3', + fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5', }, { envVar: 'BOXLITE_SYSTEM_PYTHON_IMAGE', - fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-python:20260605-p0-r3', + fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5', }, { envVar: 'BOXLITE_SYSTEM_NODE_IMAGE', - fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-node:20260605-p0-r3', + fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5', }, ] diff --git a/apps/dashboard/src/components/Box/CreateBoxSheet.tsx b/apps/dashboard/src/components/Box/CreateBoxSheet.tsx index 2aef774bf..522271604 100644 --- a/apps/dashboard/src/components/Box/CreateBoxSheet.tsx +++ b/apps/dashboard/src/components/Box/CreateBoxSheet.tsx @@ -34,6 +34,7 @@ import { createSearchParams, generatePath, useNavigate } from 'react-router-dom' import { toast } from 'sonner' import { z } from 'zod' import { ScrollArea } from '../ui/scroll-area' +import { SUPPORTED_BOX_IMAGES } from './supportedBoxImages' const NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/ @@ -80,27 +81,6 @@ const BOX_CREATE_DEFAULTS: Record = { disk: '10', } -const SUPPORTED_BOX_IMAGES = [ - { - id: 'base', - name: 'Base', - ref: 'ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3', - isDefault: true, - }, - { - id: 'python', - name: 'Python', - ref: 'ghcr.io/boxlite-ai/boxlite-agent-python:20260605-p0-r3', - isDefault: false, - }, - { - id: 'node', - name: 'Node.js', - ref: 'ghcr.io/boxlite-ai/boxlite-agent-node:20260605-p0-r3', - isDefault: false, - }, -] as const - const RESOURCE_FIELDS: Array<{ name: ResourceFieldName label: string diff --git a/apps/dashboard/src/components/Box/supportedBoxImages.test.ts b/apps/dashboard/src/components/Box/supportedBoxImages.test.ts new file mode 100644 index 000000000..fc151e861 --- /dev/null +++ b/apps/dashboard/src/components/Box/supportedBoxImages.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest' +import { SUPPORTED_BOX_IMAGES } from './supportedBoxImages' + +describe('supported box images', () => { + it('exposes the three versioned runtime image refs, base first', () => { + expect(SUPPORTED_BOX_IMAGES.map((image) => image.ref)).toEqual([ + 'ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5', + 'ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5', + 'ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5', + ]) + expect(SUPPORTED_BOX_IMAGES[0]).toMatchObject({ id: 'base', isDefault: true }) + }) +}) diff --git a/apps/dashboard/src/components/Box/supportedBoxImages.ts b/apps/dashboard/src/components/Box/supportedBoxImages.ts new file mode 100644 index 000000000..b78375fa9 --- /dev/null +++ b/apps/dashboard/src/components/Box/supportedBoxImages.ts @@ -0,0 +1,20 @@ +export const SUPPORTED_BOX_IMAGES = [ + { + id: 'base', + name: 'Base', + ref: 'ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5', + isDefault: true, + }, + { + id: 'python', + name: 'Python', + ref: 'ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5', + isDefault: false, + }, + { + id: 'node', + name: 'Node.js', + ref: 'ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5', + isDefault: false, + }, +] as const diff --git a/apps/infra/sst.config.ts b/apps/infra/sst.config.ts index 859b29296..4b6d9d4ee 100644 --- a/apps/infra/sst.config.ts +++ b/apps/infra/sst.config.ts @@ -440,23 +440,23 @@ export default $config({ VERSION: '0.1.0', DEFAULT_REGION_ENFORCE_QUOTAS: 'false', DEFAULT_TEMPLATE: envOr('DEFAULT_TEMPLATE', 'boxlite/base'), - // Box base images: only the three digest-pinned *_IMAGE refs below are live — the + // Box base images: only the three versioned *_IMAGE refs below are live — the // API gates box creation to that curated set (apps/api curated-images.constant.ts) // and the runner pulls them straight from ghcr.io with its GHCR_TOKEN. IMAGE_TAG and // the SOURCE_REGISTRY_* block are inert Daytona-port residue (no consumer — see // apps/api configuration.ts), kept only as reserved names for a future registry path. - BOXLITE_SYSTEM_IMAGE_TAG: envOr('BOXLITE_SYSTEM_IMAGE_TAG', '20260605-p0-r3'), + BOXLITE_SYSTEM_IMAGE_TAG: envOr('BOXLITE_SYSTEM_IMAGE_TAG', 'v0.9.5'), BOXLITE_SYSTEM_BASE_IMAGE: envOr( 'BOXLITE_SYSTEM_BASE_IMAGE', - 'ghcr.io/boxlite-ai/boxlite-agent-base:20260605-p0-r3', + 'ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5', ), BOXLITE_SYSTEM_PYTHON_IMAGE: envOr( 'BOXLITE_SYSTEM_PYTHON_IMAGE', - 'ghcr.io/boxlite-ai/boxlite-agent-python:20260605-p0-r3', + 'ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5', ), BOXLITE_SYSTEM_NODE_IMAGE: envOr( 'BOXLITE_SYSTEM_NODE_IMAGE', - 'ghcr.io/boxlite-ai/boxlite-agent-node:20260605-p0-r3', + 'ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5', ), ...(process.env.BOXLITE_SYSTEM_SOURCE_REGISTRY_URL && { BOXLITE_SYSTEM_SOURCE_REGISTRY_NAME: envOr( From e55dc6b17e79f81639b2b576290bcabc474a5cd4 Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:10:03 +0800 Subject: [PATCH 08/12] docs: rename runtime image plans without dates --- ...runtime-images-design.md => agent-runtime-images-v2-design.md} | 0 ...6-06-16-agent-runtime-images.md => agent-runtime-images-v2.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/plans/{2026-06-16-agent-runtime-images-design.md => agent-runtime-images-v2-design.md} (100%) rename docs/plans/{2026-06-16-agent-runtime-images.md => agent-runtime-images-v2.md} (100%) diff --git a/docs/plans/2026-06-16-agent-runtime-images-design.md b/docs/plans/agent-runtime-images-v2-design.md similarity index 100% rename from docs/plans/2026-06-16-agent-runtime-images-design.md rename to docs/plans/agent-runtime-images-v2-design.md diff --git a/docs/plans/2026-06-16-agent-runtime-images.md b/docs/plans/agent-runtime-images-v2.md similarity index 100% rename from docs/plans/2026-06-16-agent-runtime-images.md rename to docs/plans/agent-runtime-images-v2.md From 690b99d7ea3ae1b96e81fcbe50e9cc2b1d11f7e8 Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:36:00 +0800 Subject: [PATCH 09/12] fix: address runtime image review comments --- .../workflows/publish-agent-runtime-images.yml | 16 +++++++++++----- apps/scripts/local-dex-env.mjs | 7 ++++--- docs/plans/agent-runtime-images-v2.md | 18 ++++++++++++------ images/agent-runtime/start-agent-runtime.sh | 7 ++++++- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.github/workflows/publish-agent-runtime-images.yml b/.github/workflows/publish-agent-runtime-images.yml index ba9ff1e9d..2255d6dc2 100644 --- a/.github/workflows/publish-agent-runtime-images.yml +++ b/.github/workflows/publish-agent-runtime-images.yml @@ -4,8 +4,12 @@ on: push: branches: [main] paths: + - '.dockerignore' - 'Cargo.toml' + - 'apps/common-go/**' - 'apps/daemon/**' + - 'apps/go.work' + - 'apps/go.work.sum' - 'images/agent-runtime/**' - 'scripts/images/build-agent-runtime.sh' - '.github/workflows/publish-agent-runtime-images.yml' @@ -31,21 +35,23 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + with: + persist-credentials: false - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff with: go-version-file: apps/go.work - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f - name: Log in to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/apps/scripts/local-dex-env.mjs b/apps/scripts/local-dex-env.mjs index 6fcc81304..770ad73d9 100644 --- a/apps/scripts/local-dex-env.mjs +++ b/apps/scripts/local-dex-env.mjs @@ -26,7 +26,7 @@ const defaultConfig = { registryContainer: 'boxlite-local-registry', registryHost: process.env.BOXLITE_E2E_REGISTRY_HOST || 'localhost:5001', runtimeImagePlatform: process.env.BOXLITE_E2E_RUNTIME_IMAGE_PLATFORM || defaultRuntimeImagePlatform(), - runtimeImageTag: process.env.BOXLITE_E2E_RUNTIME_IMAGE_TAG || '20260605-p0-r5-local', + runtimeImageTag: process.env.BOXLITE_E2E_RUNTIME_IMAGE_TAG || 'v0.9.5-local', runnerHomeDir: process.env.BOXLITE_E2E_RUNNER_HOME_DIR || '/tmp/blrt', dockerConfigDir: process.env.BOXLITE_E2E_DOCKER_CONFIG || path.join(os.tmpdir(), 'boxlite-local-docker-config'), } @@ -208,7 +208,8 @@ function runtimeImageRef(config, name) { function ensureDaemonRuntimeBinary(config) { const outputDir = path.join(appsRoot, 'dist', 'apps', 'daemon-runtime') - const outputPath = path.join(outputDir, 'boxlite-daemon') + const targetArch = runtimeImageGoarch(config) + const outputPath = path.join(outputDir, `boxlite-daemon-${targetArch}`) fs.mkdirSync(outputDir, { recursive: true }) console.log(`[local-dex] building Linux daemon runtime binary for ${config.runtimeImagePlatform}`) @@ -219,7 +220,7 @@ function ensureDaemonRuntimeBinary(config) { env: { ...process.env, GOOS: 'linux', - GOARCH: runtimeImageGoarch(config), + GOARCH: targetArch, CGO_ENABLED: '0', }, }) diff --git a/docs/plans/agent-runtime-images-v2.md b/docs/plans/agent-runtime-images-v2.md index 8d441bbfb..cc042f472 100644 --- a/docs/plans/agent-runtime-images-v2.md +++ b/docs/plans/agent-runtime-images-v2.md @@ -10,9 +10,10 @@ --- -### Task 1: Restore Agent Runtime Sources +## Task 1: Restore Agent Runtime Sources **Files:** + - Create: `images/agent-runtime/base.Dockerfile` - Create: `images/agent-runtime/python.Dockerfile` - Create: `images/agent-runtime/node.Dockerfile` @@ -47,9 +48,10 @@ git add .dockerignore images/agent-runtime git commit -m "build: restore agent runtime Dockerfiles" ``` -### Task 2: Add Versioned Build Script +## Task 2: Add Versioned Build Script **Files:** + - Create: `scripts/images/build-agent-runtime.sh` **Step 1: Write script behavior** @@ -82,9 +84,10 @@ git add scripts/images/build-agent-runtime.sh git commit -m "build: add versioned agent runtime image script" ``` -### Task 3: Add Publish Workflow +## Task 3: Add Publish Workflow **Files:** + - Create: `.github/workflows/publish-agent-runtime-images.yml` **Step 1: Create workflow** @@ -116,9 +119,10 @@ git add .github/workflows/publish-agent-runtime-images.yml git commit -m "ci: publish agent runtime images" ``` -### Task 4: Update API And Infra Refs Test-First +## Task 4: Update API And Infra Refs Test-First **Files:** + - Modify: `apps/api/src/box/constants/curated-images.constant.spec.ts` - Modify: `apps/api/src/box/constants/curated-images.constant.ts` - Modify: `apps/infra/sst.config.ts` @@ -164,9 +168,10 @@ git add apps/api/src/box/constants/curated-images.constant.ts apps/api/src/box/c git commit -m "feat: switch curated API refs to agent runtime v2" ``` -### Task 5: Update Dashboard Picker Test-First +## Task 5: Update Dashboard Picker Test-First **Files:** + - Create: `apps/dashboard/src/components/Box/supportedBoxImages.ts` - Create: `apps/dashboard/src/components/Box/supportedBoxImages.test.ts` - Modify: `apps/dashboard/src/components/Box/CreateBoxSheet.tsx` @@ -206,9 +211,10 @@ git add apps/dashboard/src/components/Box/CreateBoxSheet.tsx apps/dashboard/src/ git commit -m "feat: switch dashboard image picker to agent runtime v2" ``` -### Task 6: Final Verification +## Task 6: Final Verification **Files:** + - All changed files. **Step 1: Run focused syntax checks** diff --git a/images/agent-runtime/start-agent-runtime.sh b/images/agent-runtime/start-agent-runtime.sh index 1c8a135f4..02c5a7ea9 100644 --- a/images/agent-runtime/start-agent-runtime.sh +++ b/images/agent-runtime/start-agent-runtime.sh @@ -3,8 +3,13 @@ set -eu mkdir -p /workspace +if [ -z "${BOXLITE_BOX_ID:-}" ]; then + BOXLITE_BOX_ID="${BOXLITE_SANDBOX_ID:-$(hostname)}" + export BOXLITE_BOX_ID +fi + if [ -z "${BOXLITE_SANDBOX_ID:-}" ]; then - BOXLITE_SANDBOX_ID="$(hostname)" + BOXLITE_SANDBOX_ID="$BOXLITE_BOX_ID" export BOXLITE_SANDBOX_ID fi From 543ef7cb712d4ff1400628f0b98930880bc12179 Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:55:20 +0800 Subject: [PATCH 10/12] fix: publish original runtime packages with version tags --- .../publish-agent-runtime-images.yml | 6 ++-- .../constants/curated-images.constant.spec.ts | 6 ++-- .../box/constants/curated-images.constant.ts | 6 ++-- .../components/Box/supportedBoxImages.test.ts | 6 ++-- .../src/components/Box/supportedBoxImages.ts | 6 ++-- apps/infra/sst.config.ts | 8 +++--- apps/scripts/local-dex-env.mjs | 2 +- ... agent-runtime-images-versioned-design.md} | 24 ++++++++-------- ...2.md => agent-runtime-images-versioned.md} | 28 +++++++++---------- images/agent-runtime/VERSION | 1 + scripts/images/build-agent-runtime.sh | 15 ++++++---- 11 files changed, 57 insertions(+), 51 deletions(-) rename docs/plans/{agent-runtime-images-v2-design.md => agent-runtime-images-versioned-design.md} (74%) rename docs/plans/{agent-runtime-images-v2.md => agent-runtime-images-versioned.md} (84%) create mode 100644 images/agent-runtime/VERSION diff --git a/.github/workflows/publish-agent-runtime-images.yml b/.github/workflows/publish-agent-runtime-images.yml index 2255d6dc2..7fb00e6c1 100644 --- a/.github/workflows/publish-agent-runtime-images.yml +++ b/.github/workflows/publish-agent-runtime-images.yml @@ -5,18 +5,18 @@ on: branches: [main] paths: - '.dockerignore' - - 'Cargo.toml' - 'apps/common-go/**' - 'apps/daemon/**' - 'apps/go.work' - 'apps/go.work.sum' + - 'images/agent-runtime/VERSION' - 'images/agent-runtime/**' - 'scripts/images/build-agent-runtime.sh' - '.github/workflows/publish-agent-runtime-images.yml' workflow_dispatch: inputs: version: - description: 'Version tag to publish, with or without leading v. Defaults to Cargo.toml version.' + description: 'Version tag to publish, with or without leading v. Defaults to images/agent-runtime/VERSION.' required: false type: string @@ -67,7 +67,7 @@ jobs: if [ -n "${INPUT_VERSION:-}" ]; then version="${INPUT_VERSION#v}" else - version="$(sed -n 's/^version = "\([^"]*\)".*/\1/p' Cargo.toml | head -1)" + version="$(tr -d '[:space:]' < images/agent-runtime/VERSION)" fi if ! echo "$version" | grep -Eq '^[0-9]+[.][0-9]+[.][0-9]+([-+][0-9A-Za-z.-]+)?$'; then diff --git a/apps/api/src/box/constants/curated-images.constant.spec.ts b/apps/api/src/box/constants/curated-images.constant.spec.ts index 4f7a3edc5..6168e30cc 100644 --- a/apps/api/src/box/constants/curated-images.constant.spec.ts +++ b/apps/api/src/box/constants/curated-images.constant.spec.ts @@ -29,9 +29,9 @@ describe('supported image allowlist', () => { it('exposes the three curated ghcr refs, base first (the default)', () => { const supported = supportedImages() expect(supported).toEqual([ - 'ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5', - 'ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5', - 'ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5', + 'ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0', + 'ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0', + 'ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0', ]) }) diff --git a/apps/api/src/box/constants/curated-images.constant.ts b/apps/api/src/box/constants/curated-images.constant.ts index e7cd20614..562b5b83b 100644 --- a/apps/api/src/box/constants/curated-images.constant.ts +++ b/apps/api/src/box/constants/curated-images.constant.ts @@ -25,15 +25,15 @@ type SupportedImageSource = { const SUPPORTED_IMAGE_SOURCES: SupportedImageSource[] = [ { envVar: 'BOXLITE_SYSTEM_BASE_IMAGE', - fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5', + fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0', }, { envVar: 'BOXLITE_SYSTEM_PYTHON_IMAGE', - fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5', + fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0', }, { envVar: 'BOXLITE_SYSTEM_NODE_IMAGE', - fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5', + fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0', }, ] diff --git a/apps/dashboard/src/components/Box/supportedBoxImages.test.ts b/apps/dashboard/src/components/Box/supportedBoxImages.test.ts index fc151e861..bd4a30e8b 100644 --- a/apps/dashboard/src/components/Box/supportedBoxImages.test.ts +++ b/apps/dashboard/src/components/Box/supportedBoxImages.test.ts @@ -4,9 +4,9 @@ import { SUPPORTED_BOX_IMAGES } from './supportedBoxImages' describe('supported box images', () => { it('exposes the three versioned runtime image refs, base first', () => { expect(SUPPORTED_BOX_IMAGES.map((image) => image.ref)).toEqual([ - 'ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5', - 'ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5', - 'ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5', + 'ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0', + 'ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0', + 'ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0', ]) expect(SUPPORTED_BOX_IMAGES[0]).toMatchObject({ id: 'base', isDefault: true }) }) diff --git a/apps/dashboard/src/components/Box/supportedBoxImages.ts b/apps/dashboard/src/components/Box/supportedBoxImages.ts index b78375fa9..210879897 100644 --- a/apps/dashboard/src/components/Box/supportedBoxImages.ts +++ b/apps/dashboard/src/components/Box/supportedBoxImages.ts @@ -2,19 +2,19 @@ export const SUPPORTED_BOX_IMAGES = [ { id: 'base', name: 'Base', - ref: 'ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5', + ref: 'ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0', isDefault: true, }, { id: 'python', name: 'Python', - ref: 'ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5', + ref: 'ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0', isDefault: false, }, { id: 'node', name: 'Node.js', - ref: 'ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5', + ref: 'ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0', isDefault: false, }, ] as const diff --git a/apps/infra/sst.config.ts b/apps/infra/sst.config.ts index 4b6d9d4ee..c05c6843f 100644 --- a/apps/infra/sst.config.ts +++ b/apps/infra/sst.config.ts @@ -445,18 +445,18 @@ export default $config({ // and the runner pulls them straight from ghcr.io with its GHCR_TOKEN. IMAGE_TAG and // the SOURCE_REGISTRY_* block are inert Daytona-port residue (no consumer — see // apps/api configuration.ts), kept only as reserved names for a future registry path. - BOXLITE_SYSTEM_IMAGE_TAG: envOr('BOXLITE_SYSTEM_IMAGE_TAG', 'v0.9.5'), + BOXLITE_SYSTEM_IMAGE_TAG: envOr('BOXLITE_SYSTEM_IMAGE_TAG', 'v0.1.0'), BOXLITE_SYSTEM_BASE_IMAGE: envOr( 'BOXLITE_SYSTEM_BASE_IMAGE', - 'ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5', + 'ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0', ), BOXLITE_SYSTEM_PYTHON_IMAGE: envOr( 'BOXLITE_SYSTEM_PYTHON_IMAGE', - 'ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5', + 'ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0', ), BOXLITE_SYSTEM_NODE_IMAGE: envOr( 'BOXLITE_SYSTEM_NODE_IMAGE', - 'ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5', + 'ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0', ), ...(process.env.BOXLITE_SYSTEM_SOURCE_REGISTRY_URL && { BOXLITE_SYSTEM_SOURCE_REGISTRY_NAME: envOr( diff --git a/apps/scripts/local-dex-env.mjs b/apps/scripts/local-dex-env.mjs index 770ad73d9..d50500c4a 100644 --- a/apps/scripts/local-dex-env.mjs +++ b/apps/scripts/local-dex-env.mjs @@ -26,7 +26,7 @@ const defaultConfig = { registryContainer: 'boxlite-local-registry', registryHost: process.env.BOXLITE_E2E_REGISTRY_HOST || 'localhost:5001', runtimeImagePlatform: process.env.BOXLITE_E2E_RUNTIME_IMAGE_PLATFORM || defaultRuntimeImagePlatform(), - runtimeImageTag: process.env.BOXLITE_E2E_RUNTIME_IMAGE_TAG || 'v0.9.5-local', + runtimeImageTag: process.env.BOXLITE_E2E_RUNTIME_IMAGE_TAG || 'v0.1.0-local', runnerHomeDir: process.env.BOXLITE_E2E_RUNNER_HOME_DIR || '/tmp/blrt', dockerConfigDir: process.env.BOXLITE_E2E_DOCKER_CONFIG || path.join(os.tmpdir(), 'boxlite-local-docker-config'), } diff --git a/docs/plans/agent-runtime-images-v2-design.md b/docs/plans/agent-runtime-images-versioned-design.md similarity index 74% rename from docs/plans/agent-runtime-images-v2-design.md rename to docs/plans/agent-runtime-images-versioned-design.md index b5b1e7969..2242eca70 100644 --- a/docs/plans/agent-runtime-images-v2-design.md +++ b/docs/plans/agent-runtime-images-versioned-design.md @@ -2,7 +2,7 @@ ## Goal -Publish the three BoxLite agent runtime images from source-controlled Dockerfiles through GitHub Actions, using new GHCR package names and version tags. Keep the existing `boxlite-agent-base`, `boxlite-agent-python`, and `boxlite-agent-node` packages untouched. +Publish the three BoxLite agent runtime images from source-controlled Dockerfiles through GitHub Actions, using the existing GHCR package names with version tags starting at `v0.1.0`. ## Context @@ -19,21 +19,21 @@ Current `origin/main` still has references to those paths in `apps/scripts/local ## Naming And Versioning -Use new package names: +Use the existing package names: -- `ghcr.io/boxlite-ai/boxlite-agent-base-v2` -- `ghcr.io/boxlite-ai/boxlite-agent-python-v2` -- `ghcr.io/boxlite-ai/boxlite-agent-node-v2` +- `ghcr.io/boxlite-ai/boxlite-agent-base` +- `ghcr.io/boxlite-ai/boxlite-agent-python` +- `ghcr.io/boxlite-ai/boxlite-agent-node` -Use version tags derived from the root `Cargo.toml` version. With the current version, the generated tag is `v0.9.5`. +Use version tags derived from `images/agent-runtime/VERSION`. The initial version is `0.1.0`, published as `v0.1.0`. Each future agent-runtime image release increments that file and publishes the matching `vX.Y.Z` tag. -Do not delete, retag, or overwrite the existing `boxlite-agent-base`, `boxlite-agent-python`, or `boxlite-agent-node` packages. +Do not delete or retag older package versions. ## Architecture Restore the historical Dockerfiles in `images/agent-runtime/` so local development and CI share one source of truth. Add a publish workflow that builds and pushes the three images as multi-architecture GHCR images for `linux/amd64` and `linux/arm64`. -The workflow reads the version from root `Cargo.toml` by default and supports a manual override through `workflow_dispatch`. A shell build script remains available for local dry runs and for CI reuse where useful. +The workflow reads the version from `images/agent-runtime/VERSION` by default and supports a manual override through `workflow_dispatch`. A shell build script remains available for local dry runs and for CI reuse where useful. Because these images embed `boxlite-daemon`, multi-architecture publishing must not copy one shared daemon binary into both platforms. The script builds `apps/dist/apps/daemon-runtime/boxlite-daemon-amd64` and `apps/dist/apps/daemon-runtime/boxlite-daemon-arm64`. The Dockerfiles use BuildKit's `TARGETARCH` argument to copy `boxlite-daemon-${TARGETARCH}` into `/boxlite/bin/boxlite-daemon`. @@ -42,7 +42,7 @@ Because these images embed `boxlite-daemon`, multi-architecture publishing must 1. Developer updates an agent runtime Dockerfile or daemon code. 2. GitHub Actions builds `boxlite-daemon-amd64` and `boxlite-daemon-arm64` for Linux. 3. Buildx builds each runtime image for `linux/amd64` and `linux/arm64`. -4. GHCR receives three new package names with the same version tag. +4. GHCR receives the three existing package names with the same version tag. 5. API allowlist, infra fallback env, and dashboard image picker point at the new refs. 6. Dashboard creates boxes using the new refs, and API rejects refs outside the curated set. @@ -70,14 +70,14 @@ The build script should fail fast when: - A required Dockerfile is missing. - A required architecture-specific daemon binary cannot be built. -The workflow should not overwrite old package names. It should only push the `*-v2` packages. +The workflow should not delete or retag existing image versions. It should push the requested version tag to the existing packages. ## Testing Use test-first changes for user-visible behavior: -- API allowlist test should expect the three `*-v2:v0.9.5` refs and fail before implementation. -- Dashboard image picker test should expect the three `*-v2:v0.9.5` refs and fail before implementation. +- API allowlist test should expect the three `*:v0.1.0` refs and fail before implementation. +- Dashboard image picker test should expect the three `*:v0.1.0` refs and fail before implementation. Then verify: diff --git a/docs/plans/agent-runtime-images-v2.md b/docs/plans/agent-runtime-images-versioned.md similarity index 84% rename from docs/plans/agent-runtime-images-v2.md rename to docs/plans/agent-runtime-images-versioned.md index cc042f472..47131bc3f 100644 --- a/docs/plans/agent-runtime-images-v2.md +++ b/docs/plans/agent-runtime-images-versioned.md @@ -2,9 +2,9 @@ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. -**Goal:** Publish BoxLite agent runtime Docker images from source-controlled Dockerfiles using new GHCR package names and version tags. +**Goal:** Publish BoxLite agent runtime Docker images from source-controlled Dockerfiles using the existing GHCR package names and version tags starting at `v0.1.0`. -**Architecture:** Restore the historical `images/agent-runtime` Dockerfiles and startup script, with one multi-arch correction: Dockerfiles copy `boxlite-daemon-${TARGETARCH}` so each platform gets the matching daemon binary. Add a reusable local build script and a GitHub Actions workflow that derives `vX.Y.Z` from root `Cargo.toml`, then pushes `linux/amd64` and `linux/arm64` images to new `*-v2` GHCR packages. Update the API allowlist, infra fallbacks, and dashboard picker to use the new package names and version tag. +**Architecture:** Restore the historical `images/agent-runtime` Dockerfiles and startup script, with one multi-arch correction: Dockerfiles copy `boxlite-daemon-${TARGETARCH}` so each platform gets the matching daemon binary. Add a reusable local build script and a GitHub Actions workflow that derives `vX.Y.Z` from `images/agent-runtime/VERSION`, then pushes `linux/amd64` and `linux/arm64` images to the existing GHCR packages. Update the API allowlist, infra fallbacks, and dashboard picker to use the existing package names and version tag. **Tech Stack:** Docker Buildx, GitHub Actions, GHCR, Go daemon build, TypeScript/Jest API tests, React/Vitest dashboard tests, Makefile verification. @@ -58,9 +58,9 @@ git commit -m "build: restore agent runtime Dockerfiles" Create a script that: -- Reads `TAG` from env or derives `v$(Cargo.toml package.version)`. +- Reads `TAG` from env or derives `v$(cat images/agent-runtime/VERSION)`. - Uses `REGISTRY=ghcr.io/boxlite-ai` by default. -- Uses package names `boxlite-agent-base-v2`, `boxlite-agent-python-v2`, and `boxlite-agent-node-v2`. +- Uses package names `boxlite-agent-base`, `boxlite-agent-python`, and `boxlite-agent-node`. - Accepts `PLATFORMS=linux/amd64,linux/arm64` by default. - Accepts `PUSH=0` for local dry-run and `PUSH=1` for registry publishing. - Fails on unsupported platforms. @@ -99,7 +99,7 @@ Add a workflow with: - `workflow_dispatch` input `version` for manual override. - `permissions: contents: read, packages: write`. - `docker/setup-qemu-action`, `docker/setup-buildx-action`, and `docker/login-action`. -- Version extraction from root `Cargo.toml` when no manual version is provided. +- Version extraction from `images/agent-runtime/VERSION` when no manual version is provided. - `TAG=v PUSH=1 PLATFORMS=linux/amd64,linux/arm64 bash scripts/images/build-agent-runtime.sh`. **Step 2: Validate workflow syntax structurally** @@ -132,9 +132,9 @@ git commit -m "ci: publish agent runtime images" Update the API allowlist spec to expect: ```text -ghcr.io/boxlite-ai/boxlite-agent-base-v2:v0.9.5 -ghcr.io/boxlite-ai/boxlite-agent-python-v2:v0.9.5 -ghcr.io/boxlite-ai/boxlite-agent-node-v2:v0.9.5 +ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0 +ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0 +ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0 ``` **Step 2: Run test to verify it fails** @@ -149,7 +149,7 @@ Expected: FAIL because production code still returns old `boxlite-agent-*` refs. **Step 3: Update production refs** -Update `curated-images.constant.ts` and `sst.config.ts` to use the `*-v2:v0.9.5` refs. +Update `curated-images.constant.ts` and `sst.config.ts` to use the `*:v0.1.0` refs. **Step 4: Run test to verify it passes** @@ -165,7 +165,7 @@ Expected: PASS. ```bash git add apps/api/src/box/constants/curated-images.constant.ts apps/api/src/box/constants/curated-images.constant.spec.ts apps/infra/sst.config.ts -git commit -m "feat: switch curated API refs to agent runtime v2" +git commit -m "feat: switch curated API refs to versioned agent runtime images" ``` ## Task 5: Update Dashboard Picker Test-First @@ -178,7 +178,7 @@ git commit -m "feat: switch curated API refs to agent runtime v2" **Step 1: Extract desired refs into a test** -Create a dashboard test that imports `SUPPORTED_BOX_IMAGES` from `supportedBoxImages.ts` and expects the three `*-v2:v0.9.5` refs, base first and default. +Create a dashboard test that imports `SUPPORTED_BOX_IMAGES` from `supportedBoxImages.ts` and expects the three `*:v0.1.0` refs, base first and default. **Step 2: Run test to verify it fails** @@ -192,7 +192,7 @@ Expected: FAIL while the module is missing or still old. **Step 3: Extract production constant** -Move the `SUPPORTED_BOX_IMAGES` array out of `CreateBoxSheet.tsx` into `supportedBoxImages.ts`, update refs to `*-v2:v0.9.5`, and import it from the sheet. +Move the `SUPPORTED_BOX_IMAGES` array out of `CreateBoxSheet.tsx` into `supportedBoxImages.ts`, update refs to `*:v0.1.0`, and import it from the sheet. **Step 4: Run test to verify it passes** @@ -208,7 +208,7 @@ Expected: PASS. ```bash git add apps/dashboard/src/components/Box/CreateBoxSheet.tsx apps/dashboard/src/components/Box/supportedBoxImages.ts apps/dashboard/src/components/Box/supportedBoxImages.test.ts -git commit -m "feat: switch dashboard image picker to agent runtime v2" +git commit -m "feat: switch dashboard image picker to versioned agent runtime images" ``` ## Task 6: Final Verification @@ -263,7 +263,7 @@ Expected: clean working tree and scoped diff. ```bash git push -u origin codex/agent-runtime-images-v2 -gh pr create --base main --head codex/agent-runtime-images-v2 --title "Publish agent runtime images from Dockerfiles" --body-file /tmp/agent-runtime-images-pr.md +gh pr create --base main --head codex/agent-runtime-images-v2 --title "Publish versioned agent runtime images from Dockerfiles" --body-file /tmp/agent-runtime-images-pr.md ``` Expected: PR created for review. diff --git a/images/agent-runtime/VERSION b/images/agent-runtime/VERSION new file mode 100644 index 000000000..6e8bf73aa --- /dev/null +++ b/images/agent-runtime/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/scripts/images/build-agent-runtime.sh b/scripts/images/build-agent-runtime.sh index 16d345c0d..a28c78971 100755 --- a/scripts/images/build-agent-runtime.sh +++ b/scripts/images/build-agent-runtime.sh @@ -4,13 +4,18 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" APPS_DIR="$ROOT_DIR/apps" DAEMON_OUT_DIR="$APPS_DIR/dist/apps/daemon-runtime" +VERSION_FILE="$ROOT_DIR/images/agent-runtime/VERSION" REGISTRY="${REGISTRY:-ghcr.io/boxlite-ai}" PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}" PUSH="${PUSH:-0}" -read_cargo_version() { - sed -n 's/^version = "\([^"]*\)".*/\1/p' "$ROOT_DIR/Cargo.toml" | head -1 +read_runtime_image_version() { + if [[ ! -f "$VERSION_FILE" ]]; then + echo "Missing runtime image version file: $VERSION_FILE" >&2 + exit 1 + fi + tr -d '[:space:]' < "$VERSION_FILE" } normalize_tag() { @@ -19,9 +24,9 @@ normalize_tag() { if [[ -n "${TAG:-}" ]]; then tag="$TAG" else - version="${VERSION:-$(read_cargo_version)}" + version="${VERSION:-$(read_runtime_image_version)}" if [[ -z "$version" ]]; then - echo "Unable to derive version from Cargo.toml; set TAG or VERSION" >&2 + echo "Unable to derive version from $VERSION_FILE; set TAG or VERSION" >&2 exit 1 fi tag="v${version#v}" @@ -85,7 +90,7 @@ build_image() { local image="$1" local tag="$2" local dockerfile="$ROOT_DIR/images/agent-runtime/${image}.Dockerfile" - local target="$REGISTRY/boxlite-agent-${image}-v2:$tag" + local target="$REGISTRY/boxlite-agent-${image}:$tag" local -a build_args=(buildx build --platform "$PLATFORMS" -f "$dockerfile" -t "$target") if [[ ! -f "$dockerfile" ]]; then From 23cebc9b65c625f06004529e5873e11b72f2fc72 Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:14:46 +0800 Subject: [PATCH 11/12] docs: annotate agent runtime image publishing flow --- .dockerignore | 1 + .../publish-agent-runtime-images.yml | 58 +++++++++---------- .../box/constants/curated-images.constant.ts | 6 +- .../src/components/Box/supportedBoxImages.ts | 6 +- apps/infra/sst.config.ts | 6 +- apps/scripts/local-dex-env.mjs | 24 ++++---- images/agent-runtime/base.Dockerfile | 23 ++++++++ images/agent-runtime/node.Dockerfile | 23 ++++++++ images/agent-runtime/python.Dockerfile | 25 ++++++++ images/agent-runtime/start-agent-runtime.sh | 14 ++--- scripts/images/build-agent-runtime.sh | 53 ++++++++--------- 11 files changed, 157 insertions(+), 82 deletions(-) diff --git a/.dockerignore b/.dockerignore index 8ce2ff222..c4d7b1b82 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,6 +14,7 @@ apps/.nx target dist apps/dist/* +# Re-include only the daemon runtime binaries needed by agent-runtime Dockerfiles. !apps/dist/apps/ apps/dist/apps/* !apps/dist/apps/daemon-runtime/ diff --git a/.github/workflows/publish-agent-runtime-images.yml b/.github/workflows/publish-agent-runtime-images.yml index 7fb00e6c1..5504a8991 100644 --- a/.github/workflows/publish-agent-runtime-images.yml +++ b/.github/workflows/publish-agent-runtime-images.yml @@ -2,17 +2,17 @@ name: Publish Agent Runtime Images on: push: - branches: [main] + branches: [main] # Publish only after the PR lands on main. paths: - - '.dockerignore' - - 'apps/common-go/**' - - 'apps/daemon/**' - - 'apps/go.work' - - 'apps/go.work.sum' - - 'images/agent-runtime/VERSION' - - 'images/agent-runtime/**' - - 'scripts/images/build-agent-runtime.sh' - - '.github/workflows/publish-agent-runtime-images.yml' + - '.dockerignore' # Docker context changes can change the published image contents. + - 'apps/common-go/**' # Daemon imports common-go packages, so these changes require republishing. + - 'apps/daemon/**' # Daemon binary is embedded into every runtime image. + - 'apps/go.work' # Go workspace membership affects daemon builds. + - 'apps/go.work.sum' # Go workspace checksums affect reproducible daemon builds. + - 'images/agent-runtime/VERSION' # Version bumps are the normal way to publish a new tag. + - 'images/agent-runtime/**' # Dockerfiles and startup script directly define image contents. + - 'scripts/images/build-agent-runtime.sh' # Build script changes affect all published images. + - '.github/workflows/publish-agent-runtime-images.yml' # Workflow changes should validate themselves on main. workflow_dispatch: inputs: version: @@ -21,12 +21,12 @@ on: type: string permissions: - contents: read - packages: write + contents: read # Checkout only needs repository read access. + packages: write # GHCR push requires package write access. concurrency: - group: publish-agent-runtime-images-${{ github.ref }} - cancel-in-progress: false + group: publish-agent-runtime-images-${{ github.ref }} # Serialize publishes per branch/ref. + cancel-in-progress: false # Do not cancel an in-flight release publish. jobs: publish: @@ -35,32 +35,32 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # Pin checkout for supply-chain stability. with: - persist-credentials: false + persist-credentials: false # Later steps do not need git credentials. - name: Set up Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # Pin Go setup action for supply-chain stability. with: - go-version-file: apps/go.work + go-version-file: apps/go.work # Use the repo's Go workspace version. - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # Enable cross-arch build emulation. - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # Buildx is required for multi-arch images. - name: Log in to GHCR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # Authenticate Docker so buildx can push to GHCR. with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + registry: ghcr.io # Target registry for BoxLite runtime images. + username: ${{ github.actor }} # GitHub actor is accepted for GITHUB_TOKEN auth. + password: ${{ secrets.GITHUB_TOKEN }} # Built-in token has packages:write from workflow permissions. - name: Determine image version id: version env: - INPUT_VERSION: ${{ github.event.inputs.version }} + INPUT_VERSION: ${{ github.event.inputs.version }} # Optional manual override from workflow_dispatch. run: | set -euo pipefail @@ -75,11 +75,11 @@ jobs: exit 1 fi - echo "tag=v$version" >> "$GITHUB_OUTPUT" + echo "tag=v$version" >> "$GITHUB_OUTPUT" # Share normalized Docker tag with the publish step. - name: Publish images env: - TAG: ${{ steps.version.outputs.tag }} - PUSH: '1' - PLATFORMS: linux/amd64,linux/arm64 + TAG: ${{ steps.version.outputs.tag }} # Use the version resolved above. + PUSH: '1' # CI path must push to GHCR instead of local Docker. + PLATFORMS: linux/amd64,linux/arm64 # Publish both supported CPU architectures. run: bash scripts/images/build-agent-runtime.sh diff --git a/apps/api/src/box/constants/curated-images.constant.ts b/apps/api/src/box/constants/curated-images.constant.ts index 562b5b83b..dd37a4dd0 100644 --- a/apps/api/src/box/constants/curated-images.constant.ts +++ b/apps/api/src/box/constants/curated-images.constant.ts @@ -25,15 +25,15 @@ type SupportedImageSource = { const SUPPORTED_IMAGE_SOURCES: SupportedImageSource[] = [ { envVar: 'BOXLITE_SYSTEM_BASE_IMAGE', - fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0', + fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0', // Default minimal image for generic boxes. }, { envVar: 'BOXLITE_SYSTEM_PYTHON_IMAGE', - fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0', + fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0', // Python-ready image exposed as a curated option. }, { envVar: 'BOXLITE_SYSTEM_NODE_IMAGE', - fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0', + fallbackRef: 'ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0', // Node-ready image exposed as a curated option. }, ] diff --git a/apps/dashboard/src/components/Box/supportedBoxImages.ts b/apps/dashboard/src/components/Box/supportedBoxImages.ts index 210879897..24dc4293e 100644 --- a/apps/dashboard/src/components/Box/supportedBoxImages.ts +++ b/apps/dashboard/src/components/Box/supportedBoxImages.ts @@ -2,19 +2,19 @@ export const SUPPORTED_BOX_IMAGES = [ { id: 'base', name: 'Base', - ref: 'ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0', + ref: 'ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0', // Default generic image shown first in Create Box. isDefault: true, }, { id: 'python', name: 'Python', - ref: 'ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0', + ref: 'ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0', // Python option for users who need Python tooling preinstalled. isDefault: false, }, { id: 'node', name: 'Node.js', - ref: 'ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0', + ref: 'ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0', // Node option for users who need Node tooling preinstalled. isDefault: false, }, ] as const diff --git a/apps/infra/sst.config.ts b/apps/infra/sst.config.ts index c05c6843f..a0ecdaf04 100644 --- a/apps/infra/sst.config.ts +++ b/apps/infra/sst.config.ts @@ -448,15 +448,15 @@ export default $config({ BOXLITE_SYSTEM_IMAGE_TAG: envOr('BOXLITE_SYSTEM_IMAGE_TAG', 'v0.1.0'), BOXLITE_SYSTEM_BASE_IMAGE: envOr( 'BOXLITE_SYSTEM_BASE_IMAGE', - 'ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0', + 'ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0', // Production fallback for the default generic box image. ), BOXLITE_SYSTEM_PYTHON_IMAGE: envOr( 'BOXLITE_SYSTEM_PYTHON_IMAGE', - 'ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0', + 'ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0', // Production fallback for Python boxes. ), BOXLITE_SYSTEM_NODE_IMAGE: envOr( 'BOXLITE_SYSTEM_NODE_IMAGE', - 'ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0', + 'ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0', // Production fallback for Node boxes. ), ...(process.env.BOXLITE_SYSTEM_SOURCE_REGISTRY_URL && { BOXLITE_SYSTEM_SOURCE_REGISTRY_NAME: envOr( diff --git a/apps/scripts/local-dex-env.mjs b/apps/scripts/local-dex-env.mjs index d50500c4a..2cb487bed 100644 --- a/apps/scripts/local-dex-env.mjs +++ b/apps/scripts/local-dex-env.mjs @@ -25,8 +25,8 @@ const defaultConfig = { dexContainer: 'boxlite-local-dex', registryContainer: 'boxlite-local-registry', registryHost: process.env.BOXLITE_E2E_REGISTRY_HOST || 'localhost:5001', - runtimeImagePlatform: process.env.BOXLITE_E2E_RUNTIME_IMAGE_PLATFORM || defaultRuntimeImagePlatform(), - runtimeImageTag: process.env.BOXLITE_E2E_RUNTIME_IMAGE_TAG || 'v0.1.0-local', + runtimeImagePlatform: process.env.BOXLITE_E2E_RUNTIME_IMAGE_PLATFORM || defaultRuntimeImagePlatform(), // Build the local runtime image for the host CPU by default. + runtimeImageTag: process.env.BOXLITE_E2E_RUNTIME_IMAGE_TAG || 'v0.1.0-local', // Local-only tag so dev images do not collide with GHCR release tags. runnerHomeDir: process.env.BOXLITE_E2E_RUNNER_HOME_DIR || '/tmp/blrt', dockerConfigDir: process.env.BOXLITE_E2E_DOCKER_CONFIG || path.join(os.tmpdir(), 'boxlite-local-docker-config'), } @@ -179,9 +179,9 @@ function ensureRegistry(config) { function ensureRuntimeImages(config) { const images = [ - ['base', runtimeImageRef(config, 'base'), path.join(repoRoot, 'images', 'agent-runtime', 'base.Dockerfile')], - ['python', runtimeImageRef(config, 'python'), path.join(repoRoot, 'images', 'agent-runtime', 'python.Dockerfile')], - ['node', runtimeImageRef(config, 'node'), path.join(repoRoot, 'images', 'agent-runtime', 'node.Dockerfile')], + ['base', runtimeImageRef(config, 'base'), path.join(repoRoot, 'images', 'agent-runtime', 'base.Dockerfile')], // Generic runtime image used as the default local box image. + ['python', runtimeImageRef(config, 'python'), path.join(repoRoot, 'images', 'agent-runtime', 'python.Dockerfile')], // Python runtime image for local create-box coverage. + ['node', runtimeImageRef(config, 'node'), path.join(repoRoot, 'images', 'agent-runtime', 'node.Dockerfile')], // Node runtime image for local create-box coverage. ] for (const [name, imageRef, dockerfile] of images) { @@ -195,10 +195,11 @@ function ensureRuntimeImages(config) { console.log(`[local-dex] building runtime image ${imageRef}`) docker(['build', '--platform', config.runtimeImagePlatform, '-f', dockerfile, '-t', imageRef, repoRoot], { + // Build the same Dockerfile local Dex and CI publish use. stdio: 'inherit', }) console.log(`[local-dex] pushing runtime image ${imageRef}`) - docker(['push', imageRef], { stdio: 'inherit', env: localDockerEnv(config) }) + docker(['push', imageRef], { stdio: 'inherit', env: localDockerEnv(config) }) // Push to the local registry so the local runner can pull by ref. } } @@ -208,20 +209,21 @@ function runtimeImageRef(config, name) { function ensureDaemonRuntimeBinary(config) { const outputDir = path.join(appsRoot, 'dist', 'apps', 'daemon-runtime') - const targetArch = runtimeImageGoarch(config) - const outputPath = path.join(outputDir, `boxlite-daemon-${targetArch}`) + const targetArch = runtimeImageGoarch(config) // Dockerfile TARGETARCH must match this suffix. + const outputPath = path.join(outputDir, `boxlite-daemon-${targetArch}`) // Dockerfiles copy boxlite-daemon-amd64 or boxlite-daemon-arm64. fs.mkdirSync(outputDir, { recursive: true }) console.log(`[local-dex] building Linux daemon runtime binary for ${config.runtimeImagePlatform}`) const result = spawnSync('go', ['build', '-o', outputPath, './daemon/cmd/daemon/main.go'], { + // Build the daemon embedded into the local runtime image. cwd: appsRoot, encoding: 'utf8', stdio: 'inherit', env: { ...process.env, - GOOS: 'linux', - GOARCH: targetArch, - CGO_ENABLED: '0', + GOOS: 'linux', // Runtime images are Linux containers even on macOS dev machines. + GOARCH: targetArch, // Match the local Docker platform architecture. + CGO_ENABLED: '0', // Avoid host C toolchain dependencies for local daemon builds. }, }) diff --git a/images/agent-runtime/base.Dockerfile b/images/agent-runtime/base.Dockerfile index 1b5fec1ca..e9a30e4fd 100644 --- a/images/agent-runtime/base.Dockerfile +++ b/images/agent-runtime/base.Dockerfile @@ -1,10 +1,26 @@ +# Debian slim keeps the base image small while still supporting apt-managed tools. FROM debian:bookworm-slim +# Noninteractive apt avoids CI prompts; pip settings let users install Python tools inside the box. ENV DEBIAN_FRONTEND=noninteractive \ TZ=Etc/UTC \ PIP_DISABLE_PIP_VERSION_CHECK=1 \ PIP_BREAK_SYSTEM_PACKAGES=1 +# Install baseline interactive/dev tools expected in every BoxLite runtime image. +# bash: familiar shell for users and scripts. +# ca-certificates: trust store for HTTPS downloads and git remotes. +# curl: common HTTP client for setup scripts and API checks. +# git: clone and inspect source repositories from inside a box. +# jq: inspect JSON responses during debugging. +# less: pager for logs and command output. +# openssh-client: SSH client utilities for git over SSH and remote access. +# procps: ps/top/free process tools used for runtime inspection. +# python3/python3-pip/python3-venv: baseline Python tooling even in the generic base image. +# sudo: allow passwordless privilege escalation inside this disposable runtime image. +# tzdata: UTC timezone data so tools report consistent timestamps. +# unzip/wget/zip: common archive and download utilities for setup workflows. +# The same RUN configures UTC, enables passwordless sudo, and removes apt metadata to keep the image small. RUN apt-get update \ && apt-get install -y --no-install-recommends \ bash \ @@ -29,12 +45,19 @@ RUN apt-get update \ && chmod 0440 /etc/sudoers.d/boxlite \ && rm -rf /var/lib/apt/lists/* +# Buildx provides TARGETARCH so each image copies the matching daemon binary. ARG TARGETARCH +# Embed the Linux daemon that exposes BoxLite toolbox/session APIs inside the box. COPY apps/dist/apps/daemon-runtime/boxlite-daemon-${TARGETARCH} /boxlite/bin/boxlite-daemon +# Embed the startup wrapper that fills required env fallbacks before launching the daemon. COPY images/agent-runtime/start-agent-runtime.sh /boxlite/bin/start-agent-runtime +# Make embedded binaries executable and create the default user workspace. RUN chmod 0755 /boxlite/bin/boxlite-daemon /boxlite/bin/start-agent-runtime \ && mkdir -p /workspace +# Users and agent commands start in /workspace. WORKDIR /workspace +# Always start through the wrapper so BOXLITE_BOX_ID fallback is applied. ENTRYPOINT ["/boxlite/bin/start-agent-runtime"] +# Keep the container alive until the runner asks the daemon to execute work. CMD ["sleep", "infinity"] diff --git a/images/agent-runtime/node.Dockerfile b/images/agent-runtime/node.Dockerfile index 1d48df82f..e8181e0d8 100644 --- a/images/agent-runtime/node.Dockerfile +++ b/images/agent-runtime/node.Dockerfile @@ -1,10 +1,26 @@ +# Node slim provides Node 22 while keeping the image smaller than full Debian. FROM node:22-bookworm-slim +# Noninteractive apt avoids CI prompts; pip settings support Python tooling often used by Node projects. ENV DEBIAN_FRONTEND=noninteractive \ TZ=Etc/UTC \ PIP_DISABLE_PIP_VERSION_CHECK=1 \ PIP_BREAK_SYSTEM_PACKAGES=1 +# Install Node-oriented runtime tools plus Python helpers needed by many JS build chains. +# bash: familiar shell for users and scripts. +# ca-certificates: trust store for HTTPS downloads and git remotes. +# curl: common HTTP client for setup scripts and API checks. +# git: clone and inspect source repositories from inside a box. +# jq: inspect JSON responses during debugging. +# less: pager for logs and command output. +# openssh-client: SSH client utilities for git over SSH and remote access. +# procps: ps/top/free process tools used for runtime inspection. +# python3/python3-pip/python3-venv: Python tooling needed by many npm packages and scripts. +# sudo: allow passwordless privilege escalation inside this disposable runtime image. +# tzdata: UTC timezone data so tools report consistent timestamps. +# unzip/wget/zip: common archive and download utilities for setup workflows. +# The same RUN configures UTC, enables Corepack, enables sudo, and removes apt metadata. RUN apt-get update \ && apt-get install -y --no-install-recommends \ bash \ @@ -30,12 +46,19 @@ RUN apt-get update \ && chmod 0440 /etc/sudoers.d/boxlite \ && rm -rf /var/lib/apt/lists/* +# Buildx provides TARGETARCH so each image copies the matching daemon binary. ARG TARGETARCH +# Embed the Linux daemon that exposes BoxLite toolbox/session APIs inside the box. COPY apps/dist/apps/daemon-runtime/boxlite-daemon-${TARGETARCH} /boxlite/bin/boxlite-daemon +# Embed the startup wrapper that fills required env fallbacks before launching the daemon. COPY images/agent-runtime/start-agent-runtime.sh /boxlite/bin/start-agent-runtime +# Make embedded binaries executable and create the default user workspace. RUN chmod 0755 /boxlite/bin/boxlite-daemon /boxlite/bin/start-agent-runtime \ && mkdir -p /workspace +# Users and agent commands start in /workspace. WORKDIR /workspace +# Always start through the wrapper so BOXLITE_BOX_ID fallback is applied. ENTRYPOINT ["/boxlite/bin/start-agent-runtime"] +# Keep the container alive until the runner asks the daemon to execute work. CMD ["sleep", "infinity"] diff --git a/images/agent-runtime/python.Dockerfile b/images/agent-runtime/python.Dockerfile index ff882b453..ea176f40e 100644 --- a/images/agent-runtime/python.Dockerfile +++ b/images/agent-runtime/python.Dockerfile @@ -1,10 +1,28 @@ +# Python slim provides Python 3.12 while keeping the image smaller than full Debian. FROM python:3.12-slim-bookworm +# Noninteractive apt avoids CI prompts; pip settings support system-level package installs in boxes. ENV DEBIAN_FRONTEND=noninteractive \ TZ=Etc/UTC \ PIP_DISABLE_PIP_VERSION_CHECK=1 \ PIP_BREAK_SYSTEM_PACKAGES=1 +# Install Python-focused runtime and build tools expected by agent workloads. +# bash: familiar shell for users and scripts. +# build-essential: gcc/make toolchain for Python packages with native extensions. +# ca-certificates: trust store for HTTPS downloads and git remotes. +# curl: common HTTP client for setup scripts and API checks. +# git: clone and inspect source repositories from inside a box. +# jq: inspect JSON responses during debugging. +# less: pager for logs and command output. +# openssh-client: SSH client utilities for git over SSH and remote access. +# pkg-config: locate native libraries when building Python wheels. +# procps: ps/top/free process tools used for runtime inspection. +# python3/python3-pip/python3-venv: Debian Python tools for compatibility with apt packages. +# sudo: allow passwordless privilege escalation inside this disposable runtime image. +# tzdata: UTC timezone data so tools report consistent timestamps. +# unzip/wget/zip: common archive and download utilities for setup workflows. +# The same RUN configures UTC, upgrades Python packaging tools, enables sudo, and removes apt metadata. RUN apt-get update \ && apt-get install -y --no-install-recommends \ bash \ @@ -32,12 +50,19 @@ RUN apt-get update \ && chmod 0440 /etc/sudoers.d/boxlite \ && rm -rf /var/lib/apt/lists/* +# Buildx provides TARGETARCH so each image copies the matching daemon binary. ARG TARGETARCH +# Embed the Linux daemon that exposes BoxLite toolbox/session APIs inside the box. COPY apps/dist/apps/daemon-runtime/boxlite-daemon-${TARGETARCH} /boxlite/bin/boxlite-daemon +# Embed the startup wrapper that fills required env fallbacks before launching the daemon. COPY images/agent-runtime/start-agent-runtime.sh /boxlite/bin/start-agent-runtime +# Make embedded binaries executable and create the default user workspace. RUN chmod 0755 /boxlite/bin/boxlite-daemon /boxlite/bin/start-agent-runtime \ && mkdir -p /workspace +# Users and agent commands start in /workspace. WORKDIR /workspace +# Always start through the wrapper so BOXLITE_BOX_ID fallback is applied. ENTRYPOINT ["/boxlite/bin/start-agent-runtime"] +# Keep the container alive until the runner asks the daemon to execute work. CMD ["sleep", "infinity"] diff --git a/images/agent-runtime/start-agent-runtime.sh b/images/agent-runtime/start-agent-runtime.sh index 02c5a7ea9..b09853a3a 100644 --- a/images/agent-runtime/start-agent-runtime.sh +++ b/images/agent-runtime/start-agent-runtime.sh @@ -1,16 +1,16 @@ #!/bin/sh -set -eu +set -eu # Exit on missing variables or failed commands so startup fails visibly. -mkdir -p /workspace +mkdir -p /workspace # Ensure the default working directory exists before the daemon starts. if [ -z "${BOXLITE_BOX_ID:-}" ]; then - BOXLITE_BOX_ID="${BOXLITE_SANDBOX_ID:-$(hostname)}" - export BOXLITE_BOX_ID + BOXLITE_BOX_ID="${BOXLITE_SANDBOX_ID:-$(hostname)}" # Prefer legacy sandbox id, then hostname, for the daemon's required box id. + export BOXLITE_BOX_ID # Make the fallback visible to boxlite-daemon. fi if [ -z "${BOXLITE_SANDBOX_ID:-}" ]; then - BOXLITE_SANDBOX_ID="$BOXLITE_BOX_ID" - export BOXLITE_SANDBOX_ID + BOXLITE_SANDBOX_ID="$BOXLITE_BOX_ID" # Keep legacy callers that still read BOXLITE_SANDBOX_ID working. + export BOXLITE_SANDBOX_ID # Make the compatibility value visible to child processes. fi -exec /boxlite/bin/boxlite-daemon "$@" +exec /boxlite/bin/boxlite-daemon "$@" # Replace the wrapper with the daemon so signals reach the real process. diff --git a/scripts/images/build-agent-runtime.sh b/scripts/images/build-agent-runtime.sh index a28c78971..ed490d4a5 100755 --- a/scripts/images/build-agent-runtime.sh +++ b/scripts/images/build-agent-runtime.sh @@ -1,24 +1,24 @@ #!/usr/bin/env bash -set -euo pipefail +set -euo pipefail # Fail fast on command errors, unset variables, and broken pipes. -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -APPS_DIR="$ROOT_DIR/apps" -DAEMON_OUT_DIR="$APPS_DIR/dist/apps/daemon-runtime" -VERSION_FILE="$ROOT_DIR/images/agent-runtime/VERSION" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # Repository root, also the Docker build context. +APPS_DIR="$ROOT_DIR/apps" # Go workspace root for building the daemon binary. +DAEMON_OUT_DIR="$APPS_DIR/dist/apps/daemon-runtime" # Build output directory that Dockerfiles copy from. +VERSION_FILE="$ROOT_DIR/images/agent-runtime/VERSION" # Agent image release version source of truth. -REGISTRY="${REGISTRY:-ghcr.io/boxlite-ai}" -PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}" -PUSH="${PUSH:-0}" +REGISTRY="${REGISTRY:-ghcr.io/boxlite-ai}" # Target registry namespace for the three image packages. +PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}" # Default publish target covers Intel and ARM Linux hosts. +PUSH="${PUSH:-0}" # PUSH=0 validates locally, PUSH=1 publishes to the registry. -read_runtime_image_version() { +read_runtime_image_version() { # Read 0.1.0-style version and let normalize_tag add the leading v. if [[ ! -f "$VERSION_FILE" ]]; then echo "Missing runtime image version file: $VERSION_FILE" >&2 exit 1 fi - tr -d '[:space:]' < "$VERSION_FILE" + tr -d '[:space:]' < "$VERSION_FILE" # Strip newline so the value can be embedded in Docker tags. } -normalize_tag() { +normalize_tag() { # Accept TAG or VERSION overrides and normalize them to vMAJOR.MINOR.PATCH. local version tag if [[ -n "${TAG:-}" ]]; then @@ -44,7 +44,7 @@ normalize_tag() { printf '%s\n' "$tag" } -platform_to_arch() { +platform_to_arch() { # Convert Docker platform strings to GOARCH and Dockerfile TARGETARCH values. case "$1" in linux/amd64) printf 'amd64\n' ;; linux/arm64) printf 'arm64\n' ;; @@ -55,7 +55,7 @@ platform_to_arch() { esac } -split_platforms() { +split_platforms() { # Validate the comma-separated PLATFORMS input before any build starts. local raw="$1" local -a out=() IFS=',' read -ra out <<< "$raw" @@ -69,7 +69,7 @@ split_platforms() { printf '%s\n' "${out[@]}" } -build_daemon() { +build_daemon() { # Build one Linux daemon binary per requested architecture. local platform="$1" local arch arch="$(platform_to_arch "$platform")" @@ -79,19 +79,20 @@ build_daemon() { echo "==> Building daemon runtime binary for $platform" ( cd "$APPS_DIR" + # Build a static Linux daemon so Dockerfiles can copy it into the matching image architecture. GOOS=linux GOARCH="$arch" CGO_ENABLED=0 \ go build -o "$DAEMON_OUT_DIR/boxlite-daemon-$arch" ./daemon/cmd/daemon/ ) - file "$DAEMON_OUT_DIR/boxlite-daemon-$arch" + file "$DAEMON_OUT_DIR/boxlite-daemon-$arch" # Print architecture metadata for CI logs and review. } -build_image() { +build_image() { # Build or publish one of base, python, or node with the shared version tag. local image="$1" local tag="$2" - local dockerfile="$ROOT_DIR/images/agent-runtime/${image}.Dockerfile" - local target="$REGISTRY/boxlite-agent-${image}:$tag" - local -a build_args=(buildx build --platform "$PLATFORMS" -f "$dockerfile" -t "$target") + local dockerfile="$ROOT_DIR/images/agent-runtime/${image}.Dockerfile" # Dockerfile selected by image flavor. + local target="$REGISTRY/boxlite-agent-${image}:$tag" # Existing GHCR package name plus version tag. + local -a build_args=(buildx build --platform "$PLATFORMS" -f "$dockerfile" -t "$target") # Common Buildx arguments. if [[ ! -f "$dockerfile" ]]; then echo "Missing Dockerfile: $dockerfile" >&2 @@ -99,27 +100,27 @@ build_image() { fi if [[ "$PUSH" == "1" || "$PUSH" == "true" ]]; then - build_args+=(--push) + build_args+=(--push) # CI publish path writes the multi-arch manifest to GHCR. elif [[ "${#REQUESTED_PLATFORMS[@]}" -eq 1 ]]; then - build_args+=(--load) + build_args+=(--load) # Single-platform local validation loads the image into Docker. else - build_args+=(--output=type=cacheonly) + build_args+=(--output=type=cacheonly) # Multi-platform dry run validates build steps without pushing. fi echo "==> Building $target from $dockerfile for $PLATFORMS" docker "${build_args[@]}" "$ROOT_DIR" } -TAG="$(normalize_tag)" -REQUESTED_PLATFORMS=() +TAG="$(normalize_tag)" # Final Docker tag such as v0.1.0. +REQUESTED_PLATFORMS=() # Parsed platform list used for per-arch daemon builds. while IFS= read -r platform; do REQUESTED_PLATFORMS+=("$platform") done < <(split_platforms "$PLATFORMS") for platform in "${REQUESTED_PLATFORMS[@]}"; do - build_daemon "$platform" + build_daemon "$platform" # Produce boxlite-daemon-amd64 or boxlite-daemon-arm64 before Docker build. done for image in base python node; do - build_image "$image" "$TAG" + build_image "$image" "$TAG" # Publish all three runtime variants with the same version tag. done From cc9e7dafd2e029edd72428268083b4940ffc1307 Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:45:48 +0800 Subject: [PATCH 12/12] fix: keep runtime images pure --- .dockerignore | 7 - .../publish-agent-runtime-images.yml | 11 +- apps/scripts/local-dex-env.mjs | 37 --- .../agent-runtime-images-versioned-design.md | 31 +- docs/plans/agent-runtime-images-versioned.md | 269 ------------------ images/agent-runtime/base.Dockerfile | 15 +- images/agent-runtime/node.Dockerfile | 15 +- images/agent-runtime/python.Dockerfile | 15 +- images/agent-runtime/start-agent-runtime.sh | 16 -- scripts/images/build-agent-runtime.sh | 46 +-- 10 files changed, 33 insertions(+), 429 deletions(-) delete mode 100644 docs/plans/agent-runtime-images-versioned.md delete mode 100644 images/agent-runtime/start-agent-runtime.sh diff --git a/.dockerignore b/.dockerignore index c4d7b1b82..3aaa7df83 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,13 +14,6 @@ apps/.nx target dist apps/dist/* -# Re-include only the daemon runtime binaries needed by agent-runtime Dockerfiles. -!apps/dist/apps/ -apps/dist/apps/* -!apps/dist/apps/daemon-runtime/ -apps/dist/apps/daemon-runtime/* -!apps/dist/apps/daemon-runtime/boxlite-daemon-amd64 -!apps/dist/apps/daemon-runtime/boxlite-daemon-arm64 coverage apps/coverage *.log diff --git a/.github/workflows/publish-agent-runtime-images.yml b/.github/workflows/publish-agent-runtime-images.yml index 5504a8991..07059afc3 100644 --- a/.github/workflows/publish-agent-runtime-images.yml +++ b/.github/workflows/publish-agent-runtime-images.yml @@ -5,12 +5,8 @@ on: branches: [main] # Publish only after the PR lands on main. paths: - '.dockerignore' # Docker context changes can change the published image contents. - - 'apps/common-go/**' # Daemon imports common-go packages, so these changes require republishing. - - 'apps/daemon/**' # Daemon binary is embedded into every runtime image. - - 'apps/go.work' # Go workspace membership affects daemon builds. - - 'apps/go.work.sum' # Go workspace checksums affect reproducible daemon builds. - 'images/agent-runtime/VERSION' # Version bumps are the normal way to publish a new tag. - - 'images/agent-runtime/**' # Dockerfiles and startup script directly define image contents. + - 'images/agent-runtime/**' # Dockerfiles and version file directly define image contents. - 'scripts/images/build-agent-runtime.sh' # Build script changes affect all published images. - '.github/workflows/publish-agent-runtime-images.yml' # Workflow changes should validate themselves on main. workflow_dispatch: @@ -39,11 +35,6 @@ jobs: with: persist-credentials: false # Later steps do not need git credentials. - - name: Set up Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # Pin Go setup action for supply-chain stability. - with: - go-version-file: apps/go.work # Use the repo's Go workspace version. - - name: Set up QEMU uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # Enable cross-arch build emulation. diff --git a/apps/scripts/local-dex-env.mjs b/apps/scripts/local-dex-env.mjs index 2cb487bed..b68307cf7 100644 --- a/apps/scripts/local-dex-env.mjs +++ b/apps/scripts/local-dex-env.mjs @@ -55,7 +55,6 @@ export async function runLocalDexEnvironment({ mode, command = [] }) { await waitForTcp('localhost', 6379, 'Redis') await waitForTcp('localhost', 5001, 'Local registry') ensureLocalDockerConfig(defaultConfig) - ensureDaemonRuntimeBinary(defaultConfig) ensureRuntimeImages(defaultConfig) ensureGoSdkDevNativeLibrary() ensureGoBuildCacheTracksNativeLibrary() @@ -207,42 +206,6 @@ function runtimeImageRef(config, name) { return `${config.registryHost}/boxlite/${name}:${config.runtimeImageTag}` } -function ensureDaemonRuntimeBinary(config) { - const outputDir = path.join(appsRoot, 'dist', 'apps', 'daemon-runtime') - const targetArch = runtimeImageGoarch(config) // Dockerfile TARGETARCH must match this suffix. - const outputPath = path.join(outputDir, `boxlite-daemon-${targetArch}`) // Dockerfiles copy boxlite-daemon-amd64 or boxlite-daemon-arm64. - fs.mkdirSync(outputDir, { recursive: true }) - - console.log(`[local-dex] building Linux daemon runtime binary for ${config.runtimeImagePlatform}`) - const result = spawnSync('go', ['build', '-o', outputPath, './daemon/cmd/daemon/main.go'], { - // Build the daemon embedded into the local runtime image. - cwd: appsRoot, - encoding: 'utf8', - stdio: 'inherit', - env: { - ...process.env, - GOOS: 'linux', // Runtime images are Linux containers even on macOS dev machines. - GOARCH: targetArch, // Match the local Docker platform architecture. - CGO_ENABLED: '0', // Avoid host C toolchain dependencies for local daemon builds. - }, - }) - - if (result.status !== 0) { - throw new Error('go build daemon runtime binary failed; agent runtime images cannot include toolbox') - } -} - -function runtimeImageGoarch(config) { - const arch = config.runtimeImagePlatform.split('/').pop() - switch (arch) { - case 'amd64': - case 'arm64': - return arch - default: - throw new Error(`Unsupported runtime image platform for daemon build: ${config.runtimeImagePlatform}`) - } -} - function ensureGoSdkDevNativeLibrary() { const libPath = path.join(repoRoot, 'target', 'debug', 'libboxlite.a') if (fs.existsSync(libPath)) { diff --git a/docs/plans/agent-runtime-images-versioned-design.md b/docs/plans/agent-runtime-images-versioned-design.md index 2242eca70..a8cdb2e92 100644 --- a/docs/plans/agent-runtime-images-versioned-design.md +++ b/docs/plans/agent-runtime-images-versioned-design.md @@ -4,18 +4,17 @@ Publish the three BoxLite agent runtime images from source-controlled Dockerfiles through GitHub Actions, using the existing GHCR package names with version tags starting at `v0.1.0`. +These images are pure OCI images. They provide the filesystem and default tools that a Box can pull and run; they do not embed `boxlite-daemon`, `start-agent-runtime.sh`, or any BoxLite process supervisor. + ## Context -The historical Dockerfiles were introduced in commit `fc88aa0b` and also appear in `bdab4823`: +The image source now lives in this repository so the GHCR packages can be rebuilt from reviewed code instead of unpublished local Dockerfiles: - `images/agent-runtime/base.Dockerfile` - `images/agent-runtime/python.Dockerfile` - `images/agent-runtime/node.Dockerfile` -- `images/agent-runtime/start-agent-runtime.sh` - -The historical build script was `scripts/images/build-agent-runtime.sh`. It built a Linux daemon binary into `apps/dist/apps/daemon-runtime/boxlite-daemon`, then used the repository root as the Docker build context. -Current `origin/main` still has references to those paths in `apps/scripts/local-dex-env.mjs`, but the Dockerfiles and build script are absent. Current `.dockerignore` excludes `apps/dist`, which would break Dockerfile copies from `apps/dist/apps/daemon-runtime/` unless fixed. +The BoxLite runtime already pulls and loads image refs. The image should therefore stay limited to OS/runtime contents and a default keep-alive command. BoxLite control-plane or runner behavior belongs outside the image. ## Naming And Versioning @@ -31,30 +30,29 @@ Do not delete or retag older package versions. ## Architecture -Restore the historical Dockerfiles in `images/agent-runtime/` so local development and CI share one source of truth. Add a publish workflow that builds and pushes the three images as multi-architecture GHCR images for `linux/amd64` and `linux/arm64`. +Add Dockerfiles in `images/agent-runtime/` so local development and CI share one source of truth. Add a publish workflow that builds and pushes the three images as multi-architecture GHCR images for `linux/amd64` and `linux/arm64`. The workflow reads the version from `images/agent-runtime/VERSION` by default and supports a manual override through `workflow_dispatch`. A shell build script remains available for local dry runs and for CI reuse where useful. -Because these images embed `boxlite-daemon`, multi-architecture publishing must not copy one shared daemon binary into both platforms. The script builds `apps/dist/apps/daemon-runtime/boxlite-daemon-amd64` and `apps/dist/apps/daemon-runtime/boxlite-daemon-arm64`. The Dockerfiles use BuildKit's `TARGETARCH` argument to copy `boxlite-daemon-${TARGETARCH}` into `/boxlite/bin/boxlite-daemon`. +Multi-architecture support is handled by Docker Buildx. The Dockerfiles contain only packages and shell setup, so no architecture-specific BoxLite binary is copied into the image. ## Data Flow -1. Developer updates an agent runtime Dockerfile or daemon code. -2. GitHub Actions builds `boxlite-daemon-amd64` and `boxlite-daemon-arm64` for Linux. -3. Buildx builds each runtime image for `linux/amd64` and `linux/arm64`. +1. Developer updates an agent runtime Dockerfile or bumps `images/agent-runtime/VERSION`. +2. GitHub Actions validates the version and logs in to GHCR. +3. Buildx builds each pure runtime image for `linux/amd64` and `linux/arm64`. 4. GHCR receives the three existing package names with the same version tag. 5. API allowlist, infra fallback env, and dashboard image picker point at the new refs. 6. Dashboard creates boxes using the new refs, and API rejects refs outside the curated set. ## Files To Change -- Restore `images/agent-runtime/base.Dockerfile` -- Restore `images/agent-runtime/python.Dockerfile` -- Restore `images/agent-runtime/node.Dockerfile` -- Restore `images/agent-runtime/start-agent-runtime.sh` -- Restore and upgrade `scripts/images/build-agent-runtime.sh` +- Add `images/agent-runtime/base.Dockerfile` +- Add `images/agent-runtime/python.Dockerfile` +- Add `images/agent-runtime/node.Dockerfile` +- Add `images/agent-runtime/VERSION` +- Add `scripts/images/build-agent-runtime.sh` - Add `.github/workflows/publish-agent-runtime-images.yml` -- Modify `.dockerignore` - Modify `apps/api/src/box/constants/curated-images.constant.ts` - Modify `apps/api/src/box/constants/curated-images.constant.spec.ts` - Modify `apps/infra/sst.config.ts` @@ -68,7 +66,6 @@ The build script should fail fast when: - `TAG` is empty or malformed. - `PLATFORMS` contains anything outside `linux/amd64` and `linux/arm64`. - A required Dockerfile is missing. -- A required architecture-specific daemon binary cannot be built. The workflow should not delete or retag existing image versions. It should push the requested version tag to the existing packages. diff --git a/docs/plans/agent-runtime-images-versioned.md b/docs/plans/agent-runtime-images-versioned.md deleted file mode 100644 index 47131bc3f..000000000 --- a/docs/plans/agent-runtime-images-versioned.md +++ /dev/null @@ -1,269 +0,0 @@ -# Agent Runtime Images Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Publish BoxLite agent runtime Docker images from source-controlled Dockerfiles using the existing GHCR package names and version tags starting at `v0.1.0`. - -**Architecture:** Restore the historical `images/agent-runtime` Dockerfiles and startup script, with one multi-arch correction: Dockerfiles copy `boxlite-daemon-${TARGETARCH}` so each platform gets the matching daemon binary. Add a reusable local build script and a GitHub Actions workflow that derives `vX.Y.Z` from `images/agent-runtime/VERSION`, then pushes `linux/amd64` and `linux/arm64` images to the existing GHCR packages. Update the API allowlist, infra fallbacks, and dashboard picker to use the existing package names and version tag. - -**Tech Stack:** Docker Buildx, GitHub Actions, GHCR, Go daemon build, TypeScript/Jest API tests, React/Vitest dashboard tests, Makefile verification. - ---- - -## Task 1: Restore Agent Runtime Sources - -**Files:** - -- Create: `images/agent-runtime/base.Dockerfile` -- Create: `images/agent-runtime/python.Dockerfile` -- Create: `images/agent-runtime/node.Dockerfile` -- Create: `images/agent-runtime/start-agent-runtime.sh` -- Modify: `.dockerignore` - -**Step 1: Restore the historical files** - -Use commit `fc88aa0b` as the source for the three Dockerfiles and `start-agent-runtime.sh`. - -**Step 2: Fix Docker build context** - -Modify `.dockerignore` so `apps/dist/apps/daemon-runtime/boxlite-daemon-amd64` and `apps/dist/apps/daemon-runtime/boxlite-daemon-arm64` can be copied into the Docker build context while unrelated app build output remains ignored. - -**Step 3: Verify Dockerfile sources exist** - -Run: - -```bash -test -f images/agent-runtime/base.Dockerfile -test -f images/agent-runtime/python.Dockerfile -test -f images/agent-runtime/node.Dockerfile -test -f images/agent-runtime/start-agent-runtime.sh -``` - -Expected: all commands exit 0. - -**Step 4: Commit** - -```bash -git add .dockerignore images/agent-runtime -git commit -m "build: restore agent runtime Dockerfiles" -``` - -## Task 2: Add Versioned Build Script - -**Files:** - -- Create: `scripts/images/build-agent-runtime.sh` - -**Step 1: Write script behavior** - -Create a script that: - -- Reads `TAG` from env or derives `v$(cat images/agent-runtime/VERSION)`. -- Uses `REGISTRY=ghcr.io/boxlite-ai` by default. -- Uses package names `boxlite-agent-base`, `boxlite-agent-python`, and `boxlite-agent-node`. -- Accepts `PLATFORMS=linux/amd64,linux/arm64` by default. -- Accepts `PUSH=0` for local dry-run and `PUSH=1` for registry publishing. -- Fails on unsupported platforms. -- Builds `boxlite-daemon-amd64` and/or `boxlite-daemon-arm64` before Docker builds. -- Uses Buildx `--platform "$PLATFORMS"` for pushes. - -**Step 2: Run script help or dry validation** - -Run: - -```bash -bash -n scripts/images/build-agent-runtime.sh -``` - -Expected: exit 0. - -**Step 3: Commit** - -```bash -git add scripts/images/build-agent-runtime.sh -git commit -m "build: add versioned agent runtime image script" -``` - -## Task 3: Add Publish Workflow - -**Files:** - -- Create: `.github/workflows/publish-agent-runtime-images.yml` - -**Step 1: Create workflow** - -Add a workflow with: - -- `name: Publish Agent Runtime Images` -- `push` trigger on `main` for `images/agent-runtime/**`, `apps/daemon/**`, `scripts/images/build-agent-runtime.sh`, and the workflow file. -- `workflow_dispatch` input `version` for manual override. -- `permissions: contents: read, packages: write`. -- `docker/setup-qemu-action`, `docker/setup-buildx-action`, and `docker/login-action`. -- Version extraction from `images/agent-runtime/VERSION` when no manual version is provided. -- `TAG=v PUSH=1 PLATFORMS=linux/amd64,linux/arm64 bash scripts/images/build-agent-runtime.sh`. - -**Step 2: Validate workflow syntax structurally** - -Run: - -```bash -ruby -e "require 'yaml'; YAML.load_file('.github/workflows/publish-agent-runtime-images.yml'); puts 'ok'" -``` - -Expected: prints `ok`. - -**Step 3: Commit** - -```bash -git add .github/workflows/publish-agent-runtime-images.yml -git commit -m "ci: publish agent runtime images" -``` - -## Task 4: Update API And Infra Refs Test-First - -**Files:** - -- Modify: `apps/api/src/box/constants/curated-images.constant.spec.ts` -- Modify: `apps/api/src/box/constants/curated-images.constant.ts` -- Modify: `apps/infra/sst.config.ts` - -**Step 1: Write failing API expectation** - -Update the API allowlist spec to expect: - -```text -ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0 -ghcr.io/boxlite-ai/boxlite-agent-python:v0.1.0 -ghcr.io/boxlite-ai/boxlite-agent-node:v0.1.0 -``` - -**Step 2: Run test to verify it fails** - -Run: - -```bash -cd apps && yarn test api/src/box/constants/curated-images.constant.spec.ts -``` - -Expected: FAIL because production code still returns old `boxlite-agent-*` refs. - -**Step 3: Update production refs** - -Update `curated-images.constant.ts` and `sst.config.ts` to use the `*:v0.1.0` refs. - -**Step 4: Run test to verify it passes** - -Run: - -```bash -cd apps && yarn test api/src/box/constants/curated-images.constant.spec.ts -``` - -Expected: PASS. - -**Step 5: Commit** - -```bash -git add apps/api/src/box/constants/curated-images.constant.ts apps/api/src/box/constants/curated-images.constant.spec.ts apps/infra/sst.config.ts -git commit -m "feat: switch curated API refs to versioned agent runtime images" -``` - -## Task 5: Update Dashboard Picker Test-First - -**Files:** - -- Create: `apps/dashboard/src/components/Box/supportedBoxImages.ts` -- Create: `apps/dashboard/src/components/Box/supportedBoxImages.test.ts` -- Modify: `apps/dashboard/src/components/Box/CreateBoxSheet.tsx` - -**Step 1: Extract desired refs into a test** - -Create a dashboard test that imports `SUPPORTED_BOX_IMAGES` from `supportedBoxImages.ts` and expects the three `*:v0.1.0` refs, base first and default. - -**Step 2: Run test to verify it fails** - -Run: - -```bash -cd apps && yarn vitest run dashboard/src/components/Box/supportedBoxImages.test.ts -``` - -Expected: FAIL while the module is missing or still old. - -**Step 3: Extract production constant** - -Move the `SUPPORTED_BOX_IMAGES` array out of `CreateBoxSheet.tsx` into `supportedBoxImages.ts`, update refs to `*:v0.1.0`, and import it from the sheet. - -**Step 4: Run test to verify it passes** - -Run: - -```bash -cd apps && yarn vitest run dashboard/src/components/Box/supportedBoxImages.test.ts -``` - -Expected: PASS. - -**Step 5: Commit** - -```bash -git add apps/dashboard/src/components/Box/CreateBoxSheet.tsx apps/dashboard/src/components/Box/supportedBoxImages.ts apps/dashboard/src/components/Box/supportedBoxImages.test.ts -git commit -m "feat: switch dashboard image picker to versioned agent runtime images" -``` - -## Task 6: Final Verification - -**Files:** - -- All changed files. - -**Step 1: Run focused syntax checks** - -Run: - -```bash -bash -n scripts/images/build-agent-runtime.sh -ruby -e "require 'yaml'; YAML.load_file('.github/workflows/publish-agent-runtime-images.yml'); puts 'ok'" -``` - -Expected: both pass. - -**Step 2: Run app tests** - -Run: - -```bash -make test:apps -``` - -Expected: PASS. If the suite is too broad or blocked by environment, run the focused API and dashboard tests and report the blocker. - -**Step 3: Optional Docker dry-run** - -If Docker is available: - -```bash -PLATFORMS=linux/amd64 PUSH=0 bash scripts/images/build-agent-runtime.sh -``` - -Expected: local build succeeds for all three images. If Docker is unavailable, report that this was not run. - -**Step 4: Inspect final diff** - -Run: - -```bash -git status --short -git diff --stat origin/main...HEAD -``` - -Expected: clean working tree and scoped diff. - -**Step 5: Push and open PR** - -```bash -git push -u origin codex/agent-runtime-images-v2 -gh pr create --base main --head codex/agent-runtime-images-v2 --title "Publish versioned agent runtime images from Dockerfiles" --body-file /tmp/agent-runtime-images-pr.md -``` - -Expected: PR created for review. diff --git a/images/agent-runtime/base.Dockerfile b/images/agent-runtime/base.Dockerfile index e9a30e4fd..109f9a46d 100644 --- a/images/agent-runtime/base.Dockerfile +++ b/images/agent-runtime/base.Dockerfile @@ -45,19 +45,10 @@ RUN apt-get update \ && chmod 0440 /etc/sudoers.d/boxlite \ && rm -rf /var/lib/apt/lists/* -# Buildx provides TARGETARCH so each image copies the matching daemon binary. -ARG TARGETARCH -# Embed the Linux daemon that exposes BoxLite toolbox/session APIs inside the box. -COPY apps/dist/apps/daemon-runtime/boxlite-daemon-${TARGETARCH} /boxlite/bin/boxlite-daemon -# Embed the startup wrapper that fills required env fallbacks before launching the daemon. -COPY images/agent-runtime/start-agent-runtime.sh /boxlite/bin/start-agent-runtime -# Make embedded binaries executable and create the default user workspace. -RUN chmod 0755 /boxlite/bin/boxlite-daemon /boxlite/bin/start-agent-runtime \ - && mkdir -p /workspace +# Create the default workspace without embedding any BoxLite daemon process. +RUN mkdir -p /workspace # Users and agent commands start in /workspace. WORKDIR /workspace -# Always start through the wrapper so BOXLITE_BOX_ID fallback is applied. -ENTRYPOINT ["/boxlite/bin/start-agent-runtime"] -# Keep the container alive until the runner asks the daemon to execute work. +# Keep the box alive when the runner does not override the image command. CMD ["sleep", "infinity"] diff --git a/images/agent-runtime/node.Dockerfile b/images/agent-runtime/node.Dockerfile index e8181e0d8..4c9f999d9 100644 --- a/images/agent-runtime/node.Dockerfile +++ b/images/agent-runtime/node.Dockerfile @@ -46,19 +46,10 @@ RUN apt-get update \ && chmod 0440 /etc/sudoers.d/boxlite \ && rm -rf /var/lib/apt/lists/* -# Buildx provides TARGETARCH so each image copies the matching daemon binary. -ARG TARGETARCH -# Embed the Linux daemon that exposes BoxLite toolbox/session APIs inside the box. -COPY apps/dist/apps/daemon-runtime/boxlite-daemon-${TARGETARCH} /boxlite/bin/boxlite-daemon -# Embed the startup wrapper that fills required env fallbacks before launching the daemon. -COPY images/agent-runtime/start-agent-runtime.sh /boxlite/bin/start-agent-runtime -# Make embedded binaries executable and create the default user workspace. -RUN chmod 0755 /boxlite/bin/boxlite-daemon /boxlite/bin/start-agent-runtime \ - && mkdir -p /workspace +# Create the default workspace without embedding any BoxLite daemon process. +RUN mkdir -p /workspace # Users and agent commands start in /workspace. WORKDIR /workspace -# Always start through the wrapper so BOXLITE_BOX_ID fallback is applied. -ENTRYPOINT ["/boxlite/bin/start-agent-runtime"] -# Keep the container alive until the runner asks the daemon to execute work. +# Keep the box alive when the runner does not override the image command. CMD ["sleep", "infinity"] diff --git a/images/agent-runtime/python.Dockerfile b/images/agent-runtime/python.Dockerfile index ea176f40e..f5b6b3deb 100644 --- a/images/agent-runtime/python.Dockerfile +++ b/images/agent-runtime/python.Dockerfile @@ -50,19 +50,10 @@ RUN apt-get update \ && chmod 0440 /etc/sudoers.d/boxlite \ && rm -rf /var/lib/apt/lists/* -# Buildx provides TARGETARCH so each image copies the matching daemon binary. -ARG TARGETARCH -# Embed the Linux daemon that exposes BoxLite toolbox/session APIs inside the box. -COPY apps/dist/apps/daemon-runtime/boxlite-daemon-${TARGETARCH} /boxlite/bin/boxlite-daemon -# Embed the startup wrapper that fills required env fallbacks before launching the daemon. -COPY images/agent-runtime/start-agent-runtime.sh /boxlite/bin/start-agent-runtime -# Make embedded binaries executable and create the default user workspace. -RUN chmod 0755 /boxlite/bin/boxlite-daemon /boxlite/bin/start-agent-runtime \ - && mkdir -p /workspace +# Create the default workspace without embedding any BoxLite daemon process. +RUN mkdir -p /workspace # Users and agent commands start in /workspace. WORKDIR /workspace -# Always start through the wrapper so BOXLITE_BOX_ID fallback is applied. -ENTRYPOINT ["/boxlite/bin/start-agent-runtime"] -# Keep the container alive until the runner asks the daemon to execute work. +# Keep the box alive when the runner does not override the image command. CMD ["sleep", "infinity"] diff --git a/images/agent-runtime/start-agent-runtime.sh b/images/agent-runtime/start-agent-runtime.sh deleted file mode 100644 index b09853a3a..000000000 --- a/images/agent-runtime/start-agent-runtime.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -set -eu # Exit on missing variables or failed commands so startup fails visibly. - -mkdir -p /workspace # Ensure the default working directory exists before the daemon starts. - -if [ -z "${BOXLITE_BOX_ID:-}" ]; then - BOXLITE_BOX_ID="${BOXLITE_SANDBOX_ID:-$(hostname)}" # Prefer legacy sandbox id, then hostname, for the daemon's required box id. - export BOXLITE_BOX_ID # Make the fallback visible to boxlite-daemon. -fi - -if [ -z "${BOXLITE_SANDBOX_ID:-}" ]; then - BOXLITE_SANDBOX_ID="$BOXLITE_BOX_ID" # Keep legacy callers that still read BOXLITE_SANDBOX_ID working. - export BOXLITE_SANDBOX_ID # Make the compatibility value visible to child processes. -fi - -exec /boxlite/bin/boxlite-daemon "$@" # Replace the wrapper with the daemon so signals reach the real process. diff --git a/scripts/images/build-agent-runtime.sh b/scripts/images/build-agent-runtime.sh index ed490d4a5..ceb1af232 100755 --- a/scripts/images/build-agent-runtime.sh +++ b/scripts/images/build-agent-runtime.sh @@ -2,8 +2,6 @@ set -euo pipefail # Fail fast on command errors, unset variables, and broken pipes. ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # Repository root, also the Docker build context. -APPS_DIR="$ROOT_DIR/apps" # Go workspace root for building the daemon binary. -DAEMON_OUT_DIR="$APPS_DIR/dist/apps/daemon-runtime" # Build output directory that Dockerfiles copy from. VERSION_FILE="$ROOT_DIR/images/agent-runtime/VERSION" # Agent image release version source of truth. REGISTRY="${REGISTRY:-ghcr.io/boxlite-ai}" # Target registry namespace for the three image packages. @@ -44,10 +42,9 @@ normalize_tag() { # Accept TAG or VERSION overrides and normalize them to vMAJOR printf '%s\n' "$tag" } -platform_to_arch() { # Convert Docker platform strings to GOARCH and Dockerfile TARGETARCH values. +validate_platform() { # Accept only the CPU architectures BoxLite publishes for these images. case "$1" in - linux/amd64) printf 'amd64\n' ;; - linux/arm64) printf 'arm64\n' ;; + linux/amd64 | linux/arm64) ;; *) echo "Unsupported platform '$1'; expected linux/amd64 or linux/arm64" >&2 exit 1 @@ -55,36 +52,17 @@ platform_to_arch() { # Convert Docker platform strings to GOARCH and Dockerfile esac } -split_platforms() { # Validate the comma-separated PLATFORMS input before any build starts. +parse_platforms() { # Validate the comma-separated PLATFORMS input before any build starts. local raw="$1" - local -a out=() - IFS=',' read -ra out <<< "$raw" - for platform in "${out[@]}"; do + REQUESTED_PLATFORMS=() + IFS=',' read -ra REQUESTED_PLATFORMS <<< "$raw" + for platform in "${REQUESTED_PLATFORMS[@]}"; do if [[ -z "$platform" ]]; then echo "Invalid empty platform in PLATFORMS=$raw" >&2 exit 1 fi - platform_to_arch "$platform" >/dev/null + validate_platform "$platform" done - printf '%s\n' "${out[@]}" -} - -build_daemon() { # Build one Linux daemon binary per requested architecture. - local platform="$1" - local arch - arch="$(platform_to_arch "$platform")" - - mkdir -p "$DAEMON_OUT_DIR" - - echo "==> Building daemon runtime binary for $platform" - ( - cd "$APPS_DIR" - # Build a static Linux daemon so Dockerfiles can copy it into the matching image architecture. - GOOS=linux GOARCH="$arch" CGO_ENABLED=0 \ - go build -o "$DAEMON_OUT_DIR/boxlite-daemon-$arch" ./daemon/cmd/daemon/ - ) - - file "$DAEMON_OUT_DIR/boxlite-daemon-$arch" # Print architecture metadata for CI logs and review. } build_image() { # Build or publish one of base, python, or node with the shared version tag. @@ -112,14 +90,8 @@ build_image() { # Build or publish one of base, python, or node with the shared } TAG="$(normalize_tag)" # Final Docker tag such as v0.1.0. -REQUESTED_PLATFORMS=() # Parsed platform list used for per-arch daemon builds. -while IFS= read -r platform; do - REQUESTED_PLATFORMS+=("$platform") -done < <(split_platforms "$PLATFORMS") - -for platform in "${REQUESTED_PLATFORMS[@]}"; do - build_daemon "$platform" # Produce boxlite-daemon-amd64 or boxlite-daemon-arm64 before Docker build. -done +REQUESTED_PLATFORMS=() # Parsed platform list used for validation and local output mode selection. +parse_platforms "$PLATFORMS" for image in base python node; do build_image "$image" "$TAG" # Publish all three runtime variants with the same version tag.