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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dist/
dist-cloudflare/
artifacts/
.crabbox/
.env.local
node_modules/
.wrangler/
*.test
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ Supported providers:
- [Cloudflare](docs/providers/cloudflare.md)
(`provider: cloudflare`): delegated Cloudflare execution through a Worker and
container runner.
- [Freestyle](docs/providers/freestyle.md) (`provider: freestyle`): delegated
Freestyle VM execution through the Freestyle REST API.
- [Railway](docs/providers/railway.md) (`provider: railway`): delegated
redeploy-and-stream execution against a pre-existing Railway service through
the [Railway](https://railway.com) GraphQL API.
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Pick whichever matches your intent:
- **Start here:** [Getting started](getting-started.md), [How Crabbox Works](how-it-works.md), [Concepts and glossary](concepts.md).
- **Get the mental model:** [Vision](vision.md), [Architecture](architecture.md), [Orchestrator](orchestrator.md).
- **Use the CLI:** [CLI](cli.md), [Commands](commands/README.md), [Features](features/README.md), [Configuration](features/configuration.md), [Jobs](features/jobs.md), [Actions hydration](features/actions-hydration.md), [Capsules](features/capsules.md), [Checkpoints](features/checkpoints.md), [Browser portal](features/portal.md), [Telemetry](features/telemetry.md).
- **Pick or add a target:** [Provider reference](providers/README.md), [Providers feature overview](features/providers.md), [Provider authoring](features/provider-authoring.md), [Provider backends](provider-backends.md), [AWS](providers/aws.md), [Azure](providers/azure.md), [Google Cloud](providers/gcp.md), [Hetzner](providers/hetzner.md), [Proxmox](providers/proxmox.md), [Parallels](providers/parallels.md), [Local Container](providers/local-container.md), [Static SSH](providers/ssh.md), [Blacksmith Testbox](providers/blacksmith-testbox.md), [Namespace Devbox](providers/namespace-devbox.md), [Semaphore](providers/semaphore.md), [Sprites](providers/sprites.md), [Daytona](providers/daytona.md), [Islo](providers/islo.md), [E2B](providers/e2b.md), [Modal](providers/modal.md), [Tensorlake](providers/tensorlake.md), [Cloudflare](providers/cloudflare.md), [Interactive desktop and VNC](features/interactive-desktop-vnc.md).
- **Pick or add a target:** [Provider reference](providers/README.md), [Providers feature overview](features/providers.md), [Provider authoring](features/provider-authoring.md), [Provider backends](provider-backends.md), [AWS](providers/aws.md), [Azure](providers/azure.md), [Google Cloud](providers/gcp.md), [Hetzner](providers/hetzner.md), [Proxmox](providers/proxmox.md), [Parallels](providers/parallels.md), [Local Container](providers/local-container.md), [Static SSH](providers/ssh.md), [Blacksmith Testbox](providers/blacksmith-testbox.md), [Namespace Devbox](providers/namespace-devbox.md), [Semaphore](providers/semaphore.md), [Sprites](providers/sprites.md), [Daytona](providers/daytona.md), [Islo](providers/islo.md), [E2B](providers/e2b.md), [Modal](providers/modal.md), [Tensorlake](providers/tensorlake.md), [Cloudflare](providers/cloudflare.md), [Freestyle](providers/freestyle.md), [Interactive desktop and VNC](features/interactive-desktop-vnc.md).
- **Operate it:** [Operations](operations.md), [Observability](observability.md), [Troubleshooting](troubleshooting.md), [Performance](performance.md).
- **Set it up or audit it:** [Infrastructure](infrastructure.md), [Security](security.md), [Source Map](source-map.md).

Expand Down
8 changes: 8 additions & 0 deletions docs/features/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ islo Islo sandboxes with delegated command execution
e2b E2B sandboxes with delegated command execution
modal Modal Sandboxes with delegated command execution
tensorlake Tensorlake Firecracker sandboxes with delegated command execution
freestyle Freestyle VMs with delegated command execution
```

## Provider Pages
Expand All @@ -63,6 +64,7 @@ tensorlake Tensorlake Firecracker sandboxes with delegated command execution
- [E2B](../providers/e2b.md): delegated E2B sandbox execution.
- [Modal](../providers/modal.md): delegated Modal Sandbox execution.
- [Tensorlake](../providers/tensorlake.md): delegated Tensorlake Firecracker sandbox execution.
- [Freestyle](../providers/freestyle.md): delegated Freestyle VM execution.
- [Provider backends](../provider-backends.md): implementation guide for adding a new provider/backend/plugin.

## Hetzner Summary
Expand Down Expand Up @@ -231,6 +233,11 @@ backend: Crabbox creates Modal Sandboxes through the local Python client, syncs
gzipped archive through Sandbox exec, and streams command output from Modal's
process API. See [Modal](../providers/modal.md).

Crabbox can use Freestyle VMs with `provider: freestyle`. Freestyle is a delegated
run backend: Crabbox creates VMs through the Freestyle REST API, syncs a gzipped
archive with file-API or exec fallback upload, and runs commands through
Freestyle exec. See [Freestyle](../providers/freestyle.md).

Static SSH targets:

```yaml
Expand Down Expand Up @@ -280,5 +287,6 @@ Related docs:
- [Islo](../providers/islo.md)
- [E2B](../providers/e2b.md)
- [Modal](../providers/modal.md)
- [Freestyle](../providers/freestyle.md)
- [Runner bootstrap](runner-bootstrap.md)
- [Cost and usage](cost-usage.md)
1 change: 1 addition & 0 deletions docs/providers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ static SSH provider for existing machines.
| [Upstash Box](upstash-box.md) | delegated run | Linux | Upstash Box execution through the Box REST API |
| [Tensorlake](tensorlake.md) | delegated run | Linux | Tensorlake Firecracker sandbox execution via the `tensorlake` CLI |
| [Cloudflare](cloudflare.md) | delegated run | Linux | Cloudflare execution through a Worker and container runner |
| [Freestyle](freestyle.md) | delegated run | Linux | Freestyle VM execution through the Freestyle REST API |
| [Railway](railway.md) | delegated run | Linux | redeploy and stream logs for an existing Railway service via the GraphQL API |
| [RunPod](runpod.md) | SSH lease | Linux | disposable RunPod pods provisioned via the REST API and accessed over public SSH |
| [W&B Sandboxes](wandb.md) | delegated run | Linux | [wandb.ai](https://wandb.ai/) (by CoreWeave) — use an existing `wandb login` credential plus `WANDB_ENTITY_NAME` |
Expand Down
127 changes: 127 additions & 0 deletions docs/providers/freestyle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Freestyle Provider

Read when:

- choosing `provider: freestyle`;
- configuring Freestyle VM size or workdir;
- changing `internal/providers/freestyle`.

Freestyle is a delegated run provider. Crabbox uses the [Freestyle](https://freestyle.sh)
v1 REST API (`https://api.freestyle.sh`) for VM lifecycle, archive sync, and
command execution through pure Go `net/http` calls. Freestyle owns VM state and
exec transport; Crabbox owns local config, repo claims, sync manifests and
guardrails, slugs, timing summaries, and normalized list/status rendering.

## When To Use

Use Freestyle when the remote Linux VM should be owned by Freestyle and commands
can run through Freestyle's exec API. Use AWS, Hetzner, Static SSH, or Daytona
when you need Crabbox SSH access.

## Commands

```sh
crabbox warmup --provider freestyle --keep
crabbox run --provider freestyle -- pnpm test
crabbox run --provider freestyle --sync-only
crabbox run --provider freestyle --id blue-lobster --shell 'pnpm install && pnpm test'
crabbox status --provider freestyle --id blue-lobster
crabbox stop --provider freestyle blue-lobster
crabbox list --provider freestyle
crabbox doctor --provider freestyle
```

## Live Smoke

Use a live smoke when changing Freestyle lifecycle, sync, or exec code. Keep the
API key in `FREESTYLE_API_KEY`; do not pass it as a command-line argument.

```sh
export FREESTYLE_API_KEY=...
go build -trimpath -o bin/crabbox ./cmd/crabbox

bin/crabbox run --provider freestyle --keep 'bash test.sh'
bin/crabbox warmup --provider freestyle --actions-runner # expect exit 2
bin/crabbox doctor --provider freestyle
```

Expected results:

- `run` prints a Freestyle lease (`fsb_...`), sync summary, command output, and
`exit=0`.
- `warmup --actions-runner` is rejected because Freestyle does not register
GitHub Actions runners.
- `doctor` reports inventory readiness when auth and list calls succeed.

## Auth

```sh
export FREESTYLE_API_KEY=...
```

`CRABBOX_FREESTYLE_API_KEY` is also accepted and wins over `FREESTYLE_API_KEY`.

Freestyle API keys must not be passed as CLI flags. Crabbox reads them from
environment variables only.

`FREESTYLE_API_URL` or `freestyle.apiUrl` can override the default
`https://api.freestyle.sh`.

## Config

```yaml
provider: freestyle
target: linux
freestyle:
apiUrl: https://api.freestyle.sh
workdir: crabbox
# Optional. Omit to use Freestyle plan defaults.
vcpus: 2
memoryMB: 4096
```

Provider flags:

```text
--freestyle-api-url
--freestyle-workdir
--freestyle-vcpus
--freestyle-memory-mb
```

`--freestyle-workdir` / `freestyle.workdir` is interpreted as a relative
directory below `/workspace`. Crabbox rejects absolute paths and `..` escapes
before workspace preparation and sync.

## Lifecycle

1. Create or resolve a Crabbox-owned Freestyle VM.
2. Store a local lease ID with the `fsb_` prefix and a friendly slug.
3. Validate the Freestyle workdir, build the Crabbox sync manifest, and upload a
gzipped archive into `/workspace/<freestyle.workdir>`.
4. Execute commands through Freestyle's exec API in that workdir via `bash -lc`.
5. Stop deletes the VM and removes the local lease claim.

## Sync

Freestyle advertises archive sync (`FeatureArchiveSync`). Crabbox supports
`--sync-only`, `--force-sync-large`, and `--no-sync`.

Archive upload tries Freestyle's file API first. When that endpoint is
unavailable, Crabbox falls back to chunked base64 upload through exec. Sync still
completes reliably through the fallback path.

`--checksum` is not supported because Freestyle uses archive sync rather than
Crabbox rsync checksum mode.

## Limitations

- `--actions-runner` is not supported for warmup or run.
- `--checksum` is not supported.
- No SSH lease path; this is delegated run only.
- API keys are env-only; there is no `--freestyle-api-key` flag.

## Doctor

`crabbox doctor --provider freestyle` checks auth and lists Crabbox-owned VMs in
the Freestyle account. It does not create a VM during the check.
44 changes: 42 additions & 2 deletions internal/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ type Config struct {
Wandb WandbConfig
Islo IsloConfig
isloImageExplicit bool
Freestyle FreestyleConfig
Tensorlake TensorlakeConfig
Modal ModalConfig
UpstashBox UpstashBoxConfig
Expand Down Expand Up @@ -255,6 +256,14 @@ type IsloConfig struct {
DiskGB int
}

type FreestyleConfig struct {
APIKey string
APIURL string
Workdir string
VCPUs int
MemoryMB int
}

type TensorlakeConfig struct {
APIKey string
APIURL string
Expand Down Expand Up @@ -731,6 +740,10 @@ func baseConfig() Config {
MemoryMB: 4096,
DiskGB: 20,
},
Freestyle: FreestyleConfig{
APIURL: "https://api.freestyle.sh",
Workdir: "crabbox",
},
Tensorlake: TensorlakeConfig{
APIURL: "https://api.tensorlake.ai",
CLIPath: "tensorlake",
Expand Down Expand Up @@ -828,6 +841,7 @@ type fileConfig struct {
Runpod *fileRunpodConfig `yaml:"runpod,omitempty"`
Wandb *fileWandbConfig `yaml:"wandb,omitempty"`
Islo *fileIsloConfig `yaml:"islo,omitempty"`
Freestyle *fileFreestyleConfig `yaml:"freestyle,omitempty"`
Tensorlake *fileTensorlakeConfig `yaml:"tensorlake,omitempty"`
Modal *fileModalConfig `yaml:"modal,omitempty"`
UpstashBox *fileUpstashBoxConfig `yaml:"upstashBox,omitempty"`
Expand Down Expand Up @@ -1106,6 +1120,13 @@ type fileIsloConfig struct {
DiskGB int `yaml:"diskGB,omitempty"`
}

type fileFreestyleConfig struct {
APIURL string `yaml:"apiUrl,omitempty"`
Workdir string `yaml:"workdir,omitempty"`
VCPUs int `yaml:"vcpus,omitempty"`
MemoryMB int `yaml:"memoryMB,omitempty"`
}

type fileTensorlakeConfig struct {
APIURL string `yaml:"apiUrl,omitempty"`
CLIPath string `yaml:"cliPath,omitempty"`
Expand Down Expand Up @@ -2055,6 +2076,20 @@ func applyFileConfig(cfg *Config, file fileConfig) {
cfg.Islo.DiskGB = file.Islo.DiskGB
}
}
if file.Freestyle != nil {
if file.Freestyle.APIURL != "" {
cfg.Freestyle.APIURL = file.Freestyle.APIURL
}
if file.Freestyle.Workdir != "" {
cfg.Freestyle.Workdir = file.Freestyle.Workdir
}
if file.Freestyle.VCPUs > 0 {
cfg.Freestyle.VCPUs = file.Freestyle.VCPUs
}
if file.Freestyle.MemoryMB > 0 {
cfg.Freestyle.MemoryMB = file.Freestyle.MemoryMB
}
}
if file.Tensorlake != nil {
if file.Tensorlake.APIURL != "" {
cfg.Tensorlake.APIURL = file.Tensorlake.APIURL
Expand Down Expand Up @@ -2790,6 +2825,11 @@ func applyEnv(cfg *Config) {
cfg.Islo.VCPUs = getenvInt("CRABBOX_ISLO_VCPUS", cfg.Islo.VCPUs)
cfg.Islo.MemoryMB = getenvInt("CRABBOX_ISLO_MEMORY_MB", cfg.Islo.MemoryMB)
cfg.Islo.DiskGB = getenvInt("CRABBOX_ISLO_DISK_GB", cfg.Islo.DiskGB)
cfg.Freestyle.APIKey = getenv("CRABBOX_FREESTYLE_API_KEY", getenv("FREESTYLE_API_KEY", cfg.Freestyle.APIKey))
cfg.Freestyle.APIURL = getenv("CRABBOX_FREESTYLE_API_URL", getenv("FREESTYLE_API_URL", cfg.Freestyle.APIURL))
cfg.Freestyle.Workdir = getenv("CRABBOX_FREESTYLE_WORKDIR", cfg.Freestyle.Workdir)
cfg.Freestyle.VCPUs = getenvInt("CRABBOX_FREESTYLE_VCPUS", cfg.Freestyle.VCPUs)
cfg.Freestyle.MemoryMB = getenvInt("CRABBOX_FREESTYLE_MEMORY_MB", cfg.Freestyle.MemoryMB)
cfg.Tensorlake.APIKey = getenv("CRABBOX_TENSORLAKE_API_KEY", getenv("TENSORLAKE_API_KEY", cfg.Tensorlake.APIKey))
cfg.Tensorlake.APIURL = getenv("CRABBOX_TENSORLAKE_API_URL", getenv("TENSORLAKE_API_URL", cfg.Tensorlake.APIURL))
cfg.Tensorlake.CLIPath = getenv("CRABBOX_TENSORLAKE_CLI", cfg.Tensorlake.CLIPath)
Expand Down Expand Up @@ -2961,7 +3001,7 @@ func serverTypeForConfig(cfg Config) string {
if resolved, err := ProviderFor(cfg.Provider); err == nil {
cfg.Provider = resolved.Name()
}
if isBlacksmithProvider(cfg.Provider) || isStaticProvider(cfg.Provider) || cfg.Provider == "islo" || cfg.Provider == "sprites" || cfg.Provider == "local-container" {
if isBlacksmithProvider(cfg.Provider) || isStaticProvider(cfg.Provider) || cfg.Provider == "islo" || cfg.Provider == "freestyle" || cfg.Provider == "sprites" || cfg.Provider == "local-container" {
return ""
}
if cfg.Provider == "namespace-devbox" || cfg.Provider == "namespace" {
Expand Down Expand Up @@ -3007,7 +3047,7 @@ func serverTypeForProviderClass(provider, class string) string {
if resolved, err := ProviderFor(provider); err == nil {
provider = resolved.Name()
}
if isBlacksmithProvider(provider) || isStaticProvider(provider) || provider == "islo" || provider == "sprites" || provider == "local-container" {
if isBlacksmithProvider(provider) || isStaticProvider(provider) || provider == "islo" || provider == "freestyle" || provider == "sprites" || provider == "local-container" {
return ""
}
if provider == "namespace-devbox" || provider == "namespace" {
Expand Down
25 changes: 25 additions & 0 deletions internal/cli/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ func clearConfigEnv(t *testing.T) {
"CRABBOX_ISLO_VCPUS",
"CRABBOX_ISLO_MEMORY_MB",
"CRABBOX_ISLO_DISK_GB",
"CRABBOX_FREESTYLE_API_KEY",
"FREESTYLE_API_KEY",
"CRABBOX_FREESTYLE_API_URL",
"FREESTYLE_API_URL",
"CRABBOX_FREESTYLE_WORKDIR",
"CRABBOX_FREESTYLE_VCPUS",
"CRABBOX_FREESTYLE_MEMORY_MB",
"CRABBOX_TENSORLAKE_API_KEY",
"TENSORLAKE_API_KEY",
"CRABBOX_TENSORLAKE_API_URL",
Expand Down Expand Up @@ -467,6 +474,11 @@ islo:
vcpus: 4
memoryMB: 8192
diskGB: 40
freestyle:
apiUrl: https://freestyle.example.test
workdir: team/repo
vcpus: 4
memoryMB: 8192
tensorlake:
apiUrl: https://api.tensorlake.example.test
cliPath: /usr/local/bin/tl
Expand Down Expand Up @@ -624,6 +636,9 @@ ssh:
if cfg.Islo.BaseURL != "https://islo.example.test" || cfg.Islo.Image != "docker.io/library/ubuntu:24.04" || cfg.Islo.Workdir != "crabbox" || cfg.Islo.GatewayProfile != "default" || cfg.Islo.SnapshotName != "snap-ready" || cfg.Islo.VCPUs != 4 || cfg.Islo.MemoryMB != 8192 || cfg.Islo.DiskGB != 40 {
t.Fatalf("islo config not loaded: %#v", cfg.Islo)
}
if cfg.Freestyle.APIURL != "https://freestyle.example.test" || cfg.Freestyle.Workdir != "team/repo" || cfg.Freestyle.VCPUs != 4 || cfg.Freestyle.MemoryMB != 8192 {
t.Fatalf("freestyle config not loaded: %#v", cfg.Freestyle)
}
if cfg.Tensorlake.APIURL != "https://api.tensorlake.example.test" || cfg.Tensorlake.CLIPath != "/usr/local/bin/tl" || cfg.Tensorlake.Image != "ubuntu-22.04" || cfg.Tensorlake.Snapshot != "snap-tl" || cfg.Tensorlake.OrganizationID != "org-tl" || cfg.Tensorlake.ProjectID != "proj-tl" || cfg.Tensorlake.Namespace != "ns-tl" || cfg.Tensorlake.Workdir != "/workspace/crabbox-test" || cfg.Tensorlake.CPUs != 4 || cfg.Tensorlake.MemoryMB != 8192 || cfg.Tensorlake.DiskMB != 30000 || cfg.Tensorlake.TimeoutSecs != 1800 || !cfg.Tensorlake.NoInternet {
t.Fatalf("tensorlake config not loaded: %#v", cfg.Tensorlake)
}
Expand Down Expand Up @@ -832,6 +847,13 @@ func TestEnvOverridesConfig(t *testing.T) {
t.Setenv("CRABBOX_ISLO_VCPUS", "8")
t.Setenv("CRABBOX_ISLO_MEMORY_MB", "16384")
t.Setenv("CRABBOX_ISLO_DISK_GB", "80")
t.Setenv("FREESTYLE_API_KEY", "freestyle-key-file")
t.Setenv("CRABBOX_FREESTYLE_API_KEY", "freestyle-key-env")
t.Setenv("FREESTYLE_API_URL", "https://freestyle-file.example")
t.Setenv("CRABBOX_FREESTYLE_API_URL", "https://freestyle-env.example")
t.Setenv("CRABBOX_FREESTYLE_WORKDIR", "env/repo")
t.Setenv("CRABBOX_FREESTYLE_VCPUS", "6")
t.Setenv("CRABBOX_FREESTYLE_MEMORY_MB", "12288")
t.Setenv("TENSORLAKE_API_KEY", "tl-api-file")
t.Setenv("CRABBOX_TENSORLAKE_API_KEY", "tl-api-env")
t.Setenv("TENSORLAKE_API_URL", "https://api.tl-file.example")
Expand Down Expand Up @@ -992,6 +1014,9 @@ func TestEnvOverridesConfig(t *testing.T) {
if cfg.Islo.APIKey != "islo-api-env" || cfg.Islo.BaseURL != "https://islo-env.example" || cfg.Islo.Image != "ubuntu:env" || cfg.Islo.Workdir != "env-workdir" || cfg.Islo.GatewayProfile != "env-gateway" || cfg.Islo.SnapshotName != "env-snapshot" || cfg.Islo.VCPUs != 8 || cfg.Islo.MemoryMB != 16384 || cfg.Islo.DiskGB != 80 {
t.Fatalf("unexpected islo env: %#v", cfg.Islo)
}
if cfg.Freestyle.APIKey != "freestyle-key-env" || cfg.Freestyle.APIURL != "https://freestyle-env.example" || cfg.Freestyle.Workdir != "env/repo" || cfg.Freestyle.VCPUs != 6 || cfg.Freestyle.MemoryMB != 12288 {
t.Fatalf("unexpected freestyle env: %#v", cfg.Freestyle)
}
if cfg.Tensorlake.APIKey != "tl-api-env" || cfg.Tensorlake.APIURL != "https://api.tl-env.example" || cfg.Tensorlake.CLIPath != "/opt/tl/bin/tensorlake" || cfg.Tensorlake.Image != "ubuntu:tl-env" || cfg.Tensorlake.Snapshot != "snap-tl-env" || cfg.Tensorlake.OrganizationID != "org-tl-env" || cfg.Tensorlake.ProjectID != "proj-tl-env" || cfg.Tensorlake.Namespace != "ns-tl-env" || cfg.Tensorlake.Workdir != "/workspace/tl-env" || cfg.Tensorlake.CPUs != 2.5 || cfg.Tensorlake.MemoryMB != 4096 || cfg.Tensorlake.DiskMB != 20480 || cfg.Tensorlake.TimeoutSecs != 900 || !cfg.Tensorlake.NoInternet {
t.Fatalf("unexpected tensorlake env: %#v", cfg.Tensorlake)
}
Expand Down
Loading