feat: add release pipeline for crates.io and npm publishing

- Add --check, --publish-crates, --publish-npm-sdk, --publish-npm-cli flags to release script
- Create CI workflow with pre-release checks (cargo fmt, clippy, test, tsc)
- Update release workflow to run checks, build binaries, and publish packages
- Add @sandbox-agent/cli npm package with platform-specific binaries (esbuild pattern)
- Configure TypeScript SDK for npm publishing (exports, files, types)
- Add crates.io metadata to Cargo.toml (repository, description)
- Rename @sandbox-agent/web to @sandbox-agent/inspector
This commit is contained in:
Nathan Flurry 2026-01-25 14:11:39 -08:00
parent 6e1b13c242
commit 016024c04b
26 changed files with 360 additions and 48 deletions

25
.github/workflows/ci.yaml vendored Normal file
View file

@ -0,0 +1,25 @@
name: ci
on:
push:
branches: [main]
pull_request:
workflow_call:
jobs:
checks:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install
- name: Run checks
run: ./scripts/release/main.ts --version 0.0.0 --check

View file

@ -18,8 +18,12 @@ env:
CARGO_INCREMENTAL: 0
jobs:
checks:
uses: ./.github/workflows/ci.yaml
setup:
name: "Setup"
needs: [checks]
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.vars.outputs.version }}
@ -54,7 +58,7 @@ jobs:
./scripts/release/main.ts --version "${{ steps.vars.outputs.version }}" --print-latest --output "$GITHUB_OUTPUT"
binaries:
name: "Build & Upload Binaries"
name: "Build Binaries"
needs: [setup]
strategy:
matrix:
@ -89,42 +93,58 @@ jobs:
docker/release/build.sh ${{ matrix.target }}
ls -la dist/
- name: Install AWS CLI
run: |
sudo apt-get update
sudo apt-get install -y unzip curl
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: dist/sandbox-agent-${{ matrix.target }}${{ matrix.binary_ext }}
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install --update
publish:
name: "Publish Packages"
needs: [setup, binaries]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Upload binaries
- uses: dtolnay/rust-toolchain@stable
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
cache: pnpm
- name: Install tsx
run: npm install -g tsx
- name: Download binaries
uses: actions/download-artifact@v4
with:
path: dist/
pattern: binary-*
merge-multiple: true
- name: List downloaded binaries
run: ls -la dist/
- name: Publish all
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_RELEASES_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_RELEASES_SECRET_ACCESS_KEY }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
VERSION="${{ needs.setup.outputs.version }}"
BINARY_NAME="sandbox-agent-${{ matrix.target }}${{ matrix.binary_ext }}"
aws s3 cp \
"dist/${BINARY_NAME}" \
"s3://rivet-releases/sandbox-agent/${VERSION}/${BINARY_NAME}" \
--region auto \
--endpoint-url https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com \
--checksum-algorithm CRC32
if [ "${{ needs.setup.outputs.latest }}" = "true" ]; then
aws s3 cp \
"dist/${BINARY_NAME}" \
"s3://rivet-releases/sandbox-agent/latest/${BINARY_NAME}" \
--region auto \
--endpoint-url https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com \
--checksum-algorithm CRC32
fi
./scripts/release/main.ts --version "$VERSION" \
--publish-crates \
--publish-npm-sdk \
--publish-npm-cli
artifacts:
name: "TypeScript + Install Script"
needs: [setup]
name: "Upload Artifacts"
needs: [setup, binaries]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@ -147,7 +167,17 @@ jobs:
unzip awscliv2.zip
sudo ./aws/install --update
- name: Upload TypeScript artifacts and install script
- name: Download binaries
uses: actions/download-artifact@v4
with:
path: dist/
pattern: binary-*
merge-multiple: true
- name: List downloaded binaries
run: ls -la dist/
- name: Upload artifacts
env:
R2_RELEASES_ACCESS_KEY_ID: ${{ secrets.R2_RELEASES_ACCESS_KEY_ID }}
R2_RELEASES_SECRET_ACCESS_KEY: ${{ secrets.R2_RELEASES_SECRET_ACCESS_KEY }}
@ -159,4 +189,7 @@ jobs:
LATEST_FLAG="--no-latest"
fi
./scripts/release/main.ts --version "$VERSION" $LATEST_FLAG --upload-typescript --upload-install
./scripts/release/main.ts --version "$VERSION" $LATEST_FLAG \
--upload-typescript \
--upload-install \
--upload-binaries

