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) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-30 12:00:37 -04:00
parent 69510cd016
commit 615636c4e7
No known key found for this signature in database
19 changed files with 93 additions and 58 deletions

View file

@ -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 |

View file

@ -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 <run-id>
fabro sandbox ssh <run-id>
```
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 <run-id>
fabro sandbox ssh <run-id>
```
This prints the SSH connection command so you can connect to the VM while the workflow runs.

View file

@ -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 <run-id> 3000 # URL + token + curl example
fabro preview <run-id> 3000 --signed # self-contained signed URL
fabro preview <run-id> 3000 --open # open in browser (implies --signed)
fabro sandbox preview <run-id> 3000 # URL + token + curl example
fabro sandbox preview <run-id> 3000 --signed # self-contained signed URL
fabro sandbox preview <run-id> 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

View file

@ -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 <run-id>
fabro sandbox ssh <run-id>
```
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 <run-id> --print
fabro ssh <run-id> --ttl 120
fabro sandbox ssh <run-id> --print
fabro sandbox ssh <run-id> --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 <MINUTES>`. 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 <MINUTES>`. If your session expires, run `fabro sandbox ssh` again to get fresh credentials.
## Limitations

View file

@ -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 <run-id> --print
fabro sandbox ssh <run-id> --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 <run-id>` 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 <run-id>` again to get fresh credentials (use `--ttl` to set a custom expiry).

View file

@ -114,7 +114,7 @@ for your organization.
Connect to a running Daytona sandbox via SSH for live debugging:
```bash
fabro ssh <run-id>
fabro sandbox ssh <run-id>
```
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.

View file

@ -41,7 +41,7 @@ image = "my-custom-image:latest"
Connect to a running exe.dev sandbox via SSH for live debugging:
```bash
fabro ssh <run-id>
fabro sandbox ssh <run-id>
```
This prints the SSH connection command so you can connect to the VM while the workflow runs.

View file

@ -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 <run-id>:/path/in/sandbox ./local-dir # download
fabro cp ./local-file <run-id>:/path/in/sandbox # upload
fabro cp -r <run-id>:/output ./results # recursive download
fabro sandbox cp <run-id>:/path/in/sandbox ./local-dir # download
fabro sandbox cp ./local-file <run-id>:/path/in/sandbox # upload
fabro sandbox cp -r <run-id>:/output ./results # recursive download
```
| Argument / Flag | Description |
@ -679,14 +679,14 @@ fabro diff <run-id> --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 <run-id>
fabro ssh <run-id> --print
fabro ssh <run-id> --ttl 120
fabro sandbox ssh <run-id>
fabro sandbox ssh <run-id> --print
fabro sandbox ssh <run-id> --ttl 120
```
| Argument / Flag | Description |
@ -697,14 +697,14 @@ fabro ssh <run-id> --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 <run-id> <port>
fabro preview <run-id> 3000 --signed
fabro preview <run-id> 3000 --open
fabro sandbox preview <run-id> <port>
fabro sandbox preview <run-id> 3000 --signed
fabro sandbox preview <run-id> 3000 --open
```
| Argument / Flag | Description |

View file

@ -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",

View file

@ -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;

View file

@ -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();

View file

@ -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,
}
}

View file

@ -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);

View file

@ -11,7 +11,7 @@ fn help() {
----- stdout -----
Copy files to/from a run's sandbox
Usage: fabro cp [OPTIONS] <SRC> <DST>
Usage: fabro sandbox cp [OPTIONS] <SRC> <DST>
Arguments:
<SRC> Source: <run-id>:<path> or local path

View file

@ -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)

View file

@ -11,7 +11,7 @@ fn help() {
----- stdout -----
Get a preview URL for a port on a run's sandbox
Usage: fabro preview [OPTIONS] <RUN> <PORT>
Usage: fabro sandbox preview [OPTIONS] <RUN> <PORT>
Arguments:
<RUN> Run ID or prefix

View file

@ -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] <RUN>
Usage: fabro sandbox ssh [OPTIONS] <RUN>
Arguments:
<RUN> Run ID or prefix

View file

@ -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(),

View file

@ -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
}