Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ DATABASE_URL=
# REDIS_URL=redis://localhost:6379/0
REDIS_URL=

# --- NVIDIA DGX Spark / local model server ---
# docker-compose.spark.yml defaults these for container-to-host access.
# SANDCASTLE_SPARK_MODE=on
# DATA_RESIDENCY=local
# NIM_BASE_URL=http://host.docker.internal:8000
# OLLAMA_HOST=http://host.docker.internal:11434
# SPARK_SANDBOX_BACKEND=docker
# DOCKER_IMAGE=sandcastle-runner:latest
# DOCKER_GID= # set with: stat -c '%g' /var/run/docker.sock

# --- Storage ---
# "local" = filesystem (default), "s3" = S3/MinIO
STORAGE_BACKEND=local
Expand All @@ -39,4 +49,10 @@ DATA_DIR=./data
# --- Optional ---
WEBHOOK_SECRET=your-webhook-signing-secret
AUTH_REQUIRED=false
# Required when AUTH_REQUIRED=true (Docker production mode sets it to true).
# Generate with: openssl rand -hex 32
API_KEY_PEPPER=
# Bootstrap admin key created on startup when set.
# Generate with: printf 'sc_%s\n' "$(openssl rand -hex 24)"
ADMIN_API_KEY=
LOG_LEVEL=info
29 changes: 29 additions & 0 deletions .github/workflows/pr-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ on:
- "src/**"
- "tests/**"
- "dashboard/**"
- "deploy/cookbooks/**"
- "cf-sandbox-worker/**"
- "Dockerfile"
- "Dockerfile.runner"
- "e2b.Dockerfile"
- "docker-compose*.yml"
- "docker-compose*.yaml"
- ".dockerignore"
- ".env.example"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/pr-tests.yml"
workflow_dispatch:

Expand Down Expand Up @@ -44,6 +54,25 @@ jobs:
python -m pip install --upgrade pip
pip install -e ".[dev,mcp,memory,otel,parse,report,docker,security]"

- name: Validate Compose files
run: |
python - <<'PY'
from pathlib import Path
import yaml

for name in (
"docker-compose.yml",
"docker-compose.local.yml",
"docker-compose.infra.yml",
"docker-compose.spark.yml",
):
data = yaml.safe_load(Path(name).read_text())
services = data.get("services") or {}
if not services:
raise SystemExit(f"{name} has no services")
print(f"{name}: {', '.join(sorted(services))}")
PY

- name: Run pytest
run: |
python -m pytest tests/ -q --tb=line --timeout=60
Expand Down
49 changes: 44 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,34 @@ jobs:
python-version: "3.12"
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- name: Build dashboard
run: cd dashboard && npm ci && npm run build
- name: TypeScript check
working-directory: dashboard
run: npx tsc --noEmit
- name: Install dependencies
run: pip install -e ".[dev]"
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,docker,mcp,security]"
- name: Validate Compose files
run: |
python - <<'PY'
from pathlib import Path
import yaml