View file

@ -41,7 +41,7 @@ Universal schema guidance:
### Default port references (update when CLI default changes)
- `frontend/packages/web/src/App.tsx`
- `frontend/packages/inspector/src/App.tsx`
- `README.md`
- `docs/cli.mdx`
- `docs/frontend.mdx`

View file

@ -25,6 +25,6 @@ eval "$(sandbox-agent credentials extract-env --export)"
Run the web console (includes all dependencies):
```bash
pnpm dev -F @sandbox-agent/web
pnpm dev -F @sandbox-agent/inspector
```

View file

@ -7,3 +7,5 @@ version = "0.1.0"
edition = "2021"
authors = ["Sandbox Agent Contributors"]
license = "Apache-2.0"
repository = "https://github.com/rivet-dev/sandbox-agent"
description = "Universal agent API for AI coding assistants"

View file

@ -3,13 +3,13 @@ title: "Frontend Demo"
description: "Run the Vite + React UI for testing the daemon."
---
The demo frontend lives at `frontend/packages/web`.
The demo frontend lives at `frontend/packages/inspector`.
## Run locally
```bash
pnpm install
pnpm --filter @sandbox-agent/web dev
pnpm --filter @sandbox-agent/inspector dev
```
The UI expects:

View file

@ -4,11 +4,11 @@ RUN npm install -g pnpm
# Copy package files for all workspaces
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY frontend/packages/web/package.json ./frontend/packages/web/
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
COPY sdks/typescript/package.json ./sdks/typescript/
# Install dependencies
RUN pnpm install --filter @sandbox-agent/web...
RUN pnpm install --filter @sandbox-agent/inspector...
# Copy SDK source (with pre-generated types)
COPY sdks/typescript ./sdks/typescript
@ -16,14 +16,14 @@ COPY sdks/typescript ./sdks/typescript
# Build SDK (just tsc, skip generate since types are pre-generated)
RUN cd sdks/typescript && pnpm exec tsc -p tsconfig.json
# Copy web source
COPY frontend/packages/web ./frontend/packages/web
# Copy inspector source
COPY frontend/packages/inspector ./frontend/packages/inspector
# Build web
RUN cd frontend/packages/web && pnpm exec vite build
# Build inspector
RUN cd frontend/packages/inspector && pnpm exec vite build
FROM caddy:alpine
COPY --from=build /app/frontend/packages/web/dist /srv
COPY --from=build /app/frontend/packages/inspector/dist /srv
RUN cat > /etc/caddy/Caddyfile <<'EOF'
:80 {
root * /srv

View file

@ -1,5 +1,5 @@
{
"name": "@sandbox-agent/web",
"name": "@sandbox-agent/inspector",
"private": true,
"version": "0.0.0",
"license": "Apache-2.0",

View file

@ -1,4 +1,6 @@
packages:
- "frontend/packages/*"
- "sdks/*"
- "sdks/cli"
- "sdks/cli/platforms/*"
- "resources/agent-schemas"

View file

@ -16,6 +16,22 @@ const BINARY_FILES = [
"sandbox-agent-aarch64-apple-darwin",
];
const CRATE_ORDER = [
"error",
"agent-credentials",
"agent-schema",
"universal-agent-schema",
"agent-management",
"sandbox-agent",
];
const PLATFORM_MAP: Record<string, { pkg: string; os: string; cpu: string; ext: string }> = {
"x86_64-unknown-linux-musl": { pkg: "linux-x64", os: "linux", cpu: "x64", ext: "" },
"x86_64-pc-windows-gnu": { pkg: "win32-x64", os: "win32", cpu: "x64", ext: ".exe" },
"x86_64-apple-darwin": { pkg: "darwin-x64", os: "darwin", cpu: "x64", ext: "" },
"aarch64-apple-darwin": { pkg: "darwin-arm64", os: "darwin", cpu: "arm64", ext: "" },
};
function parseArgs(argv: string[]) {
const args = new Map<string, string>();
const flags = new Set<string>();
@ -279,6 +295,81 @@ function uploadBinaries(rootDir: string, version: string, latest: boolean) {
}
}
// Pre-release checks
function runChecks(rootDir: string) {
console.log("==> Running Rust checks");
run("cargo", ["fmt", "--all", "--", "--check"], { cwd: rootDir });
run("cargo", ["clippy", "--all-targets", "--", "-D", "warnings"], { cwd: rootDir });
run("cargo", ["test", "--all-targets"], { cwd: rootDir });
console.log("==> Running TypeScript checks");
run("pnpm", ["install"], { cwd: rootDir });
run("pnpm", ["run", "build"], { cwd: rootDir });
}
// Crates.io publishing
function publishCrates(rootDir: string, version: string) {
// Update workspace version
const cargoPath = path.join(rootDir, "Cargo.toml");
let cargoContent = fs.readFileSync(cargoPath, "utf8");
cargoContent = cargoContent.replace(/^version = ".*"/m, `version = "${version}"`);
fs.writeFileSync(cargoPath, cargoContent);
for (const crate of CRATE_ORDER) {
console.log(`==> Publishing sandbox-agent-${crate}`);
const crateDir = path.join(rootDir, "engine", "packages", crate);
run("cargo", ["publish", "--allow-dirty"], { cwd: crateDir });
// Wait for crates.io index propagation
console.log("Waiting 30s for index...");
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 30000);
}
}
// npm SDK publishing
function publishNpmSdk(rootDir: string, version: string) {
const sdkDir = path.join(rootDir, "sdks", "typescript");
console.log("==> Publishing TypeScript SDK to npm");
run("npm", ["version", version, "--no-git-tag-version"], { cwd: sdkDir });
run("pnpm", ["install"], { cwd: sdkDir });
run("pnpm", ["run", "build"], { cwd: sdkDir });
run("npm", ["publish", "--access", "public"], { cwd: sdkDir });
}
// npm CLI publishing
function publishNpmCli(rootDir: string, version: string) {
const cliDir = path.join(rootDir, "sdks", "cli");
const distDir = path.join(rootDir, "dist");
// Publish platform packages first
for (const [target, info] of Object.entries(PLATFORM_MAP)) {
const platformDir = path.join(cliDir, "platforms", info.pkg);
const binDir = path.join(platformDir, "bin");
fs.mkdirSync(binDir, { recursive: true });
// Copy binary
const srcBinary = path.join(distDir, `sandbox-agent-${target}${info.ext}`);
const dstBinary = path.join(binDir, `sandbox-agent${info.ext}`);
fs.copyFileSync(srcBinary, dstBinary);
if (info.ext !== ".exe") fs.chmodSync(dstBinary, 0o755);
// Update version and publish
console.log(`==> Publishing @sandbox-agent/cli-${info.pkg}`);
run("npm", ["version", version, "--no-git-tag-version"], { cwd: platformDir });
run("npm", ["publish", "--access", "public"], { cwd: platformDir });
}
// Publish main package (update optionalDeps versions)
console.log("==> Publishing @sandbox-agent/cli");
const pkgPath = path.join(cliDir, "package.json");
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
pkg.version = version;
for (const dep of Object.keys(pkg.optionalDependencies || {})) {
pkg.optionalDependencies[dep] = version;
}
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
run("npm", ["publish", "--access", "public"], { cwd: cliDir });
}
function main() {
const { args, flags } = parseArgs(process.argv.slice(2));
const versionArg = args.get("--version");
@ -308,6 +399,22 @@ function main() {
}
}
if (flags.has("--check")) {
runChecks(process.cwd());
}
if (flags.has("--publish-crates")) {
publishCrates(process.cwd(), version);
}
if (flags.has("--publish-npm-sdk")) {
publishNpmSdk(process.cwd(), version);
}
if (flags.has("--publish-npm-cli")) {
publishNpmCli(process.cwd(), version);
}
if (flags.has("--upload-typescript")) {
uploadTypescriptArtifacts(process.cwd(), version, latest);
}

26
sdks/cli/bin/sandbox-agent Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env node
const { execFileSync } = require("child_process");
const path = require("path");
const PLATFORMS = {
"darwin-arm64": "@sandbox-agent/cli-darwin-arm64",
"darwin-x64": "@sandbox-agent/cli-darwin-x64",
"linux-x64": "@sandbox-agent/cli-linux-x64",
"win32-x64": "@sandbox-agent/cli-win32-x64",
};
const key = `${process.platform}-${process.arch}`;
const pkg = PLATFORMS[key];
if (!pkg) {
console.error(`Unsupported platform: ${key}`);
process.exit(1);
}
try {
const pkgPath = require.resolve(`${pkg}/package.json`);
const bin = process.platform === "win32" ? "sandbox-agent.exe" : "sandbox-agent";
execFileSync(path.join(path.dirname(pkgPath), "bin", bin), process.argv.slice(2), { stdio: "inherit" });
} catch (e) {
if (e.status !== undefined) process.exit(e.status);
throw e;
}

22
sdks/cli/package.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "@sandbox-agent/cli",
"version": "0.1.0",
"description": "CLI for sandbox-agent - run AI coding agents in sandboxes",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/rivet-dev/sandbox-agent"
},
"bin": {
"sandbox-agent": "bin/sandbox-agent"
},
"optionalDependencies": {
"@sandbox-agent/cli-darwin-arm64": "0.1.0",
"@sandbox-agent/cli-darwin-x64": "0.1.0",
"@sandbox-agent/cli-linux-x64": "0.1.0",
"@sandbox-agent/cli-win32-x64": "0.1.0"
},
"files": [
"bin"
]
}

