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
1 change: 1 addition & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"@agentbox/sandbox-docker": "workspace:*",
"@agentbox/sandbox-e2b": "workspace:*",
"@agentbox/sandbox-hetzner": "workspace:*",
"@agentbox/sandbox-islo": "workspace:*",
"@agentbox/sandbox-vercel": "workspace:*",
"@types/node": "^22.10.1",
"tsup": "^8.3.5",
Expand Down
7 changes: 5 additions & 2 deletions apps/cli/src/commands/checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { providerForBox } from '../provider/registry.js';
import { handleLifecycleError } from './_errors.js';

/** Cloud backends that store snapshots under ~/.agentbox/cloud-checkpoints/<backend>/. */
const CLOUD_BACKENDS = ['daytona', 'hetzner', 'vercel', 'e2b'] as const;
const CLOUD_BACKENDS = ['daytona', 'hetzner', 'vercel', 'e2b', 'islo'] as const;
type CloudBackend = (typeof CLOUD_BACKENDS)[number];

/** Lazily resolve a cloud provider's checkpoint capability (dynamic import keeps SDKs out of the hot path). */
Expand All @@ -49,6 +49,8 @@ async function cloudProviderFor(backend: CloudBackend): Promise<import('@agentbo
return (await import('@agentbox/sandbox-vercel')).vercelProvider;
case 'e2b':
return (await import('@agentbox/sandbox-e2b')).e2bProvider;
case 'islo':
return (await import('@agentbox/sandbox-islo')).isloProvider;
}
}

Expand Down Expand Up @@ -307,7 +309,7 @@ const setDefaultSub = new Command('set-default')
.option('--clear', 'unset the project default instead of setting one')
.option(
'--provider <name>',
'set the default for only this provider (docker|daytona|hetzner|vercel); without it, sets the cross-provider fallback',
'set the default for only this provider (docker|daytona|hetzner|vercel|e2b|islo); without it, sets the cross-provider fallback',
)
.action(async (ref: string | undefined, opts: { clear?: boolean; provider?: string }) => {
try {
Expand Down Expand Up @@ -421,6 +423,7 @@ const rmSub = new Command('rm')
['box.defaultCheckpointHetzner', projectBox?.defaultCheckpointHetzner, cfg.effective.box.defaultCheckpointHetzner],
['box.defaultCheckpointVercel', projectBox?.defaultCheckpointVercel, cfg.effective.box.defaultCheckpointVercel],
['box.defaultCheckpointE2b', projectBox?.defaultCheckpointE2b, cfg.effective.box.defaultCheckpointE2b],
['box.defaultCheckpointIslo', projectBox?.defaultCheckpointIslo, cfg.effective.box.defaultCheckpointIslo],
] as const;
for (const [key, projectValue, effectiveValue] of defKeys) {
if (projectValue === ref) {
Expand Down
22 changes: 15 additions & 7 deletions apps/cli/src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ export const createCommand = new Command('create')
)
.option('-w, --workspace <path>', 'host workspace to mount', process.cwd())
.option('-n, --name <name>', 'friendly box name (default: <workspace-basename>-<id>)')
.option('--provider <name>', "sandbox backend: 'docker' (default) or 'daytona' (cloud)")
.option(
'--provider <name>',
'sandbox backend: docker (default), daytona, hetzner, vercel, e2b, or islo',
)
.option(
'--host-snapshot',
'APFS-clone the host workspace into a per-box scratch dir before seeding /workspace (stabilizes the tar-pipe source)',
Expand Down Expand Up @@ -186,11 +189,11 @@ export const createCommand = new Command('create')
)
.option(
'--size <spec>',
'VM size for cloud providers. Hetzner: server type (e.g. cx33). Daytona: cpu-mem-disk GB (e.g. 4-8-20). Overrides box.size / box.size<Provider>.',
'VM size for cloud providers. Hetzner: server type (e.g. cx33). Daytona/Islo: cpu-mem-disk GB (e.g. 4-8-20). Overrides box.size / box.size<Provider>.',
)
.option(
'--bundle-depth <n>',
'cap commits shipped in the cloud-seed git bundle (daytona, hetzner). 0 = full history. Unset = adaptive (200 commits, re-bundle at 100 if >20 MB). Ignored for docker.',
'cap commits shipped in the cloud-seed git bundle (cloud providers). 0 = full history. Unset = adaptive (200 commits, re-bundle at 100 if >20 MB). Ignored for docker.',
(v) => {
const n = Number.parseInt(v, 10);
if (!Number.isInteger(n) || n < 0)
Expand Down Expand Up @@ -233,23 +236,23 @@ export const createCommand = new Command('create')
opts,
resolveDefaultCheckpoint(
cfg.effective,
providerName as 'docker' | 'daytona' | 'hetzner' | 'vercel',
providerName,
),
);
// VM size: `--size` flag wins; otherwise the cascaded box.size /
// box.size<Provider>. Resolved here (not in buildCliOverrides) so a CLI
// override beats a project-level per-provider key.
const sizeDefault = resolveBoxSize(
cfg.effective,
providerName as 'docker' | 'daytona' | 'hetzner' | 'vercel',
providerName,
);
const effectiveSize = opts.size && opts.size.length > 0 ? opts.size : sizeDefault;
// Box image: same precedence pattern as --size. `--image` wins; otherwise
// the cascaded box.image / box.image<Provider> (written by `agentbox
// prepare --provider X`).
const imageDefault = resolveBoxImage(
cfg.effective,
providerName as 'docker' | 'daytona' | 'hetzner' | 'vercel',
providerName,
);
const effectiveImage = opts.image && opts.image.length > 0 ? opts.image : imageDefault;

Expand Down Expand Up @@ -381,7 +384,7 @@ export const createCommand = new Command('create')
cfg.effective.box.withPlaywright || cfg.effective.browser.default !== 'agent-browser';
// --provider flag wins over box.provider config. The registry hands back
// a DockerProvider for 'docker' and (once Phase 5 wires it) a cloud
// provider for 'daytona'; everything below is provider-neutral.
// provider for cloud backends; everything below is provider-neutral.
const provider = await providerForCreate({ flag: opts.provider, config: cfg.effective });
let fromBranch: string | undefined;
let useBranch: string | undefined;
Expand Down Expand Up @@ -443,6 +446,11 @@ export const createCommand = new Command('create')
networkPolicy: cfg.effective.box.vercelNetworkPolicy,
}
: {}),
...(provider.name === 'islo'
? {
networkPolicy: cfg.effective.box.isloGatewayProfile,
}
: {}),
},
});
s.stop(`box ${result.record.container} ready`);
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ const PROVIDER_HINTS: Record<ProviderName, string> = {
daytona: 'approve a browser sign-in link',
vercel: 'installs the Vercel sandbox CLI, then a browser sign-in',
e2b: 'paste an API key from the E2B dashboard',
islo: 'paste an Islo API key; run `islo ssh --setup` for interactive attach',
};