for name in (
"docker-compose.yml",
"docker-compose.local.yml",
"docker-compose.infra.yml",
"docker-compose.spark.yml",
):
data = yaml.safe_load(Path(name).read_text())
services = data.get("services") or {}
if not services:
raise SystemExit(f"{name} has no services")
print(f"{name}: {', '.join(sorted(services))}")
PY
- name: Lint
run: ruff check src/
- name: Test
Expand All @@ -40,7 +60,7 @@ jobs:
python-version: "3.12"
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- name: Build dashboard
run: cd dashboard && npm ci && npm run build
- name: Check if version already on PyPI
Expand All @@ -58,10 +78,29 @@ jobs:
fi
- name: Install build tools
if: steps.check.outputs.exists == 'false'
run: pip install build
run: pip install build twine
- name: Build package
if: steps.check.outputs.exists == 'false'
run: python -m build --wheel
run: python -m build
- name: Check package metadata
if: steps.check.outputs.exists == 'false'
run: python -m twine check dist/*
- name: Verify dashboard is packaged
if: steps.check.outputs.exists == 'false'
run: |
python - <<'PY'
from pathlib import Path
import zipfile

wheels = sorted(Path("dist").glob("sandcastle_ai-*.whl"))
if len(wheels) != 1:
raise SystemExit(f"expected one wheel, found {len(wheels)}")
with zipfile.ZipFile(wheels[0]) as wheel:
names = set(wheel.namelist())
if "sandcastle/dashboard/index.html" not in names:
raise SystemExit("wheel is missing sandcastle/dashboard/index.html")
print(f"dashboard packaged in {wheels[0].name}")
PY
- name: Publish to PyPI
if: steps.check.outputs.exists == 'false'
uses: pypa/gh-action-pypi-publish@release/v1
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.40.1] - 2026-07-11 - "Docker on Your Box"

Patch release for teams installing Sandcastle through Docker Compose and for NVIDIA DGX Spark
deployments that run Sandcastle, local model servers, and Docker sandboxes on the same host.

### Added
- Added `docker-compose.spark.yml`, a full-stack Spark override that enables Docker sandboxes,
host-local NIM/Ollama/vLLM access, Docker socket group wiring, and a prebuilt
`sandcastle-runner:latest` runner image.
- Added explicit Spark/Docker environment examples to `.env.example` and README, including
`SPARK_SANDBOX_BACKEND`, `DOCKER_GID`, `NIM_BASE_URL`, and `OLLAMA_HOST`.
- Added an infra-only Compose file (`docker-compose.infra.yml`) for PostgreSQL, Redis, and
MinIO-only development setups.

### Changed
- The production Docker image now builds the dashboard before packaging, includes `README.md`,
and installs Sandcastle with the `docker` extra so `aiodocker` is present for the Docker
sandbox backend.
- Docker Compose startup now runs migrations before the API, scheduler, and worker services.
- Docker runner images now pin Playwright, Claude Agent SDK, and OpenAI npm package versions for
reproducible builds; the self-hosted Docker cookbook now defaults to Playwright 1.61.1.
- Ruff's target version now matches the package's Python 3.12+ runtime support.

### Fixed
- Fixed Docker builds failing because the package wheel expected `README.md` and
`dashboard/dist` to exist inside the image build context.
- Fixed authenticated Docker Compose deployments starting without required `API_KEY_PEPPER` and
`ADMIN_API_KEY` values.
- Fixed Docker-backed Spark deployments where sandbox runner containers could not reach
host-local model servers by adding `host.docker.internal:host-gateway` wiring.
- Fixed the `ocr` extra by using the published `chandra-ocr` version range, and avoided a yanked
`grpcio` release in the optional memory dependency path.
- Fixed LightPanda cookbook downloads on ARM64 hosts by selecting the architecture-specific
Linux binary.
- Fixed self-hosted sandbox cookbooks that tried to install the `ant` CLI from a non-existent
npm package; they now use the official Anthropic CLI release binary pinned to `v1.17.0`.
- Fixed cookbook Docker build blockers caused by an invalid optional `COPY requirements.txt`
pattern and a Daytona `COPY skills/` instruction pointing at a directory that is not shipped.
## [0.40.0] - 2026-06-11 - "Build Once, Run Anywhere"

Describe what you want in plain English and Sandcastle builds the workflow. Run it on any model — Claude, GPT, Mistral, a local model on your own box — and move it between them with one line. Deploy it your way: cloud, your server, fully air-gapped, EU-only. And it gets better over time, on its own. Build any agent once; run it on any model, anywhere; watch it improve.
Expand Down
19 changes: 16 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
# -- Builder stage --
# -- Dashboard builder stage --
FROM node:20-slim AS dashboard-builder

WORKDIR /dashboard

COPY dashboard/package*.json ./
RUN npm ci

COPY dashboard/ ./
RUN npm run build


# -- Python builder stage --
FROM python:3.12-slim AS builder

WORKDIR /build
Expand All @@ -7,13 +19,14 @@ WORKDIR /build
RUN pip install --no-cache-dir hatchling

# Copy project metadata and source for pip install
COPY pyproject.toml ./
COPY pyproject.toml README.md ./
COPY src/ src/
COPY --from=dashboard-builder /dashboard/dist dashboard/dist/

# Build a wheel and install into a virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir .
RUN pip install --no-cache-dir ".[docker]"


# -- Runtime stage --
Expand Down
23 changes: 17 additions & 6 deletions Dockerfile.runner
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
FROM node:20-slim

RUN useradd --uid 1000 --create-home runner
ARG PLAYWRIGHT_VERSION=1.61.1
ARG CLAUDE_AGENT_SDK_VERSION=0.3.207
ARG OPENAI_VERSION=6.46.0

ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright

RUN useradd --uid 1000 --create-home --home-dir /home/user runner

WORKDIR /home/user

# Pre-install Playwright + Chromium as root so browsers land in a system path
# accessible to the runner user. This eliminates the ~60s cold-start penalty
# that occurs when playwright/chromium must be installed at step runtime.
RUN npm install playwright && npx playwright install --with-deps chromium
RUN npm install \
"playwright@${PLAYWRIGHT_VERSION}" \
"@anthropic-ai/claude-agent-sdk@${CLAUDE_AGENT_SDK_VERSION}" \
"openai@${OPENAI_VERSION}" \
&& npx playwright install --with-deps chromium \
&& npm cache clean --force \
&& chmod -R a+rX /ms-playwright \
&& chown -R runner:runner /home/user /ms-playwright

USER runner
WORKDIR /home/user

# Pre-install agent SDKs so sandbox start is fast
RUN npm install @anthropic-ai/claude-agent-sdk openai && npm cache clean --force

ENV NODE_PATH=/home/user/node_modules
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Build once. Run anywhere.** Sandcastle is an open-source, production-ready orchestrator for AI agents. **Describe a workflow in plain English and it builds it** (or write the YAML yourself); run it on **any model** — Claude, GPT, Mistral, or a local model on your own box — and move between them with one line; deploy it **your way** — cloud, your own server, fully air-gapped, or EU-only; and it **gets better over time**, on its own. Local models run at `$0/run` with hard data-residency enforcement and a tamper-evident audit trail; the cloud is there too, with 7 providers and auto-failover, 22 step types, verified templates, and a full dashboard. Sovereign by default. European-built.

[![PyPI](https://img.shields.io/badge/PyPI-v0.40.0-blue?style=flat-square)](https://pypi.org/project/sandcastle-ai/0.40.0/)
[![PyPI](https://img.shields.io/badge/PyPI-v0.40.1-blue?style=flat-square)](https://pypi.org/project/sandcastle-ai/0.40.1/)
[![License: BSL 1.1](https://img.shields.io/badge/License-BSL_1.1-blue.svg)](LICENSE)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![Tests](https://img.shields.io/badge/tests-17900%2B%20passing-brightgreen?style=flat-square)](https://github.com/gizmax/Sandcastle/actions)
Expand Down
6 changes: 4 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ S3-compatible storage (agent data persistence).
```
sandcastle/
├── sandcastle.yaml.example # Example workflow definition
├── docker-compose.yaml # Redis + Postgres + MinIO for local dev
├── docker-compose.yml # Full application stack
├── docker-compose.spark.yml # DGX Spark / local GPU inference override
├── docker-compose.infra.yml # Redis + Postgres + MinIO for local dev
├── pyproject.toml
├── .env.example
├── README.md
Expand Down Expand Up @@ -756,7 +758,7 @@ TEST: curl with a simple 2-step workflow → get result back.
9. queue/worker.py -arq worker
10. api/routes.py -add POST /workflows/run (async) + GET /runs/{id}
11. engine/storage.py -S3 storage backend
12. docker-compose.yaml
12. docker-compose.yml / docker-compose.spark.yml / docker-compose.infra.yml

TEST: Submit workflow, poll /runs/{id}, see status progress.

Expand Down
8 changes: 7 additions & 1 deletion cf-sandbox-worker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
FROM node:20-slim

ARG CLAUDE_AGENT_SDK_VERSION=0.3.207
ARG OPENAI_VERSION=6.46.0

RUN useradd --uid 1000 --create-home runner
USER runner
WORKDIR /home/user

RUN npm install @anthropic-ai/claude-agent-sdk openai && npm cache clean --force
RUN npm install \
"@anthropic-ai/claude-agent-sdk@${CLAUDE_AGENT_SDK_VERSION}" \
"openai@${OPENAI_VERSION}" \
&& npm cache clean --force

ENV NODE_PATH=/home/user/node_modules
25 changes: 20 additions & 5 deletions deploy/cookbooks/cloudflare/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

ARG PYTHON_VERSION=3.12
ARG NODE_VERSION=22
ARG ANT_VERSION=latest
ARG ANT_VERSION=v1.17.0

# ---------------------------------------------------------------------------
# Stage 1: builder
Expand All @@ -24,13 +24,14 @@ FROM python:${PYTHON_VERSION}-slim-bookworm AS builder

ARG NODE_VERSION
ARG ANT_VERSION
ARG TARGETARCH

ENV DEBIAN_FRONTEND=noninteractive \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1

# Documented runtime requirements for the ant CLI: bash, tar, unzip, curl,
# ca-certificates. Node is needed because ant ships as an npm package.
# Documented worker requirements: bash, tar, unzip, curl, ca-certificates.
# Node 22 stays available for JavaScript tool calls; ant is a release binary.
RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
ca-certificates \
Expand All @@ -43,7 +44,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

RUN npm install -g "@anthropic-ai/ant@${ANT_VERSION}"
RUN mkdir -p /tmp/ant \
&& ANT_VERSION_NO_V="${ANT_VERSION#v}" \
&& case "${TARGETARCH:-$(dpkg --print-architecture)}" in \
amd64|x86_64) ANT_ARCH="amd64" ;; \
arm64|aarch64) ANT_ARCH="arm64" ;; \
*) echo "Unsupported ant arch: ${TARGETARCH:-$(dpkg --print-architecture)}"; exit 1 ;; \
esac \
&& curl -fsSL -o /tmp/ant/ant.tar.gz \
"https://github.com/anthropics/anthropic-cli/releases/download/${ANT_VERSION}/ant_${ANT_VERSION_NO_V}_linux_${ANT_ARCH}.tar.gz" \
&& tar -xzf /tmp/ant/ant.tar.gz -C /tmp/ant \
&& install -m 0755 /tmp/ant/ant /usr/local/bin/ant \
&& ant --version \
&& rm -rf /tmp/ant

# ---------------------------------------------------------------------------
# Stage 2: runtime
Expand All @@ -66,9 +79,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

# Carry over Node + ant CLI from the builder stage.
COPY --from=builder /usr/local/bin/ant /usr/local/bin/ant
COPY --from=builder /usr/bin/node /usr/bin/node
COPY --from=builder /usr/bin/npm /usr/bin/npm
COPY --from=builder /usr/lib/node_modules /usr/lib/node_modules
RUN ln -s /usr/lib/node_modules/@anthropic-ai/ant/bin/ant /usr/local/bin/ant
RUN ant --version

# Non-root user. UID 10001 matches the docker/ cookbook so the Sandcastle
# worker volume permissions line up across runtimes.
Expand Down
Loading
Loading