View file

@ -0,0 +1,19 @@
{
"name": "@sandbox-agent/cli-darwin-arm64",
"version": "0.1.0",
"description": "sandbox-agent CLI binary for macOS ARM64",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/rivet-dev/sandbox-agent"
},
"os": [
"darwin"
],
"cpu": [
"arm64"
],
"files": [
"bin"
]
}

View file

@ -0,0 +1,19 @@
{
"name": "@sandbox-agent/cli-darwin-x64",
"version": "0.1.0",
"description": "sandbox-agent CLI binary for macOS x64",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/rivet-dev/sandbox-agent"
},
"os": [
"darwin"
],
"cpu": [
"x64"
],
"files": [
"bin"
]
}

View file

@ -0,0 +1,19 @@
{
"name": "@sandbox-agent/cli-linux-x64",
"version": "0.1.0",
"description": "sandbox-agent CLI binary for Linux x64",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/rivet-dev/sandbox-agent"
},
"os": [
"linux"
],
"cpu": [
"x64"
],
"files": [
"bin"
]
}

View file

@ -0,0 +1,19 @@
{
"name": "@sandbox-agent/cli-win32-x64",
"version": "0.1.0",
"description": "sandbox-agent CLI binary for Windows x64",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/rivet-dev/sandbox-agent"
},
"os": [
"win32"
],
"cpu": [
"x64"
],
"files": [
"bin"
]
}