const PROVIDER_LABEL: Record<ProviderName, string> = {
Expand All @@ -345,6 +346,7 @@ const PROVIDER_LABEL: Record<ProviderName, string> = {
daytona: 'Daytona (cloud sandbox)',
vercel: 'Vercel (cloud microVM)',
e2b: 'E2B (cloud microVM)',
islo: 'Islo (agent computer)',
};

function ensureTty(): boolean {
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const HELP_GROUPS: HelpGroup[] = [
'hetzner',
'vercel',
'e2b',
'islo',
],
},
];
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 @@ -32,6 +32,7 @@ import { dockerCommand } from './commands/docker.js';
import { hetznerCommand } from '@agentbox/sandbox-hetzner/cli';
import { vercelCommand } from '@agentbox/sandbox-vercel/cli';
import { e2bCommand } from '@agentbox/sandbox-e2b/cli';
import { isloCommand } from '@agentbox/sandbox-islo/cli';
import { destroyCommand } from './commands/destroy.js';
import { downloadCommand } from './commands/download.js';
import { driveCommand } from './commands/drive.js';
Expand Down Expand Up @@ -114,6 +115,7 @@ program.addCommand(daytonaCommand);
program.addCommand(hetznerCommand);
program.addCommand(vercelCommand);
program.addCommand(e2bCommand);
program.addCommand(isloCommand);
program.addCommand(dockerCommand);
program.addCommand(updateCommand);
program.addCommand(installCommand);
Expand Down
51 changes: 48 additions & 3 deletions apps/cli/src/lib/doctor-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ export interface CheckResult {
}

export interface CheckGroup {
/** Group title: 'system' | 'docker' | 'daytona' | 'hetzner' | 'vercel' | 'e2b'. */
/** Group title: 'system' | 'docker' | 'daytona' | 'hetzner' | 'vercel' | 'e2b' | 'islo'. */
title: string;
results: CheckResult[];
}

export type ProviderName = 'docker' | 'daytona' | 'hetzner' | 'vercel' | 'e2b';
export type ProviderName = 'docker' | 'daytona' | 'hetzner' | 'vercel' | 'e2b' | 'islo';

