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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ agentbox claude
> Run setup wizard? -> Yes

# Also use a cloud:
agentbox hetzner claude # or vercel, daytona
agentbox hetzner claude # or vercel, daytona, e2b, createos

# Ctrl+a d to detach, claude keep going, to reconnect later:
agentbox attach 1
Expand Down Expand Up @@ -81,12 +81,12 @@ Uses `portless` to give box web apps the same URL from inside the box and on the

## Cloud Providers

| | local docker | remote docker | hetzner | daytona | vercel | e2b |
| ------------------- | ------------------------- | ---------------------- | ---------------------- | ------------------ | ------------------ | ------------------ |
| Support | ✅ | ✅ | ✅ | ⚠️ Partial | ✅ | ✅ |
| Base image | Dockerfile | Dockerfile (on the remote) | Setup script (Ubuntu) | Dockerfile | Setup script | Dockerfile (`Template.build`) |
| Live snapshots | ✅ | ✅ (`docker commit`) | ✅ | 🧪 Experimental | ✅ | ✅ |
| Private preview URLs| ✅ (portless or OrbStack) | ✅ (portless over SSH) | ✅ (portless) | ✅ (native) | ✅ (native) | ✅ (native) |
| | local docker | remote docker | hetzner | daytona | vercel | e2b | createos |
| ------------------- | ------------------------- | ---------------------- | ---------------------- | ------------------ | ------------------ | ------------------ | ----------------- |
| Support | ✅ | ✅ | ✅ | ⚠️ Partial | ✅ | ✅ | ⚠️ Partial |
| Base image | Dockerfile | Dockerfile (on the remote) | Setup script (Ubuntu) | Dockerfile | Setup script | Dockerfile (`Template.build`) | Rootfs + provision install |
| Live snapshots | ✅ | ✅ (`docker commit`) | ✅ | 🧪 Experimental | ✅ | ✅ | Pause/resume |
| Private preview URLs| ✅ (portless or OrbStack) | ✅ (portless over SSH) | ✅ (portless) | ✅ (native) | ✅ (native) | ✅ (native) | ✅ (native) |

**Cloud setup** (optional — skip for local Docker)

Expand All @@ -95,9 +95,10 @@ Uses `portless` to give box web apps the same URL from inside the box and on the
- `agentbox hetzner login` — interactive Hetzner Cloud token setup, saved to `~/.agentbox/secrets.env`
- `agentbox daytona login` — interactive Daytona API key setup, saved to `~/.agentbox/secrets.env`
- `agentbox e2b login` — interactive E2B API key setup, saved to `~/.agentbox/secrets.env`
- `agentbox createos login` — interactive CreateOS API key setup, saved to `~/.agentbox/secrets.env`
- `agentbox digitalocean login` — interactive DigitalOcean Personal Access Token setup, saved to `~/.agentbox/secrets.env`
- `agentbox remote-docker doctor <host>` — run boxes on a machine you already own, over SSH. No login and no token: it connects as you, using your own `~/.ssh/config`. Then `agentbox docker:<host> claude`.
- `agentbox prepare [--provider daytona|hetzner|vercel|e2b|digitalocean|docker:<host>]` — build the image and initial snapshot (e2b builds from a Dockerfile via `Template.build()`)
- `agentbox prepare [--provider daytona|hetzner|vercel|e2b|createos|digitalocean|docker:<host>]` — build the image and initial snapshot where supported; createos validates credentials/runtime assets and installs during provision
- `agentbox hetzner claude`, `agentbox hetzner codex`, `agentbox hetzner create`, etc.