View file

@ -1,9 +1,24 @@
{
"name": "sandbox-agent",
"version": "0.1.0",
"private": true,
"description": "TypeScript SDK for sandbox-agent",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/rivet-dev/sandbox-agent"
},
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out src/generated/openapi.json",
"generate:types": "openapi-typescript src/generated/openapi.json -o src/generated/openapi.ts",
@ -14,5 +29,8 @@
"@types/node": "^22.0.0",
"openapi-typescript": "^6.7.0",
"typescript": "^5.7.0"
},
"optionalDependencies": {
"@sandbox-agent/cli": "0.1.0"
}
}

View file

@ -436,7 +436,7 @@ this machine is already authenticated with codex & claude & opencode (for codex)
## testing frontend
in frontend/packages/web/ build a vite + react app that:
in frontend/packages/inspector/ build a vite + react app that:
- connect screen: prompts the user to provide an endpoint & optional token
- shows instructions on how to run the sandbox-agent (including cors)

View file

@ -68,10 +68,11 @@
- [ ] Run the full suite against every agent (Claude/Codex/OpenCode/Amp) without mocks
- [x] Add real install/version/spawn tests for Claude/Codex/OpenCode (Amp conditional)
- [x] Expand agent lifecycle tests (reinstall, session id extraction, resume, plan mode)
- [ ] Add OpenCode server-mode tests (session create, prompt, SSE)
- [x] Add OpenCode server-mode tests (session create, prompt, SSE)
- [ ] Add tests for question/permission flows using deterministic prompts
- [x] Add HTTP/SSE snapshot tests for real agents (env-configured)
## Frontend (frontend/packages/web)
## Frontend (frontend/packages/inspector)
- [x] Build Vite + React app with connect screen (endpoint + optional token)
- [x] Add instructions to run sandbox-agent (including CORS)
- [x] Implement full agent UI covering all features