const ALL_PROVIDERS: ProviderName[] = ['docker', 'daytona', 'hetzner', 'vercel', 'e2b'];
const ALL_PROVIDERS: ProviderName[] = ['docker', 'daytona', 'hetzner', 'vercel', 'e2b', 'islo'];
const NODE_MIN_MAJOR = 20;
const NODE_MIN_MINOR = 10;

Expand Down Expand Up @@ -381,6 +381,48 @@ async function e2bChecks(): Promise<CheckResult[]> {
}
}

async function isloChecks(): Promise<CheckResult[]> {
try {
const mod = await import('@agentbox/sandbox-islo');
const cred = mod.readIsloCredStatus();
const credRes: CheckResult =
cred.auth === 'none'
? {
label: 'credentials',
status: 'warn',
detail: 'API key not configured',
hint: '`agentbox islo login` or set ISLO_API_KEY',
}
: {
label: 'credentials',
status: 'ok',
detail: `${cred.auth} (${cred.source})`,
};
return [
credRes,
{
label: 'base url',
status: 'ok',
detail: cred.baseUrl,
},
{
label: 'ssh attach',
status: 'info',
detail: 'requires Islo SSH setup',
hint: '`islo ssh --setup`',
},
];
} catch (err) {
return [
{
label: 'credentials',
status: 'warn',
detail: errSummary(err),
},
];
}
}

