diff --git a/.dockerignore b/.dockerignore index 942fcb6f4..3aaa7df83 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,7 +13,7 @@ apps/.nx # Build outputs target dist -apps/dist +apps/dist/* coverage apps/coverage *.log diff --git a/.github/workflows/publish-agent-runtime-images.yml b/.github/workflows/publish-agent-runtime-images.yml new file mode 100644 index 000000000..07059afc3 --- /dev/null +++ b/.github/workflows/publish-agent-runtime-images.yml @@ -0,0 +1,76 @@ +name: Publish Agent Runtime Images + +on: + push: + branches: [main] # Publish only after the PR lands on main. + paths: + - '.dockerignore' # Docker context changes can change the published image contents. + - 'images/agent-runtime/VERSION' # Version bumps are the normal way to publish a new tag. + - '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: + inputs: + version: + description: 'Version tag to publish, with or without leading v. Defaults to images/agent-runtime/VERSION.' + required: false + type: string + +permissions: + contents: read # Checkout only needs repository read access. + packages: write # GHCR push requires package write access. + +concurrency: + 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: + name: Publish agent runtime images + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # Pin checkout for supply-chain stability. + with: + persist-credentials: false # Later steps do not need git credentials. + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # Enable cross-arch build emulation. + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # Buildx is required for multi-arch images. + + - name: Log in to GHCR + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # Authenticate Docker so buildx can push to GHCR. + with: + 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 }} # Optional manual override from workflow_dispatch. + run: | + set -euo pipefail + + if [ -n "${INPUT_VERSION:-}" ]; then + version="${INPUT_VERSION#v}" + else + 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 + echo "Invalid version '$version'; expected MAJOR.MINOR.PATCH" >&2 + exit 1 + fi + + echo "tag=v$version" >> "$GITHUB_OUTPUT" # Share normalized Docker tag with the publish step. + + - name: Publish images + env: + 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.spec.ts b/apps/api/src/box/constants/curated-images.constant.spec.ts index 2fda47f59..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: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: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 f70a15208..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:20260605-p0-r3', + 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:20260605-p0-r3', + 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:20260605-p0-r3', + 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/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..bd4a30e8b --- /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: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 new file mode 100644 index 000000000..24dc4293e --- /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: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', // 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', // 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 859b29296..a0ecdaf04 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.1.0'), 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: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:20260605-p0-r3', + '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:20260605-p0-r3', + '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 6fcc81304..b68307cf7 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 || '20260605-p0-r5-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'), } @@ -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() @@ -179,9 +178,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 +194,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. } } @@ -206,40 +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 outputPath = path.join(outputDir, 'boxlite-daemon') - 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'], { - cwd: appsRoot, - encoding: 'utf8', - stdio: 'inherit', - env: { - ...process.env, - GOOS: 'linux', - GOARCH: runtimeImageGoarch(config), - CGO_ENABLED: '0', - }, - }) - - 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 new file mode 100644 index 000000000..a8cdb2e92 --- /dev/null +++ b/docs/plans/agent-runtime-images-versioned-design.md @@ -0,0 +1,90 @@ +# Agent Runtime Images Design + +## Goal + +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 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` + +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 + +Use the existing package names: + +- `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 `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 or retag older package versions. + +## Architecture + +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. + +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 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 + +- 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 `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 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 `*: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: + +- `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. 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/images/agent-runtime/base.Dockerfile b/images/agent-runtime/base.Dockerfile new file mode 100644 index 000000000..109f9a46d --- /dev/null +++ b/images/agent-runtime/base.Dockerfile @@ -0,0 +1,54 @@ +# 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 \ + 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/* + +# Create the default workspace without embedding any BoxLite daemon process. +RUN mkdir -p /workspace + +# Users and agent commands start in /workspace. +WORKDIR /workspace +# 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 new file mode 100644 index 000000000..4c9f999d9 --- /dev/null +++ b/images/agent-runtime/node.Dockerfile @@ -0,0 +1,55 @@ +# 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 \ + 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/* + +# Create the default workspace without embedding any BoxLite daemon process. +RUN mkdir -p /workspace + +# Users and agent commands start in /workspace. +WORKDIR /workspace +# 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 new file mode 100644 index 000000000..f5b6b3deb --- /dev/null +++ b/images/agent-runtime/python.Dockerfile @@ -0,0 +1,59 @@ +# 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 \ + 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/* + +# Create the default workspace without embedding any BoxLite daemon process. +RUN mkdir -p /workspace + +# Users and agent commands start in /workspace. +WORKDIR /workspace +# Keep the box alive when the runner does not override the image command. +CMD ["sleep", "infinity"] diff --git a/scripts/images/build-agent-runtime.sh b/scripts/images/build-agent-runtime.sh new file mode 100755 index 000000000..ceb1af232 --- /dev/null +++ b/scripts/images/build-agent-runtime.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +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. +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. +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 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" # Strip newline so the value can be embedded in Docker tags. +} + +normalize_tag() { # Accept TAG or VERSION overrides and normalize them to vMAJOR.MINOR.PATCH. + local version tag + + if [[ -n "${TAG:-}" ]]; then + tag="$TAG" + else + version="${VERSION:-$(read_runtime_image_version)}" + if [[ -z "$version" ]]; then + echo "Unable to derive version from $VERSION_FILE; 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" +} + +validate_platform() { # Accept only the CPU architectures BoxLite publishes for these images. + case "$1" in + linux/amd64 | linux/arm64) ;; + *) + echo "Unsupported platform '$1'; expected linux/amd64 or linux/arm64" >&2 + exit 1 + ;; + esac +} + +parse_platforms() { # Validate the comma-separated PLATFORMS input before any build starts. + local raw="$1" + 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 + validate_platform "$platform" + done +} + +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" # 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 + exit 1 + fi + + if [[ "$PUSH" == "1" || "$PUSH" == "true" ]]; then + build_args+=(--push) # CI publish path writes the multi-arch manifest to GHCR. + elif [[ "${#REQUESTED_PLATFORMS[@]}" -eq 1 ]]; then + build_args+=(--load) # Single-platform local validation loads the image into Docker. + else + 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)" # Final Docker tag such as v0.1.0. +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. +done