## How to use
Expand Down Expand Up @@ -156,7 +157,7 @@ Full documentation lives at **[agent-box.sh/docs](https://agent-box.sh/docs)**:
- [Quickstart](https://agent-box.sh/docs) and [Core concepts](https://agent-box.sh/docs/core-concepts)
- [Teleport a project](https://agent-box.sh/docs/teleport-a-project), [Run an agent](https://agent-box.sh/docs/run-an-agent), [Access your box](https://agent-box.sh/docs/access-your-box)
- [Configuration](https://agent-box.sh/docs/configuration), [Services & tasks](https://agent-box.sh/docs/services-and-tasks), [Sync & git](https://agent-box.sh/docs/sync-and-git)
- Cloud providers: [Hetzner](https://agent-box.sh/docs/hetzner), [Daytona](https://agent-box.sh/docs/daytona), [Vercel](https://agent-box.sh/docs/vercel), [E2B](https://agent-box.sh/docs/e2b), [DigitalOcean](https://agent-box.sh/docs/digitalocean)
- Cloud providers: [Hetzner](https://agent-box.sh/docs/hetzner), [Daytona](https://agent-box.sh/docs/daytona), [Vercel](https://agent-box.sh/docs/vercel), [E2B](https://agent-box.sh/docs/e2b), [CreateOS](https://agent-box.sh/docs/createos), [DigitalOcean](https://agent-box.sh/docs/digitalocean)
- Full [CLI reference](https://agent-box.sh/docs/cli)

## Development
Expand Down Expand Up @@ -211,4 +212,3 @@ Bug reports, docs fixes, and provider work are welcome — see [CONTRIBUTING.md]
# License

MIT. See [LICENSE](./LICENSE).

1 change: 1 addition & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"@agentbox/relay": "workspace:*",
"@agentbox/sandbox-cloud": "workspace:*",
"@agentbox/sandbox-core": "workspace:*",
"@agentbox/sandbox-createos": "workspace:*",
"@agentbox/sandbox-daytona": "workspace:*",
"@agentbox/sandbox-digitalocean": "workspace:*",
"@agentbox/sandbox-remote-docker": "workspace:*",
Expand Down
26 changes: 26 additions & 0 deletions apps/cli/scripts/stage-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const runtime = join(cliRoot, 'runtime');
const dockerCtx = join(runtime, 'docker');
const hetznerCtx = join(runtime, 'hetzner');
const digitaloceanCtx = join(runtime, 'digitalocean');
const createosCtx = join(runtime, 'createos');
const daytonaCtx = join(runtime, 'daytona');
const vercelCtx = join(runtime, 'vercel');
const e2bCtx = join(runtime, 'e2b');
Expand Down Expand Up @@ -185,6 +186,31 @@ for (const [srcRel, destRel, exec] of digitaloceanFiles) {
copy(srcRel, join(digitaloceanCtx, destRel), exec);
}

// CreateOS provider — runtime assets uploaded into the fresh microVM during
// provision. CreateOS currently has no file-context template bake, so this
// mirrors the flat VPS install layout and runs per sandbox.
const createosFiles = [
['packages/sandbox-createos/scripts/install-box.sh', 'scripts/install-box.sh', true],
['packages/ctl/dist/bin.cjs', 'ctl.cjs', true],
['packages/sandbox-docker/scripts/agentbox-vnc-start', 'agentbox-vnc-start', true],
['packages/sandbox-docker/scripts/agentbox-dockerd-start', 'agentbox-dockerd-start', true],
['packages/sandbox-docker/scripts/agentbox-portless-trust', 'agentbox-portless-trust', true],
['packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup', 'agentbox-checkpoint-cleanup', true],
['packages/sandbox-docker/scripts/agentbox-open', 'agentbox-open', true],
['packages/sandbox-docker/scripts/gh-shim', 'gh-shim', true],
['packages/sandbox-docker/scripts/git-shim', 'git-shim', true],
['packages/sandbox-docker/scripts/ntn-shim', 'ntn-shim', true],
['packages/sandbox-docker/scripts/linear-shim', 'linear-shim', true],
['packages/sandbox-createos/scripts/custom-system-CLAUDE.md', 'custom-system-CLAUDE.md', false],
['packages/sandbox-docker/scripts/claude-managed-settings.json', 'claude-managed-settings.json', false],
['packages/sandbox-docker/scripts/agentbox-codex-hooks.json', 'agentbox-codex-hooks.json', false],
['packages/sandbox-docker/scripts/opencode-agentbox-plugin.js', 'opencode-agentbox-plugin.js', false],
['apps/cli/share/agentbox-setup/SKILL.md', 'agentbox-setup-skill.md', false],
];
for (const [srcRel, destRel, exec] of createosFiles) {
copy(srcRel, join(createosCtx, destRel), exec);
}

// Daytona provider — overlay files the daytona prepare step adds on top of
// Dockerfile.box via Image.addLocalFile(). Resolver lives at
// `packages/sandbox-daytona/src/dockerfile-context.ts` and looks for these
Expand Down
3 changes: 2 additions & 1 deletion apps/cli/src/commands/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { confirm, intro, log, outro, spinner } from '../lib/prompt.js';
import {
findProjectRoot,
loadEffectiveConfig,
PROVIDER_NAMES,
resolveDefaultCheckpoint,
type AttachOpenIn,
type UserConfig,
Expand Down Expand Up @@ -502,7 +503,7 @@ export const claudeCommand = new Command('claude')
.option('--cpus <n>', 'CPU count cap (fractional ok, e.g. 1.5); unset = unlimited')
.option('--pids-limit <n>', 'max process count (PIDs cgroup); unset = unlimited')
.option('--disk <size>', 'best-effort writable-layer size (e.g. 10g); no-op on overlay2/macOS')
.option('--provider <name>', "sandbox backend: 'docker' (default) or 'daytona' for a cloud box")
.option('--provider <name>', `sandbox backend: ${PROVIDER_NAMES.join(', ')}. Default: docker.`)
.option(
'--from-branch <ref>',
"base the box's per-box branch on this ref (branch / tag / SHA) instead of HEAD. Branch/tag names are fetched from origin first.",
Expand Down
3 changes: 2 additions & 1 deletion apps/cli/src/commands/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { confirm, intro, log, outro, spinner } from '../lib/prompt.js';
import {
findProjectRoot,
loadEffectiveConfig,
PROVIDER_NAMES,
resolveDefaultCheckpoint,
type AttachOpenIn,
type UserConfig,
Expand Down Expand Up @@ -435,7 +436,7 @@ export const codexCommand = new Command('codex')
.option('--cpus <n>', 'CPU count cap (fractional ok, e.g. 1.5); unset = unlimited')
.option('--pids-limit <n>', 'max process count (PIDs cgroup); unset = unlimited')
.option('--disk <size>', 'best-effort writable-layer size (e.g. 10g); no-op on overlay2/macOS')
.option('--provider <name>', "sandbox backend: 'docker' (default) or 'daytona' for a cloud box")
.option('--provider <name>', `sandbox backend: ${PROVIDER_NAMES.join(', ')}. Default: docker.`)
.option(
'--from-branch <ref>',
"base the box's per-box branch on this ref (branch / tag / SHA) instead of HEAD. Branch/tag names are fetched from origin first.",
Expand Down
6 changes: 5 additions & 1 deletion apps/cli/src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
bumpProjectGcCounter,
findProjectRoot,
loadEffectiveConfig,
PROVIDER_NAMES,
pruneOrphanProjectConfigs,
registerProject,
resolveBoxImage,
Expand Down Expand Up @@ -155,6 +156,9 @@ async function attachShell(record: BoxRecord): Promise<never> {
process.exit(code);
}

const providerHelp = `sandbox backend: ${PROVIDER_NAMES.join(', ')}. Default: docker. ` +
"`docker:<host>` runs the box on that machine's docker engine over SSH.";

export const createCommand = new Command('create')
.description(
'Create and start a new agent box (Docker container with /workspace seeded via in-container git worktree)',
Expand All @@ -163,7 +167,7 @@ export const createCommand = new Command('create')
.option('-n, --name <name>', 'friendly box name (default: <workspace-basename>-<id>)')
.option(
'--provider <name>',
"sandbox backend: docker (default), daytona, hetzner, digitalocean, vercel, e2b, remote-docker. `docker:<host>` runs the box on that machine's docker engine over SSH.",
providerHelp,
)
.option(
'--remote-host <dest>',
Expand Down
30 changes: 22 additions & 8 deletions apps/cli/src/commands/inbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ interface InboundOptions {
yes?: boolean;
}

const INBOUND_PROVIDERS = new Set(['hetzner', 'digitalocean']);

export const inboundCommand = new Command('inbound')
.description(
"Set a VPS box's inbound-access policy (hetzner / digitalocean per-box firewall). " +
Expand All @@ -30,6 +32,26 @@ export const inboundCommand = new Command('inbound')
try {
const box = await resolveBoxOrExit(idOrName);
const provider = await providerForBox(box);
const providerName = box.provider ?? 'docker';

if (!INBOUND_PROVIDERS.has(providerName)) {
if (opts.show || spec.length === 0) {
process.stdout.write(
`inbound: unsupported for provider '${providerName}' - only hetzner / digitalocean boxes have a per-box firewall.\n`,
);
return;
}
log.error(
`inbound access control isn't supported for provider '${providerName}' - ` +
'only hetzner / digitalocean boxes have a per-box firewall.',
);
process.exit(2);
}

if (!provider.setInbound) {
log.error(`inbound access control isn't available for provider '${providerName}'.`);
process.exit(2);
}

if (opts.show || spec.length === 0) {
const policy = box.cloud?.inbound ?? { mode: 'locked' as const, sources: [] };
Expand All @@ -43,14 +65,6 @@ export const inboundCommand = new Command('inbound')
return;
}

if (!provider.setInbound) {
log.error(
`inbound access control isn't supported for provider '${box.provider ?? 'docker'}' — ` +
'only hetzner / digitalocean boxes have a per-box firewall.',
);
process.exit(2);
}

const raw = spec.join(' ');
const policy = parseInboundSpec(raw); // validates early (throws on a bad spec)

Expand Down
3 changes: 2 additions & 1 deletion apps/cli/src/commands/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { confirm, intro, log, outro, spinner, text } from '../lib/prompt.js';
import {
findProjectRoot,
loadEffectiveConfig,
PROVIDER_NAMES,
resolveDefaultCheckpoint,
type AttachOpenIn,
type UserConfig,
Expand Down Expand Up @@ -437,7 +438,7 @@ export const opencodeCommand = new Command('opencode')
.option('--disk <size>', 'best-effort writable-layer size (e.g. 10g); no-op on overlay2/macOS')
.option(
'--provider <name>',
"sandbox backend: 'docker' (default) or 'daytona' for a cloud box",
`sandbox backend: ${PROVIDER_NAMES.join(', ')}. Default: docker.`,
)
.option(
'--from-branch <ref>',
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/commands/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ export const prepareCommand = new Command('prepare')
)
.option(
'-p, --provider <name>',
'provider to prepare (docker | daytona | hetzner | vercel | e2b | digitalocean). Omit for status-only.',
'provider to prepare (docker | daytona | hetzner | vercel | e2b | digitalocean | createos). Omit for status-only.',
)
.option('-n, --name <name>', 'snapshot name (Daytona only; default: agentbox-base-<timestamp>)')
.option('-f, --force', 'rebuild even if the image / snapshot already exists')
Expand All @@ -526,7 +526,7 @@ export const prepareCommand = new Command('prepare')
)
.option(
'--size <spec>',
'bake-time VM size. daytona: cpu-memory-disk GB (e.g. 4-8-20). e2b: cpu-memory GB (e.g. 4-8). Overrides box.size / box.size<Provider>. Ignored by docker/hetzner/vercel.',
'bake-time VM size. daytona: cpu-memory-disk GB (e.g. 4-8-20). e2b: cpu-memory GB (e.g. 4-8). CreateOS accepts shape slugs on create. Overrides box.size / box.size<Provider>.',
)
.action(async (opts: PrepareOptions) => {
// Status-only path: no provider, or explicit --status.
Expand Down
5 changes: 3 additions & 2 deletions apps/cli/src/commands/prune.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const pruneCommand = new Command('prune')
.option('-y, --yes', 'skip the confirmation prompt')
.option(
'--provider <name>',
'restrict prune to a specific provider (docker | daytona | hetzner | vercel | e2b | digitalocean). For cloud providers, lists sandboxes that are not in this CLI\'s state.json and offers to delete them.',
'restrict prune to a specific provider (docker | daytona | hetzner | vercel | e2b | digitalocean | createos). For cloud providers, lists sandboxes that are not in this CLI\'s state.json and offers to delete them.',
)
.action(async (opts: PruneOptions) => {
try {
Expand All @@ -89,7 +89,7 @@ export const pruneCommand = new Command('prune')
return;
}
if (opts.provider !== undefined && opts.provider !== 'docker') {
log.error(`unknown provider '${opts.provider}'; expected docker, daytona, hetzner, vercel, e2b, or digitalocean`);
log.error(`unknown provider '${opts.provider}'; expected docker, daytona, hetzner, vercel, e2b, digitalocean, or createos`);
process.exit(2);
}
const dryRun = opts.dryRun ?? false;
Expand Down Expand Up @@ -136,6 +136,7 @@ const CLOUD_PRUNE_PROVIDERS = [
'vercel',
'e2b',
'digitalocean',
'createos',
'remote-docker',
] as const;
type CloudPruneProvider = (typeof CLOUD_PRUNE_PROVIDERS)[number];
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { hetznerCommand } from '@agentbox/sandbox-hetzner/cli';
import { vercelCommand } from '@agentbox/sandbox-vercel/cli';
import { e2bCommand } from '@agentbox/sandbox-e2b/cli';
import { digitaloceanCommand } from '@agentbox/sandbox-digitalocean/cli';
import { createosCommand } from '@agentbox/sandbox-createos/cli';
import { remoteDockerCommand } from '@agentbox/sandbox-remote-docker/cli';
import { destroyCommand } from './commands/destroy.js';
import { downloadCommand } from './commands/download.js';
Expand Down Expand Up @@ -186,6 +187,7 @@ program.addCommand(hetznerCommand);
program.addCommand(vercelCommand);
program.addCommand(e2bCommand);
program.addCommand(digitaloceanCommand);
program.addCommand(createosCommand);
program.addCommand(remoteDockerCommand);
program.addCommand(dockerCommand);
program.addCommand(updateCommand);
Expand Down
7 changes: 6 additions & 1 deletion apps/cli/src/lib/cloud-sizing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export interface CloudSizingFlags {
* - **all providers**: `size` — the generic VM-size string, `--size` flag first,
* else `box.size<Provider>` / `box.size`. Each backend interprets it natively
* (hetzner: server type; daytona: `cpu-mem-disk` GB; vercel: vCPU count;
* e2b: baked at prepare time; docker: ignored).
* e2b: baked at prepare time; createos: shape slug or `cpu-memory[-disk]`;
* docker: ignored).
* - **hetzner**: `location` — datacenter, `--location` flag first, else
* `box.hetznerLocation`.
* - **digitalocean**: `location` — region, `--location` flag first, else
Expand All @@ -42,6 +43,7 @@ export interface CloudSizingFlags {
* - **e2b**: `timeoutMs` — the session timeout the box is created with (and
* records as `cloud.sessionTimeoutMs`, which seeds the host keepalive loop so
* it can push the deadline forward while the agent is working).
* - **createos**: `timeoutMs` — mapped to CreateOS' sandbox auto-pause window.
* - **daytona**: `timeoutMs` (auto-stop inactivity window, same keepalive rail
* as e2b), `sandboxClass` (`linux-vm` | `container`) and `location` (region).
* `location` carries only an EXPLICIT `box.daytonaRegion` — the class-derived
Expand Down Expand Up @@ -91,6 +93,9 @@ export function cloudSizingProviderOptions(
if (providerName === 'e2b') {
out.timeoutMs = cfg.box.e2bTimeoutMs;
}
if (providerName === 'createos') {
out.timeoutMs = cfg.box.createosTimeoutMs;
}
if (providerName === 'remote-docker') {
// Which machine runs the container. Unlike every other provider's options
// this one is mandatory — there is no sensible default engine — so resolve
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/provider/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const IMPORTERS: Record<ProviderKind, () => Promise<{ providerModule: ProviderMo
vercel: () => import('@agentbox/sandbox-vercel'),
e2b: () => import('@agentbox/sandbox-e2b'),
digitalocean: () => import('@agentbox/sandbox-digitalocean'),
createos: () => import('@agentbox/sandbox-createos'),
'remote-docker': () => import('@agentbox/sandbox-remote-docker'),
};

Expand Down
3 changes: 2 additions & 1 deletion apps/cli/test/argv-prefix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
SUGARED_COMMANDS,
rewriteProviderPrefix,
} from '../src/provider/argv-prefix.js';
import { PROVIDER_NAMES } from '@agentbox/config';

const NODE = '/usr/local/bin/node';
const BIN = '/path/to/agentbox';
Expand All @@ -19,7 +20,7 @@ describe('rewriteProviderPrefix', () => {
});

it('rewrites every (provider × sugared-command) pair', () => {
for (const provider of ['docker', 'daytona', 'hetzner'] as const) {
for (const provider of PROVIDER_NAMES) {
for (const sub of SUGARED_COMMANDS) {
expect(rewriteProviderPrefix(argv(provider, sub))).toEqual(
argv(sub, '--provider', provider),
Expand Down
Loading