/**
* Probe a binary, treating ENOENT (missing on PATH) as a distinct outcome
* from a non-zero exit. `execa({reject:false})` returns a result envelope
Expand Down Expand Up @@ -554,6 +596,9 @@ export async function runProviderChecks(name: ProviderName): Promise<CheckGroup>
case 'e2b':
results = await e2bChecks();
break;
case 'islo':
results = await isloChecks();
break;
}
return { title: name, results };
}
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/provider/argv-prefix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Provider-prefix argv sugar:
*
* agentbox <provider> <subcmd> [...rest]
* where provider ∈ {docker, daytona, hetzner, vercel}
* where provider ∈ {docker, daytona, hetzner, vercel, e2b, islo}
* and subcmd ∈ SUGARED_COMMANDS
*
* ↓ rewritten before commander parses
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/src/provider/cloud-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export async function cloudBackendForProvider(
return (await import('@agentbox/sandbox-vercel')).vercelBackend;
case 'e2b':
return (await import('@agentbox/sandbox-e2b')).e2bBackend;
case 'islo':
return (await import('@agentbox/sandbox-islo')).isloBackend;
default:
return null;
}
Expand Down Expand Up @@ -44,6 +46,8 @@ export async function currentCloudBaseFingerprintLive(
return (await import('@agentbox/sandbox-vercel')).currentVercelBaseFingerprintLive();
case 'e2b':
return (await import('@agentbox/sandbox-e2b')).currentE2bBaseFingerprintLive();
case 'islo':
return undefined;
default:
return undefined;
}
Expand Down
9 changes: 7 additions & 2 deletions apps/cli/src/provider/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import type { EffectiveConfig } from '@agentbox/config';
import type { BoxRecord, Provider, ProviderName } from '@agentbox/core';

export type KnownProviderName = 'docker' | 'daytona' | 'hetzner' | 'vercel' | 'e2b';
export type KnownProviderName = 'docker' | 'daytona' | 'hetzner' | 'vercel' | 'e2b' | 'islo';

const KNOWN: readonly KnownProviderName[] = ['docker', 'daytona', 'hetzner', 'vercel', 'e2b'];
const KNOWN: readonly KnownProviderName[] = ['docker', 'daytona', 'hetzner', 'vercel', 'e2b', 'islo'];

export function isKnownProvider(name: string): name is KnownProviderName {
return (KNOWN as readonly string[]).includes(name);
Expand Down Expand Up @@ -63,6 +63,11 @@ export async function getProvider(name: ProviderName): Promise<Provider> {
await mod.ensureE2bCredentials();
return mod.e2bProvider;
}
case 'islo': {
const mod = await import('@agentbox/sandbox-islo');
await mod.ensureIsloCredentials();
return mod.isloProvider;
}
default:
throw new Error(`unknown sandbox provider: ${String(name)}`);
}
Expand Down
13 changes: 13 additions & 0 deletions apps/cli/src/types/node-pty-prebuilt-multiarch.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
declare module '@homebridge/node-pty-prebuilt-multiarch' {
export function spawn(
file: string,
args: string[],
opts: { name: string; cols: number; rows: number; env: NodeJS.ProcessEnv },
): {
onData(cb: (data: string) => void): void;
onExit(cb: (event: { exitCode: number }) => void): void;
write(data: string): void;
resize(cols: number, rows: number): void;
kill(): void;
};
}
2 changes: 2 additions & 0 deletions apps/cli/test/help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { dockerCommand } from '../src/commands/docker.js';
import { hetznerCommand } from '@agentbox/sandbox-hetzner/cli';
import { vercelCommand } from '@agentbox/sandbox-vercel/cli';
import { e2bCommand } from '@agentbox/sandbox-e2b/cli';
import { isloCommand } from '@agentbox/sandbox-islo/cli';
import { destroyCommand } from '../src/commands/destroy.js';
import { doctorCommand } from '../src/commands/doctor.js';
import { downloadCommand } from '../src/commands/download.js';
Expand Down Expand Up @@ -87,6 +88,7 @@ function buildProgram(): Command {
dockerCommand,
vercelCommand,
e2bCommand,
isloCommand,
gitCommand,
doctorCommand,
updateCommand,
Expand Down
48 changes: 48 additions & 0 deletions docs/islo-and-crabbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Islo and Crabbox

AgentBox treats Islo as an experimental native cloud provider and Crabbox as a companion tool.

## Islo Provider

Use Islo when you want AgentBox boxes to run on persistent Islo agent computers:

```sh
agentbox islo login
agentbox islo create
agentbox islo claude
```

The provider uses the Islo HTTP API for lifecycle, exec, shares, and snapshots. It maps the local-only `agentbox/box:dev` image sentinel to the published AgentBox image:

```text
ghcr.io/madarco/agentbox/box:latest
```

For interactive attach commands such as `agentbox islo shell` or `agentbox islo claude`, run Islo's SSH setup once:

```sh
islo ssh --setup
```

Optional config:

```yaml
box:
provider: islo
sizeIslo: 2-4-20
isloGatewayProfile: production-apis
```

`sizeIslo` uses `cpu-memory-disk` in GB, with memory converted to MB for Islo.

## Crabbox Companion

Crabbox is not an AgentBox provider. It owns a different workflow: lease remote capacity, sync a dirty checkout, run a command, stream output, collect evidence, and release the target.

That makes Crabbox useful beside AgentBox for remote test/proof runs:

```sh
crabbox run -- pnpm test
```

Do not route AgentBox boxes through Crabbox. AgentBox providers need long-lived box lifecycle, attach, screen/code/browser URLs, checkpoints, workspace seeding, and host relay behavior; Crabbox's Islo path is delegated command execution and does not provide that full box surface.
4 changes: 4 additions & 0 deletions packages/config/schema/user-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
"defaultCheckpointHetzner": { "type": "string", "minLength": 1 },
"defaultCheckpointVercel": { "type": "string", "minLength": 1 },
"defaultCheckpointE2b": { "type": "string", "minLength": 1 },
"defaultCheckpointIslo": { "type": "string", "minLength": 1 },
"size": { "type": "string", "minLength": 1 },
"sizeDocker": { "type": "string", "minLength": 1 },
"sizeDaytona": { "type": "string", "minLength": 1 },
"sizeHetzner": { "type": "string", "minLength": 1 },
"sizeVercel": { "type": "string", "minLength": 1 },
"sizeE2b": { "type": "string", "minLength": 1 },
"sizeIslo": { "type": "string", "minLength": 1 },
"withPlaywright": { "type": "boolean" },
"withEnv": { "type": "boolean" },
"resyncOnStart": { "type": "boolean" },
Expand All @@ -37,6 +39,7 @@
"imageHetzner": { "type": "string", "minLength": 1 },
"imageVercel": { "type": "string", "minLength": 1 },
"imageE2b": { "type": "string", "minLength": 1 },
"imageIslo": { "type": "string", "minLength": 1 },
"dockerCacheShared": { "type": "boolean" },
"memory": { "type": "integer", "minimum": 0 },
"cpus": { "type": "integer", "minimum": 0 },
Expand All @@ -46,6 +49,7 @@
"vercelVcpus": { "type": "integer", "minimum": 1 },
"vercelTimeoutMs": { "type": "integer", "minimum": 1 },
"vercelNetworkPolicy": { "type": "string" },
"isloGatewayProfile": { "type": "string" },
"cpMaxBytes": { "type": "integer", "minimum": 1 }
}
},
Expand Down
Loading