mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-18 07:01:34 +00:00
Merge remote-tracking branch 'origin/full-docker-defaults' into async-action-fixes
# Conflicts: # foundry/CLAUDE.md # foundry/packages/backend/src/actors/task/workbench.ts # foundry/packages/frontend/src/components/dev-panel.tsx # foundry/packages/frontend/src/components/mock-layout.tsx # justfile
This commit is contained in:
commit
14d5413f8a
30 changed files with 521 additions and 413 deletions
18
.github/workflows/release.yaml
vendored
18
.github/workflows/release.yaml
vendored
|
|
@ -180,10 +180,20 @@ jobs:
|
||||||
include:
|
include:
|
||||||
- platform: linux/arm64
|
- platform: linux/arm64
|
||||||
runner: depot-ubuntu-24.04-arm-8
|
runner: depot-ubuntu-24.04-arm-8
|
||||||
arch_suffix: -arm64
|
tag_suffix: -arm64
|
||||||
|
dockerfile: docker/runtime/Dockerfile
|
||||||
- platform: linux/amd64
|
- platform: linux/amd64
|
||||||
runner: depot-ubuntu-24.04-8
|
runner: depot-ubuntu-24.04-8
|
||||||
arch_suffix: -amd64
|
tag_suffix: -amd64
|
||||||
|
dockerfile: docker/runtime/Dockerfile
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: depot-ubuntu-24.04-arm-8
|
||||||
|
tag_suffix: -full-arm64
|
||||||
|
dockerfile: docker/runtime/Dockerfile.full
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: depot-ubuntu-24.04-8
|
||||||
|
tag_suffix: -full-amd64
|
||||||
|
dockerfile: docker/runtime/Dockerfile.full
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -205,8 +215,8 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: rivetdev/sandbox-agent:${{ steps.vars.outputs.sha_short }}${{ matrix.arch_suffix }}
|
tags: rivetdev/sandbox-agent:${{ steps.vars.outputs.sha_short }}${{ matrix.tag_suffix }}
|
||||||
file: docker/runtime/Dockerfile
|
file: ${{ matrix.dockerfile }}
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
build-args: |
|
build-args: |
|
||||||
TARGETARCH=${{ contains(matrix.platform, 'arm64') && 'arm64' || 'amd64' }}
|
TARGETARCH=${{ contains(matrix.platform, 'arm64') && 'arm64' || 'amd64' }}
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@
|
||||||
## Docker Examples (Dev Testing)
|
## Docker Examples (Dev Testing)
|
||||||
|
|
||||||
- When manually testing bleeding-edge (unreleased) versions of sandbox-agent in `examples/`, use `SANDBOX_AGENT_DEV=1` with the Docker-based examples.
|
- When manually testing bleeding-edge (unreleased) versions of sandbox-agent in `examples/`, use `SANDBOX_AGENT_DEV=1` with the Docker-based examples.
|
||||||
- This triggers `examples/shared/Dockerfile.dev` which builds the server binary from local source and packages it into the Docker image.
|
- This triggers a local build of `docker/runtime/Dockerfile.full` which builds the server binary from local source and packages it into the Docker image.
|
||||||
- Example: `SANDBOX_AGENT_DEV=1 pnpm --filter @sandbox-agent/example-mcp start`
|
- Example: `SANDBOX_AGENT_DEV=1 pnpm --filter @sandbox-agent/example-mcp start`
|
||||||
|
|
||||||
## Install Version References
|
## Install Version References
|
||||||
|
|
@ -152,7 +152,7 @@
|
||||||
- `.claude/commands/post-release-testing.md`
|
- `.claude/commands/post-release-testing.md`
|
||||||
- `examples/cloudflare/Dockerfile`
|
- `examples/cloudflare/Dockerfile`
|
||||||
- `examples/daytona/src/index.ts`
|
- `examples/daytona/src/index.ts`
|
||||||
- `examples/daytona/src/daytona-with-snapshot.ts`
|
- `examples/shared/src/docker.ts`
|
||||||
- `examples/docker/src/index.ts`
|
- `examples/docker/src/index.ts`
|
||||||
- `examples/e2b/src/index.ts`
|
- `examples/e2b/src/index.ts`
|
||||||
- `examples/vercel/src/index.ts`
|
- `examples/vercel/src/index.ts`
|
||||||
|
|
|
||||||
|
|
@ -143,10 +143,7 @@ sandbox-agent server --token "$SANDBOX_TOKEN" --host 127.0.0.1 --port 2468
|
||||||
Optional: preinstall agent binaries (no server required; they will be installed lazily on first use if you skip this):
|
Optional: preinstall agent binaries (no server required; they will be installed lazily on first use if you skip this):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent install-agent claude
|
sandbox-agent install-agent --all
|
||||||
sandbox-agent install-agent codex
|
|
||||||
sandbox-agent install-agent opencode
|
|
||||||
sandbox-agent install-agent amp
|
|
||||||
```
|
```
|
||||||
|
|
||||||
To disable auth locally:
|
To disable auth locally:
|
||||||
|
|
|
||||||
|
|
@ -167,4 +167,4 @@ WORKDIR /home/sandbox
|
||||||
EXPOSE 2468
|
EXPOSE 2468
|
||||||
|
|
||||||
ENTRYPOINT ["sandbox-agent"]
|
ENTRYPOINT ["sandbox-agent"]
|
||||||
CMD ["--host", "0.0.0.0", "--port", "2468"]
|
CMD ["server", "--host", "0.0.0.0", "--port", "2468"]
|
||||||
|
|
|
||||||
162
docker/runtime/Dockerfile.full
Normal file
162
docker/runtime/Dockerfile.full
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
# syntax=docker/dockerfile:1.10.0
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Build inspector frontend
|
||||||
|
# ============================================================================
|
||||||
|
FROM node:22-alpine AS inspector-build
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
|
||||||
|
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
|
||||||
|
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
|
||||||
|
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
|
||||||
|
COPY sdks/react/package.json ./sdks/react/
|
||||||
|
COPY sdks/typescript/package.json ./sdks/typescript/
|
||||||
|
|
||||||
|
RUN pnpm install --filter @sandbox-agent/inspector...
|
||||||
|
|
||||||
|
COPY docs/openapi.json ./docs/
|
||||||
|
COPY sdks/cli-shared ./sdks/cli-shared
|
||||||
|
COPY sdks/acp-http-client ./sdks/acp-http-client
|
||||||
|
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
|
||||||
|
COPY sdks/react ./sdks/react
|
||||||
|
COPY sdks/typescript ./sdks/typescript
|
||||||
|
|
||||||
|
RUN cd sdks/cli-shared && pnpm exec tsup
|
||||||
|
RUN cd sdks/acp-http-client && pnpm exec tsup
|
||||||
|
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
|
||||||
|
RUN cd sdks/persist-indexeddb && pnpm exec tsup
|
||||||
|
RUN cd sdks/react && pnpm exec tsup
|
||||||
|
|
||||||
|
COPY frontend/packages/inspector ./frontend/packages/inspector
|
||||||
|
RUN cd frontend/packages/inspector && pnpm exec vite build
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# AMD64 Builder - Uses cross-tools musl toolchain
|
||||||
|
# ============================================================================
|
||||||
|
FROM --platform=linux/amd64 rust:1.88.0 AS builder-amd64
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
musl-tools \
|
||||||
|
musl-dev \
|
||||||
|
llvm-14-dev \
|
||||||
|
libclang-14-dev \
|
||||||
|
clang-14 \
|
||||||
|
libssl-dev \
|
||||||
|
pkg-config \
|
||||||
|
ca-certificates \
|
||||||
|
g++ \
|
||||||
|
g++-multilib \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
wget && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN wget -q https://github.com/cross-tools/musl-cross/releases/latest/download/x86_64-unknown-linux-musl.tar.xz && \
|
||||||
|
tar -xf x86_64-unknown-linux-musl.tar.xz -C /opt/ && \
|
||||||
|
rm x86_64-unknown-linux-musl.tar.xz && \
|
||||||
|
rustup target add x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
ENV PATH="/opt/x86_64-unknown-linux-musl/bin:$PATH" \
|
||||||
|
LIBCLANG_PATH=/usr/lib/llvm-14/lib \
|
||||||
|
CLANG_PATH=/usr/bin/clang-14 \
|
||||||
|
CC_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-gcc \
|
||||||
|
CXX_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-g++ \
|
||||||
|
AR_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-ar \
|
||||||
|
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-unknown-linux-musl-gcc \
|
||||||
|
CARGO_INCREMENTAL=0 \
|
||||||
|
CARGO_NET_GIT_FETCH_WITH_CLI=true
|
||||||
|
|
||||||
|
ENV SSL_VER=1.1.1w
|
||||||
|
RUN wget https://www.openssl.org/source/openssl-$SSL_VER.tar.gz && \
|
||||||
|
tar -xzf openssl-$SSL_VER.tar.gz && \
|
||||||
|
cd openssl-$SSL_VER && \
|
||||||
|
./Configure no-shared no-async --prefix=/musl --openssldir=/musl/ssl linux-x86_64 && \
|
||||||
|
make -j$(nproc) && \
|
||||||
|
make install_sw && \
|
||||||
|
cd .. && \
|
||||||
|
rm -rf openssl-$SSL_VER*
|
||||||
|
|
||||||
|
ENV OPENSSL_DIR=/musl \
|
||||||
|
OPENSSL_INCLUDE_DIR=/musl/include \
|
||||||
|
OPENSSL_LIB_DIR=/musl/lib \
|
||||||
|
PKG_CONFIG_ALLOW_CROSS=1 \
|
||||||
|
RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-static-libgcc"
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/git \
|
||||||
|
--mount=type=cache,target=/build/target \
|
||||||
|
cargo build -p sandbox-agent --release --target x86_64-unknown-linux-musl && \
|
||||||
|
cp target/x86_64-unknown-linux-musl/release/sandbox-agent /sandbox-agent
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ARM64 Builder - Uses Alpine with native musl
|
||||||
|
# ============================================================================
|
||||||
|
FROM --platform=linux/arm64 rust:1.88-alpine AS builder-arm64
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
musl-dev \
|
||||||
|
clang \
|
||||||
|
llvm-dev \
|
||||||
|
openssl-dev \
|
||||||
|
openssl-libs-static \
|
||||||
|
pkgconfig \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
build-base
|
||||||
|
|
||||||
|
RUN rustup target add aarch64-unknown-linux-musl
|
||||||
|
|
||||||
|
ENV CARGO_INCREMENTAL=0 \
|
||||||
|
CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
||||||
|
RUSTFLAGS="-C target-feature=+crt-static"
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/git \
|
||||||
|
--mount=type=cache,target=/build/target \
|
||||||
|
cargo build -p sandbox-agent --release --target aarch64-unknown-linux-musl && \
|
||||||
|
cp target/aarch64-unknown-linux-musl/release/sandbox-agent /sandbox-agent
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Select the appropriate builder based on target architecture
|
||||||
|
# ============================================================================
|
||||||
|
ARG TARGETARCH
|
||||||
|
FROM builder-${TARGETARCH} AS builder
|
||||||
|
|
||||||
|
# Runtime stage - full image with all supported agents preinstalled
|
||||||
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
bash \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=builder /sandbox-agent /usr/local/bin/sandbox-agent
|
||||||
|
RUN chmod +x /usr/local/bin/sandbox-agent
|
||||||
|
|
||||||
|
RUN useradd -m -s /bin/bash sandbox
|
||||||
|
USER sandbox
|
||||||
|
WORKDIR /home/sandbox
|
||||||
|
|
||||||
|
RUN sandbox-agent install-agent --all
|
||||||
|
|
||||||
|
EXPOSE 2468
|
||||||
|
|
||||||
|
ENTRYPOINT ["sandbox-agent"]
|
||||||
|
CMD ["server", "--host", "0.0.0.0", "--port", "2468"]
|
||||||
12
docs/cli.mdx
12
docs/cli.mdx
|
|
@ -39,20 +39,24 @@ Notes:
|
||||||
|
|
||||||
## install-agent
|
## install-agent
|
||||||
|
|
||||||
Install or reinstall a single agent.
|
Install or reinstall a single agent, or every supported agent with `--all`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent install-agent <AGENT> [OPTIONS]
|
sandbox-agent install-agent [<AGENT>] [OPTIONS]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
|
| `--all` | Install every supported agent |
|
||||||
| `-r, --reinstall` | Force reinstall |
|
| `-r, --reinstall` | Force reinstall |
|
||||||
| `--agent-version <VERSION>` | Override agent package version |
|
| `--agent-version <VERSION>` | Override agent package version (conflicts with `--all`) |
|
||||||
| `--agent-process-version <VERSION>` | Override agent process version |
|
| `--agent-process-version <VERSION>` | Override agent process version (conflicts with `--all`) |
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent install-agent claude --reinstall
|
sandbox-agent install-agent claude --reinstall
|
||||||
|
sandbox-agent install-agent --all
|
||||||
```
|
```
|
||||||
|
|
||||||
## opencode (experimental)
|
## opencode (experimental)
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,18 @@ Docker is not recommended for production isolation of untrusted workloads. Use d
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Run Sandbox Agent with agents pre-installed:
|
Run the published full image with all supported agents pre-installed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm -p 3000:3000 \
|
docker run --rm -p 3000:3000 \
|
||||||
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
||||||
-e OPENAI_API_KEY="$OPENAI_API_KEY" \
|
-e OPENAI_API_KEY="$OPENAI_API_KEY" \
|
||||||
alpine:latest sh -c "\
|
rivetdev/sandbox-agent:0.3.1-full \
|
||||||
apk add --no-cache curl ca-certificates libstdc++ libgcc bash nodejs npm && \
|
server --no-token --host 0.0.0.0 --port 3000
|
||||||
curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh && \
|
|
||||||
sandbox-agent server --no-token --host 0.0.0.0 --port 3000"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `0.3.1-full` tag pins the exact version. The moving `full` tag is also published for contributors who want the latest full image.
|
||||||
|
|
||||||
## TypeScript with dockerode
|
## TypeScript with dockerode
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|
@ -31,14 +31,8 @@ const docker = new Docker();
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
|
|
||||||
const container = await docker.createContainer({
|
const container = await docker.createContainer({
|
||||||
Image: "node:22-bookworm-slim",
|
Image: "rivetdev/sandbox-agent:0.3.1-full",
|
||||||
Cmd: ["sh", "-c", [
|
Cmd: ["server", "--no-token", "--host", "0.0.0.0", "--port", `${PORT}`],
|
||||||
"apt-get update",
|
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6",
|
|
||||||
"rm -rf /var/lib/apt/lists/*",
|
|
||||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
|
||||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`,
|
|
||||||
].join(" && ")],
|
|
||||||
Env: [
|
Env: [
|
||||||
`ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`,
|
`ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`,
|
||||||
`OPENAI_API_KEY=${process.env.OPENAI_API_KEY}`,
|
`OPENAI_API_KEY=${process.env.OPENAI_API_KEY}`,
|
||||||
|
|
@ -60,6 +54,29 @@ const session = await sdk.createSession({ agent: "codex" });
|
||||||
await session.prompt([{ type: "text", text: "Summarize this repository." }]);
|
await session.prompt([{ type: "text", text: "Summarize this repository." }]);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Building a custom image with everything preinstalled
|
||||||
|
|
||||||
|
If you need to extend your own base image, install Sandbox Agent and preinstall every supported agent in one step:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
bash ca-certificates curl git && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh && \
|
||||||
|
sandbox-agent install-agent --all
|
||||||
|
|
||||||
|
RUN useradd -m -s /bin/bash sandbox
|
||||||
|
USER sandbox
|
||||||
|
WORKDIR /home/sandbox
|
||||||
|
|
||||||
|
EXPOSE 2468
|
||||||
|
ENTRYPOINT ["sandbox-agent"]
|
||||||
|
CMD ["server", "--host", "0.0.0.0", "--port", "2468"]
|
||||||
|
```
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,11 @@ icon: "rocket"
|
||||||
|
|
||||||
<Tab title="Docker">
|
<Tab title="Docker">
|
||||||
```bash
|
```bash
|
||||||
docker run -e ANTHROPIC_API_KEY="sk-ant-..." \
|
docker run -p 2468:2468 \
|
||||||
|
-e ANTHROPIC_API_KEY="sk-ant-..." \
|
||||||
-e OPENAI_API_KEY="sk-..." \
|
-e OPENAI_API_KEY="sk-..." \
|
||||||
your-image
|
rivetdev/sandbox-agent:0.3.1-full \
|
||||||
|
server --no-token --host 0.0.0.0 --port 2468
|
||||||
```
|
```
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
@ -215,10 +217,7 @@ icon: "rocket"
|
||||||
To preinstall agents:
|
To preinstall agents:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sandbox-agent install-agent claude
|
sandbox-agent install-agent --all
|
||||||
sandbox-agent install-agent codex
|
|
||||||
sandbox-agent install-agent opencode
|
|
||||||
sandbox-agent install-agent amp
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If agents are not installed up front, they are lazily installed when creating a session.
|
If agents are not installed up front, they are lazily installed when creating a session.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsx src/index.ts",
|
"start": "tsx src/index.ts",
|
||||||
"start:snapshot": "tsx src/daytona-with-snapshot.ts",
|
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { Daytona, Image } from "@daytonaio/sdk";
|
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
|
||||||
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
|
|
||||||
|
|
||||||
const daytona = new Daytona();
|
|
||||||
|
|
||||||
const envVars: Record<string, string> = {};
|
|
||||||
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
|
||||||
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
||||||
|
|
||||||
// Build a custom image with sandbox-agent pre-installed (slower first run, faster subsequent runs)
|
|
||||||
const image = Image.base("ubuntu:22.04").runCommands(
|
|
||||||
"apt-get update && apt-get install -y curl ca-certificates",
|
|
||||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Creating Daytona sandbox (first run builds the base image and may take a few minutes, subsequent runs are fast)...");
|
|
||||||
const sandbox = await daytona.create({ envVars, image, autoStopInterval: 0 }, { timeout: 180 });
|
|
||||||
|
|
||||||
await sandbox.process.executeCommand("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
|
|
||||||
|
|
||||||
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
|
|
||||||
|
|
||||||
console.log("Connecting to server...");
|
|
||||||
const client = await SandboxAgent.connect({ baseUrl });
|
|
||||||
const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/daytona", mcpServers: [] } });
|
|
||||||
const sessionId = session.id;
|
|
||||||
|
|
||||||
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
|
||||||
console.log(" Press Ctrl+C to stop.");
|
|
||||||
|
|
||||||
const keepAlive = setInterval(() => {}, 60_000);
|
|
||||||
const cleanup = async () => {
|
|
||||||
clearInterval(keepAlive);
|
|
||||||
await sandbox.delete(60);
|
|
||||||
process.exit(0);
|
|
||||||
};
|
|
||||||
process.once("SIGINT", cleanup);
|
|
||||||
process.once("SIGTERM", cleanup);
|
|
||||||
|
|
@ -3,12 +3,13 @@ import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { SandboxAgent } from "sandbox-agent";
|
import { SandboxAgent } from "sandbox-agent";
|
||||||
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
|
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
|
||||||
|
import { FULL_IMAGE } from "@sandbox-agent/example-shared/docker";
|
||||||
|
|
||||||
const IMAGE = "node:22-bookworm-slim";
|
const IMAGE = FULL_IMAGE;
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
const agent = detectAgent();
|
const agent = detectAgent();
|
||||||
const codexAuthPath = process.env.HOME ? path.join(process.env.HOME, ".codex", "auth.json") : null;
|
const codexAuthPath = process.env.HOME ? path.join(process.env.HOME, ".codex", "auth.json") : null;
|
||||||
const bindMounts = codexAuthPath && fs.existsSync(codexAuthPath) ? [`${codexAuthPath}:/root/.codex/auth.json:ro`] : [];
|
const bindMounts = codexAuthPath && fs.existsSync(codexAuthPath) ? [`${codexAuthPath}:/home/sandbox/.codex/auth.json:ro`] : [];
|
||||||
|
|
||||||
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
||||||
|
|
||||||
|
|
@ -28,17 +29,7 @@ try {
|
||||||
console.log("Starting container...");
|
console.log("Starting container...");
|
||||||
const container = await docker.createContainer({
|
const container = await docker.createContainer({
|
||||||
Image: IMAGE,
|
Image: IMAGE,
|
||||||
Cmd: [
|
Cmd: ["server", "--no-token", "--host", "0.0.0.0", "--port", `${PORT}`],
|
||||||
"sh",
|
|
||||||
"-c",
|
|
||||||
[
|
|
||||||
"apt-get update",
|
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6",
|
|
||||||
"rm -rf /var/lib/apt/lists/*",
|
|
||||||
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
|
|
||||||
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`,
|
|
||||||
].join(" && "),
|
|
||||||
],
|
|
||||||
Env: [
|
Env: [
|
||||||
process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "",
|
process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "",
|
||||||
process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "",
|
process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "",
|
||||||
|
|
@ -56,7 +47,7 @@ await container.start();
|
||||||
const baseUrl = `http://127.0.0.1:${PORT}`;
|
const baseUrl = `http://127.0.0.1:${PORT}`;
|
||||||
|
|
||||||
const client = await SandboxAgent.connect({ baseUrl });
|
const client = await SandboxAgent.connect({ baseUrl });
|
||||||
const session = await client.createSession({ agent, sessionInit: { cwd: "/root", mcpServers: [] } });
|
const session = await client.createSession({ agent, sessionInit: { cwd: "/home/sandbox", mcpServers: [] } });
|
||||||
const sessionId = session.id;
|
const sessionId = session.id;
|
||||||
|
|
||||||
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ const persist = new InMemorySessionPersistDriver();
|
||||||
console.log("Starting sandbox...");
|
console.log("Starting sandbox...");
|
||||||
const sandbox = await startDockerSandbox({
|
const sandbox = await startDockerSandbox({
|
||||||
port: 3000,
|
port: 3000,
|
||||||
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@ try {
|
||||||
console.log("Starting sandbox...");
|
console.log("Starting sandbox...");
|
||||||
const sandbox = await startDockerSandbox({
|
const sandbox = await startDockerSandbox({
|
||||||
port: 3000,
|
port: 3000,
|
||||||
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ const persist = new SQLiteSessionPersistDriver({ filename: "./sessions.db" });
|
||||||
console.log("Starting sandbox...");
|
console.log("Starting sandbox...");
|
||||||
const sandbox = await startDockerSandbox({
|
const sandbox = await startDockerSandbox({
|
||||||
port: 3000,
|
port: 3000,
|
||||||
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
FROM node:22-bookworm-slim
|
|
||||||
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends ca-certificates > /dev/null 2>&1 && \
|
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
|
||||||
npm install -g --silent @sandbox-agent/cli@latest && \
|
|
||||||
sandbox-agent install-agent claude
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
FROM node:22-bookworm-slim AS frontend
|
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
# Copy workspace root config
|
|
||||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
||||||
|
|
||||||
# Copy packages needed for the inspector build chain:
|
|
||||||
# inspector -> sandbox-agent SDK -> acp-http-client, cli-shared, persist-indexeddb
|
|
||||||
COPY sdks/typescript/ sdks/typescript/
|
|
||||||
COPY sdks/acp-http-client/ sdks/acp-http-client/
|
|
||||||
COPY sdks/cli-shared/ sdks/cli-shared/
|
|
||||||
COPY sdks/persist-indexeddb/ sdks/persist-indexeddb/
|
|
||||||
COPY sdks/react/ sdks/react/
|
|
||||||
COPY frontend/packages/inspector/ frontend/packages/inspector/
|
|
||||||
COPY docs/openapi.json docs/
|
|
||||||
|
|
||||||
# Create stub package.json for workspace packages referenced in pnpm-workspace.yaml
|
|
||||||
# but not needed for the inspector build (avoids install errors).
|
|
||||||
RUN set -e; for dir in \
|
|
||||||
sdks/cli sdks/gigacode \
|
|
||||||
sdks/persist-postgres sdks/persist-sqlite sdks/persist-rivet \
|
|
||||||
resources/agent-schemas resources/vercel-ai-sdk-schemas \
|
|
||||||
scripts/release scripts/sandbox-testing \
|
|
||||||
examples/shared examples/docker examples/e2b examples/vercel \
|
|
||||||
examples/daytona examples/cloudflare examples/file-system \
|
|
||||||
examples/mcp examples/mcp-custom-tool \
|
|
||||||
examples/skills examples/skills-custom-tool \
|
|
||||||
frontend/packages/website; do \
|
|
||||||
mkdir -p "$dir"; \
|
|
||||||
printf '{"name":"@stub/%s","private":true,"version":"0.0.0"}\n' "$(basename "$dir")" > "$dir/package.json"; \
|
|
||||||
done; \
|
|
||||||
for parent in sdks/cli/platforms sdks/gigacode/platforms; do \
|
|
||||||
for plat in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-x64; do \
|
|
||||||
mkdir -p "$parent/$plat"; \
|
|
||||||
printf '{"name":"@stub/%s-%s","private":true,"version":"0.0.0"}\n' "$(basename "$parent")" "$plat" > "$parent/$plat/package.json"; \
|
|
||||||
done; \
|
|
||||||
done
|
|
||||||
|
|
||||||
RUN pnpm install --no-frozen-lockfile
|
|
||||||
ENV SKIP_OPENAPI_GEN=1
|
|
||||||
RUN pnpm --filter sandbox-agent build && \
|
|
||||||
pnpm --filter @sandbox-agent/inspector build
|
|
||||||
|
|
||||||
FROM rust:1.88.0-bookworm AS builder
|
|
||||||
WORKDIR /build
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY server/ ./server/
|
|
||||||
COPY gigacode/ ./gigacode/
|
|
||||||
COPY resources/agent-schemas/artifacts/ ./resources/agent-schemas/artifacts/
|
|
||||||
COPY scripts/agent-configs/ ./scripts/agent-configs/
|
|
||||||
COPY --from=frontend /build/frontend/packages/inspector/dist/ ./frontend/packages/inspector/dist/
|
|
||||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
|
||||||
--mount=type=cache,target=/usr/local/cargo/git \
|
|
||||||
--mount=type=cache,target=/build/target \
|
|
||||||
cargo build -p sandbox-agent --release && \
|
|
||||||
cp target/release/sandbox-agent /sandbox-agent
|
|
||||||
|
|
||||||
FROM node:22-bookworm-slim
|
|
||||||
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends ca-certificates > /dev/null 2>&1 && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
COPY --from=builder /sandbox-agent /usr/local/bin/sandbox-agent
|
|
||||||
RUN sandbox-agent install-agent claude
|
|
||||||
|
|
@ -6,10 +6,10 @@ import { PassThrough } from "node:stream";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const EXAMPLE_IMAGE = "sandbox-agent-examples:latest";
|
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
|
||||||
const EXAMPLE_IMAGE_DEV = "sandbox-agent-examples-dev:latest";
|
|
||||||
const DOCKERFILE_DIR = path.resolve(__dirname, "..");
|
/** Pre-built Docker image with all agents installed. */
|
||||||
const REPO_ROOT = path.resolve(DOCKERFILE_DIR, "../..");
|
export const FULL_IMAGE = "rivetdev/sandbox-agent:0.3.1-full";
|
||||||
|
|
||||||
export interface DockerSandboxOptions {
|
export interface DockerSandboxOptions {
|
||||||
/** Container port used by sandbox-agent inside Docker. */
|
/** Container port used by sandbox-agent inside Docker. */
|
||||||
|
|
@ -18,7 +18,7 @@ export interface DockerSandboxOptions {
|
||||||
hostPort?: number;
|
hostPort?: number;
|
||||||
/** Additional shell commands to run before starting sandbox-agent. */
|
/** Additional shell commands to run before starting sandbox-agent. */
|
||||||
setupCommands?: string[];
|
setupCommands?: string[];
|
||||||
/** Docker image to use. Defaults to the pre-built sandbox-agent-examples image. */
|
/** Docker image to use. Defaults to the pre-built full image. */
|
||||||
image?: string;
|
image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,33 +131,31 @@ function stripAnsi(value: string): string {
|
||||||
return value.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g, "");
|
return value.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureExampleImage(_docker: Docker): Promise<string> {
|
async function ensureImage(docker: Docker, image: string): Promise<void> {
|
||||||
const dev = !!process.env.SANDBOX_AGENT_DEV;
|
if (process.env.SANDBOX_AGENT_DEV) {
|
||||||
const imageName = dev ? EXAMPLE_IMAGE_DEV : EXAMPLE_IMAGE;
|
console.log(" Building sandbox image from source (may take a while)...");
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
console.log(" Building sandbox image from source (may take a while, only runs once)...");
|
|
||||||
try {
|
try {
|
||||||
execFileSync("docker", ["build", "-t", imageName, "-f", path.join(DOCKERFILE_DIR, "Dockerfile.dev"), REPO_ROOT], {
|
execFileSync("docker", ["build", "-t", image, "-f", path.join(REPO_ROOT, "docker/runtime/Dockerfile.full"), REPO_ROOT], {
|
||||||
stdio: ["ignore", "ignore", "pipe"],
|
|
||||||
});
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr) : "";
|
|
||||||
throw new Error(`Failed to build sandbox image: ${stderr}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(" Building sandbox image (may take a while, only runs once)...");
|
|
||||||
try {
|
|
||||||
execFileSync("docker", ["build", "-t", imageName, DOCKERFILE_DIR], {
|
|
||||||
stdio: ["ignore", "ignore", "pipe"],
|
stdio: ["ignore", "ignore", "pipe"],
|
||||||
});
|
});
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr) : "";
|
const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr) : "";
|
||||||
throw new Error(`Failed to build sandbox image: ${stderr}`);
|
throw new Error(`Failed to build sandbox image: ${stderr}`);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return imageName;
|
try {
|
||||||
|
await docker.getImage(image).inspect();
|
||||||
|
} catch {
|
||||||
|
console.log(` Pulling ${image}...`);
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
docker.modem.followProgress(stream, (err: Error | null) => (err ? reject(err) : resolve()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -166,8 +164,7 @@ async function ensureExampleImage(_docker: Docker): Promise<string> {
|
||||||
*/
|
*/
|
||||||
export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<DockerSandbox> {
|
export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<DockerSandbox> {
|
||||||
const { port, hostPort } = opts;
|
const { port, hostPort } = opts;
|
||||||
const useCustomImage = !!opts.image;
|
const image = opts.image ?? FULL_IMAGE;
|
||||||
let image = opts.image ?? EXAMPLE_IMAGE;
|
|
||||||
// TODO: Replace setupCommands shell bootstrapping with native sandbox-agent exec API once available.
|
// TODO: Replace setupCommands shell bootstrapping with native sandbox-agent exec API once available.
|
||||||
const setupCommands = [...(opts.setupCommands ?? [])];
|
const setupCommands = [...(opts.setupCommands ?? [])];
|
||||||
const credentialEnv = collectCredentialEnv();
|
const credentialEnv = collectCredentialEnv();
|
||||||
|
|
@ -197,27 +194,13 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
||||||
|
|
||||||
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
||||||
|
|
||||||
if (useCustomImage) {
|
await ensureImage(docker, image);
|
||||||
try {
|
|
||||||
await docker.getImage(image).inspect();
|
|
||||||
} catch {
|
|
||||||
console.log(` Pulling ${image}...`);
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
docker.modem.followProgress(stream, (err: Error | null) => (err ? reject(err) : resolve()));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
image = await ensureExampleImage(docker);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bootCommands = [...setupCommands, `sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`];
|
const bootCommands = [...setupCommands, `sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`];
|
||||||
|
|
||||||
const container = await docker.createContainer({
|
const container = await docker.createContainer({
|
||||||
Image: image,
|
Image: image,
|
||||||
WorkingDir: "/root",
|
WorkingDir: "/home/sandbox",
|
||||||
Cmd: ["sh", "-c", bootCommands.join(" && ")],
|
Cmd: ["sh", "-c", bootCommands.join(" && ")],
|
||||||
Env: [...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`), ...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`)],
|
Env: [...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`), ...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`)],
|
||||||
ExposedPorts: { [`${port}/tcp`]: {} },
|
ExposedPorts: { [`${port}/tcp`]: {} },
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,26 @@ Use `pnpm` workspaces and Turborepo.
|
||||||
- Foundry is the canonical name for this product tree. Do not introduce or preserve legacy pre-Foundry naming in code, docs, commands, or runtime paths.
|
- Foundry is the canonical name for this product tree. Do not introduce or preserve legacy pre-Foundry naming in code, docs, commands, or runtime paths.
|
||||||
- Install deps: `pnpm install`
|
- Install deps: `pnpm install`
|
||||||
- Full active-workspace validation: `pnpm -w typecheck`, `pnpm -w build`, `pnpm -w test`
|
- Full active-workspace validation: `pnpm -w typecheck`, `pnpm -w build`, `pnpm -w test`
|
||||||
- Start the full dev stack: `just foundry-dev`
|
- Start the full dev stack (real backend + frontend): `just foundry-dev` — frontend on **port 4173**, backend on **port 7741** (Docker via `compose.dev.yaml`)
|
||||||
|
- Start the mock frontend stack (no backend): `just foundry-mock` — mock frontend on **port 4174** (Docker via `compose.mock.yaml`)
|
||||||
- Start the local production-build preview stack: `just foundry-preview`
|
- Start the local production-build preview stack: `just foundry-preview`
|
||||||
- Start only the backend locally: `just foundry-backend-start`
|
- Start only the backend locally: `just foundry-backend-start`
|
||||||
- Start only the frontend locally: `pnpm --filter @sandbox-agent/foundry-frontend dev`
|
- Start only the frontend locally: `pnpm --filter @sandbox-agent/foundry-frontend dev`
|
||||||
- Start the frontend against the mock workbench client: `FOUNDRY_FRONTEND_CLIENT_MODE=mock pnpm --filter @sandbox-agent/foundry-frontend dev`
|
- Start the mock frontend locally (no Docker): `just foundry-dev-mock` — mock frontend on **port 4174**
|
||||||
|
- Dev and mock stacks can run simultaneously on different ports (4173 and 4174).
|
||||||
- Stop the compose dev stack: `just foundry-dev-down`
|
- Stop the compose dev stack: `just foundry-dev-down`
|
||||||
- Tail compose logs: `just foundry-dev-logs`
|
- Tail compose dev logs: `just foundry-dev-logs`
|
||||||
|
- Stop the mock stack: `just foundry-mock-down`
|
||||||
|
- Tail mock logs: `just foundry-mock-logs`
|
||||||
- Stop the preview stack: `just foundry-preview-down`
|
- Stop the preview stack: `just foundry-preview-down`
|
||||||
- Tail preview logs: `just foundry-preview-logs`
|
- Tail preview logs: `just foundry-preview-logs`
|
||||||
|
|
||||||
|
## Dev Environment Setup
|
||||||
|
|
||||||
|
- `compose.dev.yaml` loads `foundry/.env` (optional) for credentials needed by the backend (GitHub OAuth, Stripe, Daytona, API keys, etc.).
|
||||||
|
- The canonical source for these credentials is `~/misc/the-foundry.env`. If `foundry/.env` does not exist, copy it: `cp ~/misc/the-foundry.env foundry/.env`
|
||||||
|
- `foundry/.env` is gitignored and must never be committed.
|
||||||
|
|
||||||
## Railway Logs
|
## Railway Logs
|
||||||
|
|
||||||
- Production Foundry Railway logs can be read from a linked workspace with `railway logs --deployment --lines 200` or `railway logs <deployment-id> --deployment --lines 200`.
|
- Production Foundry Railway logs can be read from a linked workspace with `railway logs --deployment --lines 200` or `railway logs <deployment-id> --deployment --lines 200`.
|
||||||
|
|
@ -117,6 +127,17 @@ The client subscribes to `app` always, `workspace` when entering a workspace, `t
|
||||||
- Backend mutations that affect sidebar data (task title, status, branch, PR state) must push the updated summary to the parent workspace actor, which broadcasts to workspace subscribers.
|
- Backend mutations that affect sidebar data (task title, status, branch, PR state) must push the updated summary to the parent workspace actor, which broadcasts to workspace subscribers.
|
||||||
- Comment architecture-related code: add doc comments explaining the materialized state pattern, why deltas flow the way they do, and the relationship between parent/child actor broadcasts. New contributors should understand the data flow from comments alone.
|
- Comment architecture-related code: add doc comments explaining the materialized state pattern, why deltas flow the way they do, and the relationship between parent/child actor broadcasts. New contributors should understand the data flow from comments alone.
|
||||||
|
|
||||||
|
## UI System
|
||||||
|
|
||||||
|
- Foundry's base UI system is `BaseUI` with `Styletron`, plus Foundry-specific theme/tokens on top. Treat that as the default UI foundation.
|
||||||
|
- The full `BaseUI` reference for available components and guidance on animations, customization, composition, and forms is at `https://base-ui.com/llms.txt`.
|
||||||
|
- Prefer existing `BaseUI` components and composition patterns whenever possible instead of building custom controls from scratch.
|
||||||
|
- Reuse the established Foundry theme/token layer for colors, typography, spacing, and surfaces instead of introducing ad hoc visual values.
|
||||||
|
- If the same UI pattern is shared with the Inspector or other consumers, prefer extracting or reusing it through `@sandbox-agent/react` rather than duplicating it in Foundry.
|
||||||
|
- If a requested UI cannot be implemented cleanly with an existing `BaseUI` component, stop and ask the user whether they are sure they want to diverge from the system.
|
||||||
|
- In that case, recommend the closest existing `BaseUI` components or compositions that could satisfy the need before proposing custom UI work.
|
||||||
|
- Only introduce custom UI primitives when `BaseUI` and existing Foundry patterns are not sufficient, or when the user explicitly confirms they want the divergence.
|
||||||
|
|
||||||
## Runtime Policy
|
## Runtime Policy
|
||||||
|
|
||||||
- Runtime is Bun-native.
|
- Runtime is Bun-native.
|
||||||
|
|
@ -174,7 +195,9 @@ For all Rivet/RivetKit implementation:
|
||||||
- Do not build blocking flows that wait on external systems to become ready or complete. Prefer push-based progression driven by actor messages, events, webhooks, or queue/workflow state changes.
|
- Do not build blocking flows that wait on external systems to become ready or complete. Prefer push-based progression driven by actor messages, events, webhooks, or queue/workflow state changes.
|
||||||
- Use workflows/background commands for any repo sync, sandbox provisioning, agent install, branch restack/rebase, or other multi-step external work. Do not keep user-facing actions/requests open while that work runs.
|
- Use workflows/background commands for any repo sync, sandbox provisioning, agent install, branch restack/rebase, or other multi-step external work. Do not keep user-facing actions/requests open while that work runs.
|
||||||
- `send` policy: always `await` the `send(...)` call itself so enqueue failures surface immediately, but default to `wait: false`.
|
- `send` policy: always `await` the `send(...)` call itself so enqueue failures surface immediately, but default to `wait: false`.
|
||||||
- Only use `send(..., { wait: true })` for short, bounded mutations that should finish quickly and do not depend on external readiness, polling actors, provider setup, repo/network I/O, or long-running queue drains.
|
- Only use `send(..., { wait: true })` for short, bounded local mutations (e.g. a DB write that returns a result the caller needs). Never use `wait: true` for operations that depend on external readiness, polling actors, provider setup, repo/network I/O, sandbox sessions, GitHub API calls, or long-running queue drains.
|
||||||
|
- Never self-send with `wait: true` from inside a workflow handler — the workflow processes one message at a time, so the handler would deadlock waiting for the new message to be dequeued.
|
||||||
|
- When an action is void-returning and triggers external work, use `wait: false` and let the UI react to state changes pushed by the workflow.
|
||||||
- Request/action contract: wait only until the minimum resource needed for the client's next step exists. Example: task creation may wait for task actor creation/identity, but not for sandbox provisioning or session bootstrap.
|
- Request/action contract: wait only until the minimum resource needed for the client's next step exists. Example: task creation may wait for task actor creation/identity, but not for sandbox provisioning or session bootstrap.
|
||||||
- Read paths must not force refresh/sync work inline. Serve the latest cached projection, mark staleness explicitly, and trigger background refresh separately when needed.
|
- Read paths must not force refresh/sync work inline. Serve the latest cached projection, mark staleness explicitly, and trigger background refresh separately when needed.
|
||||||
- If a workflow needs to resume after some external work completes, model that as workflow state plus follow-up messages/events instead of holding the original request open.
|
- If a workflow needs to resume after some external work completes, model that as workflow state plus follow-up messages/events instead of holding the original request open.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ services:
|
||||||
dockerfile: foundry/docker/backend.dev.Dockerfile
|
dockerfile: foundry/docker/backend.dev.Dockerfile
|
||||||
image: foundry-backend-dev
|
image: foundry-backend-dev
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
|
env_file:
|
||||||
|
- path: .env
|
||||||
|
required: false
|
||||||
environment:
|
environment:
|
||||||
HF_BACKEND_HOST: "0.0.0.0"
|
HF_BACKEND_HOST: "0.0.0.0"
|
||||||
HF_BACKEND_PORT: "7741"
|
HF_BACKEND_PORT: "7741"
|
||||||
|
|
|
||||||
32
foundry/compose.mock.yaml
Normal file
32
foundry/compose.mock.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
name: foundry-mock
|
||||||
|
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: foundry/docker/frontend.dev.Dockerfile
|
||||||
|
working_dir: /app
|
||||||
|
environment:
|
||||||
|
HOME: "/tmp"
|
||||||
|
FOUNDRY_FRONTEND_CLIENT_MODE: "mock"
|
||||||
|
ports:
|
||||||
|
- "4174:4174"
|
||||||
|
command: ["bash", "-lc", "pnpm install --force --frozen-lockfile --filter @sandbox-agent/foundry-frontend... && cd foundry/packages/frontend && exec pnpm vite --host 0.0.0.0 --port 4174"]
|
||||||
|
volumes:
|
||||||
|
- "..:/app"
|
||||||
|
- "./.foundry:/app/foundry/.foundry"
|
||||||
|
- "../../../task/rivet-checkout:/task/rivet-checkout:ro"
|
||||||
|
- "mock_node_modules:/app/node_modules"
|
||||||
|
- "mock_client_node_modules:/app/foundry/packages/client/node_modules"
|
||||||
|
- "mock_frontend_errors_node_modules:/app/foundry/packages/frontend-errors/node_modules"
|
||||||
|
- "mock_frontend_node_modules:/app/foundry/packages/frontend/node_modules"
|
||||||
|
- "mock_shared_node_modules:/app/foundry/packages/shared/node_modules"
|
||||||
|
- "mock_pnpm_store:/tmp/.local/share/pnpm/store"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mock_node_modules: {}
|
||||||
|
mock_client_node_modules: {}
|
||||||
|
mock_frontend_errors_node_modules: {}
|
||||||
|
mock_frontend_node_modules: {}
|
||||||
|
mock_shared_node_modules: {}
|
||||||
|
mock_pnpm_store: {}
|
||||||
|
|
@ -39,4 +39,4 @@ ENV SANDBOX_AGENT_BIN="/root/.local/bin/sandbox-agent"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
CMD ["bash", "-lc", "git config --global --add safe.directory /app >/dev/null 2>&1 || true; pnpm install --force --frozen-lockfile --filter @sandbox-agent/foundry-backend... && exec bun foundry/packages/backend/src/index.ts start --host 0.0.0.0 --port 7741"]
|
CMD ["bash", "-lc", "git config --global --add safe.directory /app >/dev/null 2>&1 || true; pnpm install --frozen-lockfile --filter @sandbox-agent/foundry-backend... && exec bun --hot foundry/packages/backend/src/index.ts start --host 0.0.0.0 --port 7741"]
|
||||||
|
|
|
||||||
|
|
@ -146,14 +146,9 @@ export const task = actor({
|
||||||
|
|
||||||
async provision(c, cmd: InitializeCommand): Promise<{ ok: true }> {
|
async provision(c, cmd: InitializeCommand): Promise<{ ok: true }> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
const result = await self.send(taskWorkflowQueueName("task.command.provision"), cmd ?? {}, {
|
await self.send(taskWorkflowQueueName("task.command.provision"), cmd ?? {}, {
|
||||||
wait: true,
|
wait: false,
|
||||||
timeout: 30 * 60_000,
|
|
||||||
});
|
});
|
||||||
const response = expectQueueResponse<{ ok: boolean; error?: string }>(result);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.error ?? "task provisioning failed");
|
|
||||||
}
|
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -182,47 +177,35 @@ export const task = actor({
|
||||||
async push(c, cmd?: TaskActionCommand): Promise<void> {
|
async push(c, cmd?: TaskActionCommand): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(taskWorkflowQueueName("task.command.push"), cmd ?? {}, {
|
await self.send(taskWorkflowQueueName("task.command.push"), cmd ?? {}, {
|
||||||
wait: true,
|
wait: false,
|
||||||
timeout: 180_000,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async sync(c, cmd?: TaskActionCommand): Promise<void> {
|
async sync(c, cmd?: TaskActionCommand): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(taskWorkflowQueueName("task.command.sync"), cmd ?? {}, {
|
await self.send(taskWorkflowQueueName("task.command.sync"), cmd ?? {}, {
|
||||||
wait: true,
|
wait: false,
|
||||||
timeout: 30_000,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async merge(c, cmd?: TaskActionCommand): Promise<void> {
|
async merge(c, cmd?: TaskActionCommand): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(taskWorkflowQueueName("task.command.merge"), cmd ?? {}, {
|
await self.send(taskWorkflowQueueName("task.command.merge"), cmd ?? {}, {
|
||||||
wait: true,
|
wait: false,
|
||||||
timeout: 30_000,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async archive(c, cmd?: TaskActionCommand): Promise<void> {
|
async archive(c, cmd?: TaskActionCommand): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
void self
|
await self.send(taskWorkflowQueueName("task.command.archive"), cmd ?? {}, {
|
||||||
.send(taskWorkflowQueueName("task.command.archive"), cmd ?? {}, {
|
wait: false,
|
||||||
wait: true,
|
});
|
||||||
timeout: 60_000,
|
|
||||||
})
|
|
||||||
.catch((error: unknown) => {
|
|
||||||
c.log.warn({
|
|
||||||
msg: "archive command failed",
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async kill(c, cmd?: TaskActionCommand): Promise<void> {
|
async kill(c, cmd?: TaskActionCommand): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(taskWorkflowQueueName("task.command.kill"), cmd ?? {}, {
|
await self.send(taskWorkflowQueueName("task.command.kill"), cmd ?? {}, {
|
||||||
wait: true,
|
wait: false,
|
||||||
timeout: 60_000,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -265,8 +248,7 @@ export const task = actor({
|
||||||
async renameWorkbenchBranch(c, input: TaskWorkbenchRenameInput): Promise<void> {
|
async renameWorkbenchBranch(c, input: TaskWorkbenchRenameInput): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(taskWorkflowQueueName("task.command.workbench.rename_branch"), { value: input.value } satisfies TaskWorkbenchValueCommand, {
|
await self.send(taskWorkflowQueueName("task.command.workbench.rename_branch"), { value: input.value } satisfies TaskWorkbenchValueCommand, {
|
||||||
wait: true,
|
wait: false,
|
||||||
timeout: 5 * 60_000,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -345,8 +327,7 @@ export const task = actor({
|
||||||
attachments: input.attachments,
|
attachments: input.attachments,
|
||||||
} satisfies TaskWorkbenchSendMessageCommand,
|
} satisfies TaskWorkbenchSendMessageCommand,
|
||||||
{
|
{
|
||||||
wait: true,
|
wait: false,
|
||||||
timeout: 10 * 60_000,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -354,8 +335,7 @@ export const task = actor({
|
||||||
async stopWorkbenchSession(c, input: TaskTabCommand): Promise<void> {
|
async stopWorkbenchSession(c, input: TaskTabCommand): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(taskWorkflowQueueName("task.command.workbench.stop_session"), { sessionId: input.tabId } satisfies TaskWorkbenchSessionCommand, {
|
await self.send(taskWorkflowQueueName("task.command.workbench.stop_session"), { sessionId: input.tabId } satisfies TaskWorkbenchSessionCommand, {
|
||||||
wait: true,
|
wait: false,
|
||||||
timeout: 5 * 60_000,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -370,8 +350,7 @@ export const task = actor({
|
||||||
async closeWorkbenchSession(c, input: TaskTabCommand): Promise<void> {
|
async closeWorkbenchSession(c, input: TaskTabCommand): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(taskWorkflowQueueName("task.command.workbench.close_session"), { sessionId: input.tabId } satisfies TaskWorkbenchSessionCommand, {
|
await self.send(taskWorkflowQueueName("task.command.workbench.close_session"), { sessionId: input.tabId } satisfies TaskWorkbenchSessionCommand, {
|
||||||
wait: true,
|
wait: false,
|
||||||
timeout: 5 * 60_000,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -381,8 +360,7 @@ export const task = actor({
|
||||||
taskWorkflowQueueName("task.command.workbench.publish_pr"),
|
taskWorkflowQueueName("task.command.workbench.publish_pr"),
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
wait: true,
|
wait: false,
|
||||||
timeout: 10 * 60_000,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -390,8 +368,7 @@ export const task = actor({
|
||||||
async revertWorkbenchFile(c, input: { path: string }): Promise<void> {
|
async revertWorkbenchFile(c, input: { path: string }): Promise<void> {
|
||||||
const self = selfTask(c);
|
const self = selfTask(c);
|
||||||
await self.send(taskWorkflowQueueName("task.command.workbench.revert_file"), input, {
|
await self.send(taskWorkflowQueueName("task.command.workbench.revert_file"), input, {
|
||||||
wait: true,
|
wait: false,
|
||||||
timeout: 5 * 60_000,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { getOrCreateTaskStatusSync, getOrCreateProject, getOrCreateWorkspace, ge
|
||||||
import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js";
|
import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js";
|
||||||
import { task as taskTable, taskRuntime, taskWorkbenchSessions } from "./db/schema.js";
|
import { task as taskTable, taskRuntime, taskWorkbenchSessions } from "./db/schema.js";
|
||||||
import { getCurrentRecord } from "./workflow/common.js";
|
import { getCurrentRecord } from "./workflow/common.js";
|
||||||
|
import { taskWorkflowQueueName } from "./workflow/queue.js";
|
||||||
|
|
||||||
const STATUS_SYNC_INTERVAL_MS = 1_000;
|
const STATUS_SYNC_INTERVAL_MS = 1_000;
|
||||||
|
|
||||||
|
|
@ -835,7 +836,14 @@ export async function renameWorkbenchBranch(c: any, value: string): Promise<void
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createWorkbenchSession(c: any, model?: string): Promise<{ tabId: string }> {
|
export async function createWorkbenchSession(c: any, model?: string): Promise<{ tabId: string }> {
|
||||||
const record = await ensureWorkbenchSeeded(c);
|
let record = await ensureWorkbenchSeeded(c);
|
||||||
|
if (!record.activeSandboxId) {
|
||||||
|
// Fire-and-forget: enqueue provisioning without waiting to avoid self-deadlock
|
||||||
|
// (this handler already runs inside the task workflow loop, so wait:true would deadlock).
|
||||||
|
const providerId = record.providerId ?? c.state.providerId ?? getActorRuntimeContext().providers.defaultProviderId();
|
||||||
|
await selfTask(c).send(taskWorkflowQueueName("task.command.provision"), { providerId }, { wait: false });
|
||||||
|
throw new Error("sandbox is provisioning — retry shortly");
|
||||||
|
}
|
||||||
|
|
||||||
if (record.activeSessionId) {
|
if (record.activeSessionId) {
|
||||||
const existingSessions = await listSessionMetaRows(c);
|
const existingSessions = await listSessionMetaRows(c);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<!--
|
|
||||||
<script src="https://unpkg.com/react-scan/dist/auto.global.js" crossorigin="anonymous"></script>
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
import("react-grab");
|
import("react-grab");
|
||||||
import("@react-grab/mcp/client");
|
import("@react-grab/mcp/client");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
-->
|
|
||||||
<script>if(window.__TAURI_INTERNALS__)document.documentElement.dataset.tauri="1"</script>
|
<script>if(window.__TAURI_INTERNALS__)document.documentElement.dataset.tauri="1"</script>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { TabStrip } from "./mock-layout/tab-strip";
|
||||||
import { TerminalPane } from "./mock-layout/terminal-pane";
|
import { TerminalPane } from "./mock-layout/terminal-pane";
|
||||||
import { TranscriptHeader } from "./mock-layout/transcript-header";
|
import { TranscriptHeader } from "./mock-layout/transcript-header";
|
||||||
import { PROMPT_TEXTAREA_MAX_HEIGHT, PROMPT_TEXTAREA_MIN_HEIGHT, SPanel, ScrollBody, Shell } from "./mock-layout/ui";
|
import { PROMPT_TEXTAREA_MAX_HEIGHT, PROMPT_TEXTAREA_MIN_HEIGHT, SPanel, ScrollBody, Shell } from "./mock-layout/ui";
|
||||||
|
import { DevPanel, useDevPanel } from "./dev-panel";
|
||||||
import {
|
import {
|
||||||
buildDisplayMessages,
|
buildDisplayMessages,
|
||||||
diffPath,
|
diffPath,
|
||||||
|
|
@ -1759,6 +1760,7 @@ export function MockLayout({ workspaceId, selectedTaskId, selectedSessionId }: M
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{showDevPanel && <DevPanel workspaceId={workspaceId} snapshot={viewModel} />}
|
||||||
</Shell>
|
</Shell>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { Download, Monitor, ChevronDown } from "lucide-react";
|
|
||||||
|
|
||||||
const DOWNLOAD_BASE = "https://releases.rivet.dev/foundry/0.1.0";
|
|
||||||
|
|
||||||
type Platform = {
|
|
||||||
label: string;
|
|
||||||
arch: string;
|
|
||||||
filename: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PLATFORMS: Platform[] = [
|
|
||||||
{
|
|
||||||
label: "macOS (Apple Silicon)",
|
|
||||||
arch: "arm64",
|
|
||||||
filename: "Foundry_0.1.0_aarch64.dmg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "macOS (Intel)",
|
|
||||||
arch: "x64",
|
|
||||||
filename: "Foundry_0.1.0_x64.dmg",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function detectPlatform(): Platform | null {
|
|
||||||
if (typeof navigator === "undefined") return null;
|
|
||||||
|
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
|
||||||
if (!ua.includes("mac")) return null;
|
|
||||||
|
|
||||||
// Apple Silicon detection: check for arm in platform or userAgentData
|
|
||||||
const isArm = navigator.platform === "MacIntel" && (navigator as any).userAgentData?.architecture === "arm";
|
|
||||||
// Fallback: newer Safari/Chrome on Apple Silicon
|
|
||||||
const couldBeArm = navigator.platform === "MacIntel" && !ua.includes("intel");
|
|
||||||
|
|
||||||
if (isArm || couldBeArm) {
|
|
||||||
return PLATFORMS[0]; // Apple Silicon
|
|
||||||
}
|
|
||||||
return PLATFORMS[1]; // Intel
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DownloadFoundry() {
|
|
||||||
const [detected, setDetected] = useState<Platform | null>(null);
|
|
||||||
const [showDropdown, setShowDropdown] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDetected(detectPlatform());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const primary = detected ?? PLATFORMS[0];
|
|
||||||
const secondary = PLATFORMS.filter((p) => p !== primary);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="border-t border-white/10 py-48">
|
|
||||||
<div className="mx-auto max-w-7xl px-6">
|
|
||||||
<div className="mb-12 text-center">
|
|
||||||
<motion.h2
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
className="mb-4 text-2xl font-normal tracking-tight text-white md:text-4xl"
|
|
||||||
>
|
|
||||||
Download Foundry
|
|
||||||
</motion.h2>
|
|
||||||
<motion.p
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.1 }}
|
|
||||||
className="mx-auto max-w-xl text-base leading-relaxed text-zinc-500"
|
|
||||||
>
|
|
||||||
Run Foundry as a native desktop app. Manage workspaces, handoffs, and coding agents locally.
|
|
||||||
</motion.p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
|
||||||
className="mx-auto flex max-w-md flex-col items-center gap-4"
|
|
||||||
>
|
|
||||||
{/* Primary download button */}
|
|
||||||
<a
|
|
||||||
href={`${DOWNLOAD_BASE}/${primary.filename}`}
|
|
||||||
className="inline-flex w-full items-center justify-center gap-3 rounded-xl bg-white px-8 py-4 text-base font-medium text-black transition-colors hover:bg-zinc-200"
|
|
||||||
>
|
|
||||||
<Download className="h-5 w-5" />
|
|
||||||
Download for {primary.label}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* Other platforms */}
|
|
||||||
<div className="relative">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowDropdown(!showDropdown)}
|
|
||||||
className="inline-flex items-center gap-2 text-sm text-zinc-500 transition-colors hover:text-white"
|
|
||||||
>
|
|
||||||
<Monitor className="h-4 w-4" />
|
|
||||||
Other platforms
|
|
||||||
<ChevronDown className={`h-3 w-3 transition-transform ${showDropdown ? "rotate-180" : ""}`} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showDropdown && (
|
|
||||||
<div className="absolute left-1/2 top-full mt-2 -translate-x-1/2 rounded-lg border border-white/10 bg-[#0f0f11] p-2 shadow-xl">
|
|
||||||
{secondary.map((p) => (
|
|
||||||
<a
|
|
||||||
key={p.arch}
|
|
||||||
href={`${DOWNLOAD_BASE}/${p.filename}`}
|
|
||||||
className="block whitespace-nowrap rounded-md px-4 py-2 text-sm text-zinc-300 transition-colors hover:bg-white/10 hover:text-white"
|
|
||||||
>
|
|
||||||
{p.label}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Unsigned app note */}
|
|
||||||
<p className="mt-4 text-center text-xs leading-relaxed text-zinc-600">
|
|
||||||
macOS only. On first launch, right-click the app and select "Open" to bypass Gatekeeper.
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Hero } from "../components/Hero";
|
||||||
import { PainPoints } from "../components/PainPoints";
|
import { PainPoints } from "../components/PainPoints";
|
||||||
import { FeatureGrid } from "../components/FeatureGrid";
|
import { FeatureGrid } from "../components/FeatureGrid";
|
||||||
import { GetStarted } from "../components/GetStarted";
|
import { GetStarted } from "../components/GetStarted";
|
||||||
import { DownloadFoundry } from "../components/DownloadFoundry";
|
|
||||||
import { Inspector } from "../components/Inspector";
|
import { Inspector } from "../components/Inspector";
|
||||||
import { FAQ } from "../components/FAQ";
|
import { FAQ } from "../components/FAQ";
|
||||||
import { Footer } from "../components/Footer";
|
import { Footer } from "../components/Footer";
|
||||||
|
|
@ -18,7 +18,7 @@ import { Footer } from "../components/Footer";
|
||||||
<PainPoints client:visible />
|
<PainPoints client:visible />
|
||||||
<FeatureGrid client:visible />
|
<FeatureGrid client:visible />
|
||||||
<GetStarted client:visible />
|
<GetStarted client:visible />
|
||||||
<DownloadFoundry client:visible />
|
|
||||||
<Inspector client:visible />
|
<Inspector client:visible />
|
||||||
<FAQ client:visible />
|
<FAQ client:visible />
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
28
justfile
28
justfile
|
|
@ -135,6 +135,34 @@ foundry-preview:
|
||||||
mkdir -p foundry/.foundry/logs
|
mkdir -p foundry/.foundry/logs
|
||||||
HF_DOCKER_UID="$(id -u)" HF_DOCKER_GID="$(id -g)" docker compose --env-file .env -f foundry/compose.preview.yaml up --build --force-recreate -d
|
HF_DOCKER_UID="$(id -u)" HF_DOCKER_GID="$(id -g)" docker compose --env-file .env -f foundry/compose.preview.yaml up --build --force-recreate -d
|
||||||
|
|
||||||
|
[group('foundry')]
|
||||||
|
foundry-frontend-dev host='127.0.0.1' port='4173' backend='http://127.0.0.1:7741/api/rivet':
|
||||||
|
pnpm install
|
||||||
|
VITE_HF_BACKEND_ENDPOINT="{{backend}}" pnpm --filter @sandbox-agent/foundry-frontend dev -- --host {{host}} --port {{port}}
|
||||||
|
|
||||||
|
[group('foundry')]
|
||||||
|
foundry-dev-mock host='127.0.0.1' port='4174':
|
||||||
|
pnpm install
|
||||||
|
FOUNDRY_FRONTEND_CLIENT_MODE=mock pnpm --filter @sandbox-agent/foundry-frontend dev -- --host {{host}} --port {{port}}
|
||||||
|
|
||||||
|
[group('foundry')]
|
||||||
|
foundry-mock:
|
||||||
|
pnpm install
|
||||||
|
mkdir -p foundry/.foundry/logs
|
||||||
|
docker compose -f foundry/compose.mock.yaml up --build --force-recreate -d
|
||||||
|
|
||||||
|
[group('foundry')]
|
||||||
|
foundry-mock-down:
|
||||||
|
docker compose -f foundry/compose.mock.yaml down
|
||||||
|
|
||||||
|
[group('foundry')]
|
||||||
|
foundry-mock-logs:
|
||||||
|
docker compose -f foundry/compose.mock.yaml logs -f --tail=200
|
||||||
|
|
||||||
|
[group('foundry')]
|
||||||
|
foundry-dev-turbo:
|
||||||
|
pnpm exec turbo run dev --parallel --filter=@sandbox-agent/foundry-*
|
||||||
|
|
||||||
[group('foundry')]
|
[group('foundry')]
|
||||||
foundry-dev-down:
|
foundry-dev-down:
|
||||||
docker compose --env-file .env -f foundry/compose.dev.yaml down
|
docker compose --env-file .env -f foundry/compose.dev.yaml down
|
||||||
|
|
|
||||||
|
|
@ -16,34 +16,46 @@ export async function tagDocker(opts: ReleaseOpts) {
|
||||||
console.log(`==> Source commit: ${sourceCommit}`);
|
console.log(`==> Source commit: ${sourceCommit}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check both architecture images exist using manifest inspect
|
|
||||||
console.log(`==> Checking images exist: ${IMAGE}:${sourceCommit}-{amd64,arm64}`);
|
|
||||||
try {
|
try {
|
||||||
console.log(`==> Inspecting ${IMAGE}:${sourceCommit}-amd64`);
|
await ensureArchImagesExist(sourceCommit, "");
|
||||||
await $({ stdio: "inherit" })`docker manifest inspect ${IMAGE}:${sourceCommit}-amd64`;
|
|
||||||
console.log(`==> Inspecting ${IMAGE}:${sourceCommit}-arm64`);
|
|
||||||
await $({ stdio: "inherit" })`docker manifest inspect ${IMAGE}:${sourceCommit}-arm64`;
|
|
||||||
console.log(`==> Both images exist`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`⚠️ Docker images ${IMAGE}:${sourceCommit}-{amd64,arm64} not found - skipping Docker tagging`);
|
console.warn(`⚠️ Docker images ${IMAGE}:${sourceCommit}-{amd64,arm64} not found - skipping Docker tagging`);
|
||||||
console.warn(` To enable Docker tagging, build and push images first, then retry the release.`);
|
console.warn(` To enable Docker tagging, build and push images first, then retry the release.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and push manifest with version
|
|
||||||
await createManifest(sourceCommit, opts.version);
|
await createManifest(sourceCommit, opts.version);
|
||||||
|
|
||||||
// Create and push manifest with latest
|
|
||||||
if (opts.latest) {
|
if (opts.latest) {
|
||||||
await createManifest(sourceCommit, "latest");
|
await createManifest(sourceCommit, "latest");
|
||||||
await createManifest(sourceCommit, opts.minorVersionChannel);
|
await createManifest(sourceCommit, opts.minorVersionChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ensureArchImagesExist(sourceCommit, "-full");
|
||||||
|
await createManifest(sourceCommit, `${opts.version}-full`, "-full");
|
||||||
|
if (opts.latest) {
|
||||||
|
await createManifest(sourceCommit, `${opts.minorVersionChannel}-full`, "-full");
|
||||||
|
await createManifest(sourceCommit, "full", "-full");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`⚠️ Full Docker images ${IMAGE}:${sourceCommit}-full-{amd64,arm64} not found - skipping full Docker tagging`);
|
||||||
|
console.warn(` To enable full Docker tagging, build and push full images first, then retry the release.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createManifest(from: string, to: string) {
|
async function ensureArchImagesExist(sourceCommit: string, variantSuffix: "" | "-full") {
|
||||||
console.log(`==> Creating manifest: ${IMAGE}:${to} from ${IMAGE}:${from}-{amd64,arm64}`);
|
console.log(`==> Checking images exist: ${IMAGE}:${sourceCommit}${variantSuffix}-{amd64,arm64}`);
|
||||||
|
console.log(`==> Inspecting ${IMAGE}:${sourceCommit}${variantSuffix}-amd64`);
|
||||||
// Use buildx imagetools to create and push multi-arch manifest
|
await $({ stdio: "inherit" })`docker manifest inspect ${IMAGE}:${sourceCommit}${variantSuffix}-amd64`;
|
||||||
// This works with manifest lists as inputs (unlike docker manifest create)
|
console.log(`==> Inspecting ${IMAGE}:${sourceCommit}${variantSuffix}-arm64`);
|
||||||
await $({ stdio: "inherit" })`docker buildx imagetools create --tag ${IMAGE}:${to} ${IMAGE}:${from}-amd64 ${IMAGE}:${from}-arm64`;
|
await $({ stdio: "inherit" })`docker manifest inspect ${IMAGE}:${sourceCommit}${variantSuffix}-arm64`;
|
||||||
|
console.log(`==> Both images exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createManifest(from: string, to: string, variantSuffix: "" | "-full" = "") {
|
||||||
|
console.log(`==> Creating manifest: ${IMAGE}:${to} from ${IMAGE}:${from}${variantSuffix}-{amd64,arm64}`);
|
||||||
|
|
||||||
|
await $({
|
||||||
|
stdio: "inherit",
|
||||||
|
})`docker buildx imagetools create --tag ${IMAGE}:${to} ${IMAGE}:${from}${variantSuffix}-amd64 ${IMAGE}:${from}${variantSuffix}-arm64`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ pub enum Command {
|
||||||
Opencode(OpencodeArgs),
|
Opencode(OpencodeArgs),
|
||||||
/// Manage the sandbox-agent background daemon.
|
/// Manage the sandbox-agent background daemon.
|
||||||
Daemon(DaemonArgs),
|
Daemon(DaemonArgs),
|
||||||
/// Install or reinstall an agent without running the server.
|
/// Install or reinstall one agent, or `all` supported agents, without running the server.
|
||||||
InstallAgent(InstallAgentArgs),
|
InstallAgent(InstallAgentArgs),
|
||||||
/// Inspect locally discovered credentials.
|
/// Inspect locally discovered credentials.
|
||||||
Credentials(CredentialsArgs),
|
Credentials(CredentialsArgs),
|
||||||
|
|
@ -295,7 +295,10 @@ pub struct AcpCloseArgs {
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
pub struct InstallAgentArgs {
|
pub struct InstallAgentArgs {
|
||||||
agent: String,
|
#[arg(required_unless_present = "all", conflicts_with = "all")]
|
||||||
|
agent: Option<String>,
|
||||||
|
#[arg(long, conflicts_with = "agent")]
|
||||||
|
all: bool,
|
||||||
#[arg(long, short = 'r')]
|
#[arg(long, short = 'r')]
|
||||||
reinstall: bool,
|
reinstall: bool,
|
||||||
#[arg(long = "agent-version")]
|
#[arg(long = "agent-version")]
|
||||||
|
|
@ -946,24 +949,73 @@ fn load_json_payload(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn install_agent_local(args: &InstallAgentArgs) -> Result<(), CliError> {
|
fn install_agent_local(args: &InstallAgentArgs) -> Result<(), CliError> {
|
||||||
let agent_id = AgentId::parse(&args.agent)
|
if args.all && (args.agent_version.is_some() || args.agent_process_version.is_some()) {
|
||||||
.ok_or_else(|| CliError::Server(format!("unsupported agent: {}", args.agent)))?;
|
return Err(CliError::Server(
|
||||||
|
"--agent-version and --agent-process-version are only supported for single-agent installs"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let agents = resolve_install_agents(args)?;
|
||||||
|
|
||||||
let manager = AgentManager::new(default_install_dir())
|
let manager = AgentManager::new(default_install_dir())
|
||||||
.map_err(|err| CliError::Server(err.to_string()))?;
|
.map_err(|err| CliError::Server(err.to_string()))?;
|
||||||
|
|
||||||
let result = manager
|
if agents.len() == 1 {
|
||||||
.install(
|
let result = manager
|
||||||
agent_id,
|
.install(
|
||||||
InstallOptions {
|
agents[0],
|
||||||
reinstall: args.reinstall,
|
InstallOptions {
|
||||||
version: args.agent_version.clone(),
|
reinstall: args.reinstall,
|
||||||
agent_process_version: args.agent_process_version.clone(),
|
version: args.agent_version.clone(),
|
||||||
},
|
agent_process_version: args.agent_process_version.clone(),
|
||||||
)
|
},
|
||||||
.map_err(|err| CliError::Server(err.to_string()))?;
|
)
|
||||||
|
.map_err(|err| CliError::Server(err.to_string()))?;
|
||||||
|
let output = install_result_json(result);
|
||||||
|
return write_stdout_line(&serde_json::to_string_pretty(&output)?);
|
||||||
|
}
|
||||||
|
|
||||||
let output = json!({
|
let mut results = Vec::with_capacity(agents.len());
|
||||||
|
for agent_id in agents {
|
||||||
|
let result = manager
|
||||||
|
.install(
|
||||||
|
agent_id,
|
||||||
|
InstallOptions {
|
||||||
|
reinstall: args.reinstall,
|
||||||
|
version: None,
|
||||||
|
agent_process_version: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|err| CliError::Server(err.to_string()))?;
|
||||||
|
results.push(json!({
|
||||||
|
"agent": agent_id.as_str(),
|
||||||
|
"result": install_result_json(result),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
write_stdout_line(&serde_json::to_string_pretty(
|
||||||
|
&json!({ "agents": results }),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_install_agents(args: &InstallAgentArgs) -> Result<Vec<AgentId>, CliError> {
|
||||||
|
if args.all {
|
||||||
|
return Ok(AgentId::all().to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
let agent = args
|
||||||
|
.agent
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| CliError::Server("missing agent: provide <AGENT> or --all".to_string()))?;
|
||||||
|
|
||||||
|
AgentId::parse(agent)
|
||||||
|
.map(|agent_id| vec![agent_id])
|
||||||
|
.ok_or_else(|| CliError::Server(format!("unsupported agent: {agent}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_result_json(result: sandbox_agent_agent_management::agents::InstallResult) -> Value {
|
||||||
|
json!({
|
||||||
"alreadyInstalled": result.already_installed,
|
"alreadyInstalled": result.already_installed,
|
||||||
"artifacts": result.artifacts.into_iter().map(|artifact| json!({
|
"artifacts": result.artifacts.into_iter().map(|artifact| json!({
|
||||||
"kind": format!("{:?}", artifact.kind),
|
"kind": format!("{:?}", artifact.kind),
|
||||||
|
|
@ -971,9 +1023,7 @@ fn install_agent_local(args: &InstallAgentArgs) -> Result<(), CliError> {
|
||||||
"source": format!("{:?}", artifact.source),
|
"source": format!("{:?}", artifact.source),
|
||||||
"version": artifact.version,
|
"version": artifact.version,
|
||||||
})).collect::<Vec<_>>()
|
})).collect::<Vec<_>>()
|
||||||
});
|
})
|
||||||
|
|
||||||
write_stdout_line(&serde_json::to_string_pretty(&output)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -1416,6 +1466,60 @@ fn write_stderr_line(text: &str) -> Result<(), CliError> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_install_agents_expands_all() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_install_agents(&InstallAgentArgs {
|
||||||
|
agent: None,
|
||||||
|
all: true,
|
||||||
|
reinstall: false,
|
||||||
|
agent_version: None,
|
||||||
|
agent_process_version: None,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
AgentId::all().to_vec()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_install_agents_supports_single_agent() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_install_agents(&InstallAgentArgs {
|
||||||
|
agent: Some("codex".to_string()),
|
||||||
|
all: false,
|
||||||
|
reinstall: false,
|
||||||
|
agent_version: None,
|
||||||
|
agent_process_version: None,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
vec![AgentId::Codex]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_install_agents_rejects_unknown_agent() {
|
||||||
|
assert!(resolve_install_agents(&InstallAgentArgs {
|
||||||
|
agent: Some("nope".to_string()),
|
||||||
|
all: false,
|
||||||
|
reinstall: false,
|
||||||
|
agent_version: None,
|
||||||
|
agent_process_version: None,
|
||||||
|
})
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_install_agents_rejects_positional_all() {
|
||||||
|
assert!(resolve_install_agents(&InstallAgentArgs {
|
||||||
|
agent: Some("all".to_string()),
|
||||||
|
all: false,
|
||||||
|
reinstall: false,
|
||||||
|
agent_version: None,
|
||||||
|
agent_process_version: None,
|
||||||
|
})
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_last_event_id_header_sets_header_when_provided() {
|
fn apply_last_event_id_header_sets_header_when_provided() {
|
||||||
let client = HttpClient::builder().build().expect("build client");
|
let client = HttpClient::builder().build().expect("build client");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue