From 615636c4e757ca5ae2b7891dbeb5b17967e78129 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 30 Mar 2026 12:00:37 -0400 Subject: [PATCH] Move cp, ssh, preview under `fabro sandbox` namespace These commands all operate on a run's sandbox environment. Grouping them under `fabro sandbox` makes the mental model clear and avoids confusion with `fabro asset cp`. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 +- docs/execution/environments.mdx | 4 +-- docs/human-tools/preview.mdx | 10 +++--- docs/human-tools/ssh-access.mdx | 10 +++--- docs/human-tools/vs-code.mdx | 6 ++-- docs/integrations/daytona.mdx | 2 +- docs/integrations/exe-dev.mdx | 2 +- docs/reference/cli.mdx | 24 ++++++------- lib/crates/fabro-cli/src/args.rs | 35 ++++++++++++++----- lib/crates/fabro-cli/src/commands/mod.rs | 1 + lib/crates/fabro-cli/src/commands/run/mod.rs | 3 -- .../fabro-cli/src/commands/sandbox/mod.rs | 11 ++++++ lib/crates/fabro-cli/src/main.rs | 9 +++-- lib/crates/fabro-cli/tests/it/cmd/cp.rs | 2 +- lib/crates/fabro-cli/tests/it/cmd/fabro.rs | 4 +-- lib/crates/fabro-cli/tests/it/cmd/preview.rs | 2 +- lib/crates/fabro-cli/tests/it/cmd/ssh.rs | 4 +-- .../fabro-cli/tests/it/scenario/lifecycle.rs | 1 + lib/crates/fabro-test/src/lib.rs | 19 ++++++---- 19 files changed, 93 insertions(+), 58 deletions(-) create mode 100644 lib/crates/fabro-cli/src/commands/sandbox/mod.rs diff --git a/README.md b/README.md index 6e733411a..4ccea8c5a 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ curl -fsSL https://fabro.sh/install.sh | bash | 🙋 | Human-in-the-loop | Approval gates pause for human decisions. Steer running agents mid-turn. Interview steps collect structured input | | 🎨 | Multi-model routing | CSS-like stylesheets route each node to the right model and provider, with automatic fallback chains | | ☁️ | Cloud sandboxes | Run agents in isolated Daytona cloud VMs with snapshot-based setup, network controls, and automatic cleanup | -| 🔌 | SSH access and preview links | Shell into running sandboxes with `fabro ssh` and expose ports with `fabro preview` for live debugging | +| 🔌 | SSH access and preview links | Shell into running sandboxes with `fabro sandbox ssh` and expose ports with `fabro sandbox preview` for live debugging | | 🌲 | Git checkpointing | Every stage commits code changes and execution metadata to Git branches. Resume, revert, or trace any change | | 📊 | Automatic retros | Each run generates a retrospective with cost, duration, files touched, and an LLM-written narrative | | ⚡ | Comprehensive API | REST API with SSE event streaming and a React web UI. Run workflows programmatically or as a service | diff --git a/docs/execution/environments.mdx b/docs/execution/environments.mdx index e8581f659..72756abac 100644 --- a/docs/execution/environments.mdx +++ b/docs/execution/environments.mdx @@ -170,7 +170,7 @@ When using server defaults, labels are merged — run config labels override def Connect to a running Daytona sandbox via SSH for live debugging: ```bash -fabro ssh +fabro sandbox ssh ``` This creates temporary SSH credentials (valid for 60 minutes) and connects directly. @@ -306,7 +306,7 @@ image = "my-custom-image:latest" Connect to a running exe.dev sandbox via SSH for live debugging: ```bash -fabro ssh +fabro sandbox ssh ``` This prints the SSH connection command so you can connect to the VM while the workflow runs. diff --git a/docs/human-tools/preview.mdx b/docs/human-tools/preview.mdx index 2d2f2bbb6..850727e21 100644 --- a/docs/human-tools/preview.mdx +++ b/docs/human-tools/preview.mdx @@ -20,15 +20,15 @@ When you request a preview, Fabro generates a time-limited, token-authenticated ## Using preview from the CLI -Use `fabro preview` to generate a preview URL for any run with an active Daytona sandbox: +Use `fabro sandbox preview` to generate a preview URL for any run with an active Daytona sandbox: ```bash -fabro preview 3000 # URL + token + curl example -fabro preview 3000 --signed # self-contained signed URL -fabro preview 3000 --open # open in browser (implies --signed) +fabro sandbox preview 3000 # URL + token + curl example +fabro sandbox preview 3000 --signed # self-contained signed URL +fabro sandbox preview 3000 --open # open in browser (implies --signed) ``` -See [`fabro preview`](/reference/cli#fabro-preview) for the full flag reference. +See [`fabro sandbox preview`](/reference/cli#fabro-sandbox-preview) for the full flag reference. ## Using preview from the web UI diff --git a/docs/human-tools/ssh-access.mdx b/docs/human-tools/ssh-access.mdx index 11142931e..7683148e4 100644 --- a/docs/human-tools/ssh-access.mdx +++ b/docs/human-tools/ssh-access.mdx @@ -11,17 +11,17 @@ SSH access is only available with the Daytona sandbox provider. Local, Docker, a ## Connecting to a run's sandbox -Use `fabro ssh` to connect to the Daytona sandbox from any completed or in-progress run: +Use `fabro sandbox ssh` to connect to the Daytona sandbox from any completed or in-progress run: ```bash -fabro ssh +fabro sandbox ssh ``` Fabro creates temporary SSH credentials and connects directly. Use `--print` to print the SSH command instead of connecting, or `--ttl` to set the credential expiry: ```bash -fabro ssh --print -fabro ssh --ttl 120 +fabro sandbox ssh --print +fabro sandbox ssh --ttl 120 ``` ## Keeping the sandbox alive @@ -56,7 +56,7 @@ Once connected, you have a full shell inside the sandbox VM: ## Credential lifetime -SSH credentials are temporary and expire after **60 minutes** by default. With `fabro ssh`, you can set a custom TTL with `--ttl `. If your session expires, run `fabro ssh` again to get fresh credentials. +SSH credentials are temporary and expire after **60 minutes** by default. With `fabro sandbox ssh`, you can set a custom TTL with `--ttl `. If your session expires, run `fabro sandbox ssh` again to get fresh credentials. ## Limitations diff --git a/docs/human-tools/vs-code.mdx b/docs/human-tools/vs-code.mdx index 6d6f467af..0f9affc4e 100644 --- a/docs/human-tools/vs-code.mdx +++ b/docs/human-tools/vs-code.mdx @@ -22,10 +22,10 @@ VS Code remote access requires [SSH access](/human-tools/ssh-access), which is o fabro run workflow.fabro --sandbox daytona --preserve-sandbox ``` -2. Use `fabro ssh` to get the connection command: +2. Use `fabro sandbox ssh` to get the connection command: ```bash - fabro ssh --print + fabro sandbox ssh --print ``` 3. In VS Code, open the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) and run **Remote-SSH: Connect to Host...** @@ -50,4 +50,4 @@ Once connected, VS Code operates as if the sandbox filesystem were local: - **Use `--preserve-sandbox`** — Without it, the sandbox is destroyed when the workflow finishes and your VS Code session disconnects. Combine with `auto_stop_interval` in your [run config](/execution/run-configuration) to control idle timeout. - **Pair with human gates** — When a workflow pauses at a [human gate](/workflows/human-in-the-loop), connect via VS Code to review the agent's changes before approving. -- **SSH credential lifetime** — Daytona SSH credentials expire after 60 minutes by default. If your VS Code session disconnects, run `fabro ssh ` again to get fresh credentials (use `--ttl` to set a custom expiry). +- **SSH credential lifetime** — Daytona SSH credentials expire after 60 minutes by default. If your VS Code session disconnects, run `fabro sandbox ssh ` again to get fresh credentials (use `--ttl` to set a custom expiry). diff --git a/docs/integrations/daytona.mdx b/docs/integrations/daytona.mdx index 165c91ae2..85094eeb4 100644 --- a/docs/integrations/daytona.mdx +++ b/docs/integrations/daytona.mdx @@ -114,7 +114,7 @@ for your organization. Connect to a running Daytona sandbox via SSH for live debugging: ```bash -fabro ssh +fabro sandbox ssh ``` This creates temporary SSH credentials (valid for 60 minutes) and connects directly. Use `--print` to print the SSH command instead of connecting, or `--ttl` to set the credential expiry. diff --git a/docs/integrations/exe-dev.mdx b/docs/integrations/exe-dev.mdx index de316af2e..92aad3159 100644 --- a/docs/integrations/exe-dev.mdx +++ b/docs/integrations/exe-dev.mdx @@ -41,7 +41,7 @@ image = "my-custom-image:latest" Connect to a running exe.dev sandbox via SSH for live debugging: ```bash -fabro ssh +fabro sandbox ssh ``` This prints the SSH connection command so you can connect to the VM while the workflow runs. diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index e2ffe22b8..8bd099d7c 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -354,14 +354,14 @@ If no LLM provider API keys are configured, the server automatically falls back --- -## `fabro cp` +## `fabro sandbox cp` Copy files between a run's sandbox and the local filesystem. The run must have a persisted sandbox record (Daytona, exe.dev, or a preserved local/Docker sandbox). ```bash -fabro cp :/path/in/sandbox ./local-dir # download -fabro cp ./local-file :/path/in/sandbox # upload -fabro cp -r :/output ./results # recursive download +fabro sandbox cp :/path/in/sandbox ./local-dir # download +fabro sandbox cp ./local-file :/path/in/sandbox # upload +fabro sandbox cp -r :/output ./results # recursive download ``` | Argument / Flag | Description | @@ -679,14 +679,14 @@ fabro diff --stat Output is colorized when writing to a terminal. -## `fabro ssh` +## `fabro sandbox ssh` SSH into a Daytona sandbox from a completed or in-progress run. Creates temporary SSH credentials and connects directly, or prints the command with `--print`. ```bash -fabro ssh -fabro ssh --print -fabro ssh --ttl 120 +fabro sandbox ssh +fabro sandbox ssh --print +fabro sandbox ssh --ttl 120 ``` | Argument / Flag | Description | @@ -697,14 +697,14 @@ fabro ssh --ttl 120 See [SSH Access](/human-tools/ssh-access) for more details on SSH workflows. -## `fabro preview` +## `fabro sandbox preview` Generate a preview URL for a port exposed by a Daytona sandbox. Useful for accessing web servers, dev tools, or APIs running inside the sandbox. ```bash -fabro preview -fabro preview 3000 --signed -fabro preview 3000 --open +fabro sandbox preview +fabro sandbox preview 3000 --signed +fabro sandbox preview 3000 --open ``` | Argument / Flag | Description | diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs index 0cecf5414..c6a57442c 100644 --- a/lib/crates/fabro-cli/src/args.rs +++ b/lib/crates/fabro-cli/src/args.rs @@ -671,12 +671,6 @@ pub(crate) enum RunCommands { #[arg(long)] resume: bool, }, - /// Copy files to/from a run's sandbox - Cp(CpArgs), - /// Get a preview URL for a port on a run's sandbox - Preview(PreviewArgs), - /// SSH into a run's Daytona sandbox - Ssh(SshArgs), /// Show the diff of changes from a workflow run #[command(hide = true)] Diff(DiffArgs), @@ -700,9 +694,6 @@ impl RunCommands { Self::Start { .. } => "start", Self::Attach { .. } => "attach", Self::Detached { .. } => "__detached", - Self::Cp(_) => "cp", - Self::Preview(_) => "preview", - Self::Ssh(_) => "ssh", Self::Diff(_) => "diff", Self::Logs(_) => "logs", Self::Resume(_) => "resume", @@ -713,6 +704,26 @@ impl RunCommands { } } +#[derive(Subcommand)] +pub(crate) enum SandboxCommand { + /// Copy files to/from a run's sandbox + Cp(CpArgs), + /// Get a preview URL for a port on a run's sandbox + Preview(PreviewArgs), + /// SSH into a run's sandbox + Ssh(SshArgs), +} + +impl SandboxCommand { + pub(crate) fn name(&self) -> &'static str { + match self { + Self::Cp(_) => "sandbox cp", + Self::Preview(_) => "sandbox preview", + Self::Ssh(_) => "sandbox ssh", + } + } +} + #[derive(Subcommand)] pub(crate) enum RunsCommands { /// List workflow runs @@ -807,6 +818,11 @@ pub(crate) enum Commands { Repo(RepoNamespace), /// Provider operations Provider(ProviderNamespace), + /// Sandbox operations (cp, ssh, preview) + Sandbox { + #[command(subcommand)] + command: SandboxCommand, + }, /// System maintenance commands System(SystemNamespace), /// Send a queued analytics event (internal) @@ -885,6 +901,7 @@ impl Commands { Self::Provider(ns) => match &ns.command { ProviderCommand::Login(_) => "provider login", }, + Self::Sandbox { command } => command.name(), Self::System(ns) => match &ns.command { SystemCommand::Prune(_) => "system prune", SystemCommand::Df(_) => "system df", diff --git a/lib/crates/fabro-cli/src/commands/mod.rs b/lib/crates/fabro-cli/src/commands/mod.rs index 10d1d8945..ab45ccf85 100644 --- a/lib/crates/fabro-cli/src/commands/mod.rs +++ b/lib/crates/fabro-cli/src/commands/mod.rs @@ -13,6 +13,7 @@ pub(crate) mod provider; pub(crate) mod repo; pub(crate) mod run; pub(crate) mod runs; +pub(crate) mod sandbox; pub(crate) mod secret; pub(crate) mod skill; pub(crate) mod store; diff --git a/lib/crates/fabro-cli/src/commands/run/mod.rs b/lib/crates/fabro-cli/src/commands/run/mod.rs index 9487cf177..d92d9de88 100644 --- a/lib/crates/fabro-cli/src/commands/run/mod.rs +++ b/lib/crates/fabro-cli/src/commands/run/mod.rs @@ -68,9 +68,6 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<( launcher_path, resume, } => detached::execute(run_dir, launcher_path, resume).await, - RunCommands::Cp(args) => cp::cp_command(args, globals).await, - RunCommands::Preview(args) => preview::run(args, globals).await, - RunCommands::Ssh(args) => ssh::run(args, globals).await, RunCommands::Diff(args) => diff::run(args, globals).await, RunCommands::Logs(args) => { let styles = Styles::detect_stdout(); diff --git a/lib/crates/fabro-cli/src/commands/sandbox/mod.rs b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs new file mode 100644 index 000000000..354cff411 --- /dev/null +++ b/lib/crates/fabro-cli/src/commands/sandbox/mod.rs @@ -0,0 +1,11 @@ +use anyhow::Result; + +use crate::args::{GlobalArgs, SandboxCommand}; + +pub(crate) async fn dispatch(command: SandboxCommand, globals: &GlobalArgs) -> Result<()> { + match command { + SandboxCommand::Cp(args) => super::run::cp::cp_command(args, globals).await, + SandboxCommand::Preview(args) => super::run::preview::run(args, globals).await, + SandboxCommand::Ssh(args) => super::run::ssh::run(args, globals).await, + } +} diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 30dc7e1af..af5993e41 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -217,6 +217,7 @@ async fn main_inner() -> (String, Result<()>) { commands::upgrade::run_upgrade(args).await?; } Commands::Provider(ns) => commands::provider::dispatch(ns).await?, + Commands::Sandbox { command } => commands::sandbox::dispatch(command, &globals).await?, Commands::System(ns) => commands::system::dispatch(ns, &globals).await?, Commands::SendAnalytics { path } => { let result = sender::upload(&path).await; @@ -382,11 +383,13 @@ mod tests { } #[test] - fn parse_cp_command() { - let cli = Cli::try_parse_from(["fabro", "cp", "ABC123:/tmp/file", "./file"]) + fn parse_sandbox_cp_command() { + let cli = Cli::try_parse_from(["fabro", "sandbox", "cp", "ABC123:/tmp/file", "./file"]) .expect("should parse"); match *cli.command { - Commands::RunCmd(RunCommands::Cp(args)) => { + Commands::Sandbox { + command: args::SandboxCommand::Cp(args), + } => { assert_eq!(args.src, "ABC123:/tmp/file"); assert_eq!(args.dst, "./file"); assert!(!args.recursive); diff --git a/lib/crates/fabro-cli/tests/it/cmd/cp.rs b/lib/crates/fabro-cli/tests/it/cmd/cp.rs index 7f2a1f2b6..c0a8fcdca 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/cp.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/cp.rs @@ -11,7 +11,7 @@ fn help() { ----- stdout ----- Copy files to/from a run's sandbox - Usage: fabro cp [OPTIONS] + Usage: fabro sandbox cp [OPTIONS] Arguments: Source: : or local path diff --git a/lib/crates/fabro-cli/tests/it/cmd/fabro.rs b/lib/crates/fabro-cli/tests/it/cmd/fabro.rs index 92aad0671..0d643c60c 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/fabro.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/fabro.rs @@ -16,9 +16,6 @@ fn help() { create Create a workflow run (allocate run dir, persist spec) start Start a created workflow run (spawn engine process) attach Attach to a running or finished workflow run - cp Copy files to/from a run's sandbox - preview Get a preview URL for a port on a run's sandbox - ssh SSH into a run's Daytona sandbox logs View the event log of a workflow run resume Resume an interrupted workflow run rewind Rewind a workflow run to an earlier checkpoint @@ -43,6 +40,7 @@ fn help() { upgrade Upgrade fabro to the latest version repo Repository commands provider Provider operations + sandbox Sandbox operations (cp, ssh, preview) system System maintenance commands help Print this message or the help of the given subcommand(s) diff --git a/lib/crates/fabro-cli/tests/it/cmd/preview.rs b/lib/crates/fabro-cli/tests/it/cmd/preview.rs index 7b11aa0ff..2c03c208a 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/preview.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/preview.rs @@ -11,7 +11,7 @@ fn help() { ----- stdout ----- Get a preview URL for a port on a run's sandbox - Usage: fabro preview [OPTIONS] + Usage: fabro sandbox preview [OPTIONS] Arguments: Run ID or prefix diff --git a/lib/crates/fabro-cli/tests/it/cmd/ssh.rs b/lib/crates/fabro-cli/tests/it/cmd/ssh.rs index 0615aa198..a2c819963 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/ssh.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/ssh.rs @@ -9,9 +9,9 @@ fn help() { success: true exit_code: 0 ----- stdout ----- - SSH into a run's Daytona sandbox + SSH into a run's sandbox - Usage: fabro ssh [OPTIONS] + Usage: fabro sandbox ssh [OPTIONS] Arguments: Run ID or prefix diff --git a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs index 1409c5c3b..ab9c8c778 100644 --- a/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs +++ b/lib/crates/fabro-cli/tests/it/scenario/lifecycle.rs @@ -163,6 +163,7 @@ fn local_run_lifecycle() { .unwrap(); let cp_dest = context.temp_dir.join("cp_download.txt"); cmd(&[ + "sandbox", "cp", &format!("{run_id}:cp_test.txt"), cp_dest.to_str().unwrap(), diff --git a/lib/crates/fabro-test/src/lib.rs b/lib/crates/fabro-test/src/lib.rs index 4f403a204..097f94f3c 100644 --- a/lib/crates/fabro-test/src/lib.rs +++ b/lib/crates/fabro-test/src/lib.rs @@ -161,10 +161,17 @@ impl TestContext { cmd } - /// Build a `cp` subcommand. + /// Build a `sandbox` subcommand. + pub fn sandbox(&self) -> Command { + let mut cmd = self.command(); + cmd.arg("sandbox"); + cmd + } + + /// Build a `sandbox cp` subcommand. pub fn cp(&self) -> Command { let mut cmd = self.command(); - cmd.arg("cp"); + cmd.args(["sandbox", "cp"]); cmd } @@ -189,10 +196,10 @@ impl TestContext { cmd } - /// Build a `preview` subcommand. + /// Build a `sandbox preview` subcommand. pub fn preview(&self) -> Command { let mut cmd = self.command(); - cmd.arg("preview"); + cmd.args(["sandbox", "preview"]); cmd } @@ -203,10 +210,10 @@ impl TestContext { cmd } - /// Build an `ssh` subcommand. + /// Build a `sandbox ssh` subcommand. pub fn ssh(&self) -> Command { let mut cmd = self.command(); - cmd.arg("ssh"); + cmd.args(["sandbox", "ssh"]); cmd }