Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ apps/.nx
# Build outputs
target
dist
apps/dist
apps/dist/*
coverage
apps/coverage
*.log
Expand Down
76 changes: 76 additions & 0 deletions .github/workflows/publish-agent-runtime-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Publish Agent Runtime Images

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better name


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
6 changes: 3 additions & 3 deletions apps/api/src/box/constants/curated-images.constant.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
])
})

Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/box/constants/curated-images.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
},
]

Expand Down
22 changes: 1 addition & 21 deletions apps/dashboard/src/components/Box/CreateBoxSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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._-]*$/

Expand Down Expand Up @@ -80,27 +81,6 @@ const BOX_CREATE_DEFAULTS: Record<ResourceFieldName, string> = {
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
Expand Down
13 changes: 13 additions & 0 deletions apps/dashboard/src/components/Box/supportedBoxImages.test.ts
Original file line number Diff line number Diff line change
@@ -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 })
})
})
20 changes: 20 additions & 0 deletions apps/dashboard/src/components/Box/supportedBoxImages.ts
Original file line number Diff line number Diff line change
@@ -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
10 changes: 5 additions & 5 deletions apps/infra/sst.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
48 changes: 7 additions & 41 deletions apps/scripts/local-dex-env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand All @@ -195,51 +194,18 @@ 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.
}
}

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)) {
Expand Down
90 changes: 90 additions & 0 deletions docs/plans/agent-runtime-images-versioned-design.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions images/agent-runtime/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0
Loading
Loading