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
19 changes: 16 additions & 3 deletions .agents/skills/openshell-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ The simplest way to get a sandbox running:
openshell sandbox create
```

This creates a sandbox with defaults and drops you into an interactive shell.
This creates a sandbox whose canonical main process is `/bin/bash -l` and
attaches your terminal to that retained process. Add `--detach` to return after
the sandbox becomes ready without attaching.

When supplying `--name`, use a portable DNS-1123 label: at most 63 lowercase alphanumeric or `-` characters, beginning and ending with an alphanumeric character. The Kubernetes driver rejects uppercase letters, underscores, dots, and other names that cannot become Kubernetes resource labels.

Expand Down Expand Up @@ -219,13 +221,19 @@ Key flags:
- `--driver-config-json`: Pass experimental driver-specific sandbox configuration
- `--label KEY=VALUE`: Add labels for later selection (repeatable)
- `--env KEY=VALUE`: Set non-secret sandbox environment variables (repeatable); use `--provider` for credentials
- `--tty`: Allocate a retained PTY for the canonical main process
- `--approval-mode manual|auto`: Control handling of agent-authored policy proposals; `manual` is the default
- `--upload <PATH>[:<DEST>]`: Upload local files into the container working directory or an explicit destination
- `--no-git-ignore`: Disable `.gitignore` filtering for uploads
- `--no-keep`: Delete the sandbox after the initial command or shell exits
- `--detach`: Start the canonical main process without attaching
- `--forward [BIND_ADDRESS:]PORT`: Forward a local port and keep the sandbox alive
- `--editor vscode|cursor`: Open a remote editor after creation and keep the sandbox alive

Do not combine `--upload` with a trailing main command. Uploads currently finish
after the canonical process starts; create a scratch sandbox and use
`sandbox exec`, or build the files into the image.

### List and inspect sandboxes

```bash
Expand All @@ -243,7 +251,10 @@ openshell sandbox connect my-sandbox
openshell sandbox connect my-sandbox --editor vscode
```

Opens an interactive SSH shell. To configure VS Code Remote-SSH:
Attaches to the sandbox's existing canonical main process. Disconnecting leaves
that process running; reconnecting targets the same process instance and replays
recent output. Use `sandbox exec --tty -- /bin/bash -l` for a new shell. To
configure VS Code Remote-SSH:

```bash
openshell sandbox ssh-config my-sandbox >> ~/.ssh/config
Expand Down Expand Up @@ -276,7 +287,9 @@ openshell sandbox exec --name my-sandbox --workdir /workspace -- ls -la
openshell sandbox exec --name my-sandbox --env MODE=test -- cargo test
```

`sandbox exec` streams output and exits with the remote command's exit code. Use `sandbox connect` for an interactive shell.
`sandbox exec` starts an independent sibling process, streams output, and exits
with the remote command's exit code. Use `sandbox connect` to attach to the
canonical main process.
Use `--env` only for non-secret values. Attach credentials to the sandbox with a
provider instead of passing API keys, tokens, or other secrets to `sandbox exec`.

Expand Down
21 changes: 18 additions & 3 deletions .agents/skills/openshell-cli/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,17 @@ identity provider. Requires an authenticated gateway connection.

### `openshell sandbox create [OPTIONS] [-- COMMAND...]`

Create a sandbox through the selected gateway, wait for readiness, then connect, open an editor, or execute the trailing command.
Create a sandbox through the selected gateway and launch its canonical main

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for a potential stupid question, but why does the cli reference needs to be manifested within a skill ? Wouldn't it be better to always rely on the current code at hand instead of constantly updating this files, which, if forgotten sometimes, might easily rot ? I'm not sure why a second source of truth is required. (i understand the token-saving concern to keep some kind of memory, but I believe the cost of a dealing with a split-brain is higher. No prove though).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pimlock maybe you can answer?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will go away soon, here's related issue: #2736 (cc @johnnygreco)


Some extra context from a thread on this particular concern (not included in the ticket):

Also on the CLI reference, I wonder if there is a pattern of something like:

  • the skill describes the CLI, maybe includes high level workflows
  • it says: "if you need full reference, run openshell agent reference" (or something similar)

This way the reference and the skill is in sync, otherwise you may get a reference that has commands your CLI doesn't support, or vice versa.

@johnnygreco
Yes, I like that! That's actually how the Data Designer skill works. It has a step that says:

Run data-designer agent context.

Agents seem to like it.

process. By default, the CLI attaches to that retained process after the
sandbox becomes ready. A trailing command defines the canonical main process;
without one, the default is `/bin/bash -l` with a PTY.

| Flag | Description |
|------|-------------|
| `--name <NAME>` | Sandbox name (auto-generated if omitted) |
| `--from <SOURCE>` | Community name, Dockerfile path, directory, or image reference (BYOC) |
| `--no-keep` | Delete the sandbox after the initial command or shell exits |
| `--detach` | Start the canonical main process without attaching |
| `--editor vscode|cursor` | Launch a remote editor and keep the sandbox alive |
| `--gpu [COUNT]` | Request the driver's default GPU selection or a specific count |
| `--cpu <QUANTITY>` | CPU limit (for example: `500m`, `1`, `2.5`) |
Expand All @@ -227,7 +231,12 @@ Create a sandbox through the selected gateway, wait for readiness, then connect,
| `--approval-mode manual|auto` | Handle agent-authored policy proposals; default: `manual` |
| `--upload <PATH>[:<DEST>]` | Upload local files to the working directory or an explicit destination (repeatable) |
| `--no-git-ignore` | Disable `.gitignore` filtering for `--upload` |
| `[-- COMMAND...]` | Initial command (defaults to an interactive shell) |
| `[-- COMMAND...]` | Canonical main command (defaults to `/bin/bash -l`) |

`--upload` cannot be combined with a trailing main command because uploads
currently complete after the canonical process starts. Create the default
scratch sandbox, upload files, then use `sandbox exec`, or build the files into
the image.

### `openshell sandbox get [name]`

Expand Down Expand Up @@ -277,7 +286,13 @@ Execute a command through the gRPC exec endpoint, stream its output, and exit wi

### `openshell sandbox connect [name]`

Open an interactive SSH shell. The name defaults to the last-used sandbox. `--editor vscode|cursor` launches a supported remote editor instead.
Attach to the sandbox's retained canonical main process. Disconnecting leaves
the process running. Reconnecting targets the same process instance and
replays recent output. Use `sandbox exec --tty -- /bin/bash -l` when you need a
new shell. The name defaults to the last-used sandbox.

`--editor vscode|cursor` launches a supported remote editor instead of
attaching to the canonical main process.

### `openshell sandbox upload <name> <path> [dest]`

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions architecture/compute-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Each runtime receives a sandbox spec from the gateway and is responsible for:
- Injecting sandbox identity and gateway callback configuration.
- Supplying TLS or secret material for supervisor callbacks.
- Providing the supervisor binary or image in the workload.
- Forwarding the exact canonical main-process argv and TTY mode without shell
reconstruction. The sandbox-level environment and policy workspace apply to
the main process.
- Reporting lifecycle and platform events back to the gateway.
- Cleaning up runtime-owned resources.

Expand All @@ -23,6 +26,10 @@ references to gateway-internal types. The gateway owns the public
`SandboxPhase::Ready` decision. This applies equally to extension drivers
implementing `ComputeDriver` out of tree.

Canonical main-process support is part of the `ComputeDriver` contract. Every
in-tree and extension driver must forward the exact specification; it is not an
optional capability that drivers can omit or negotiate.

Drivers own runtime-specific platform event interpretation. When an event should
drive client provisioning UI, the driver attaches the shared
`openshell.progress.*` metadata defined in `openshell-core` instead of requiring
Expand Down
8 changes: 8 additions & 0 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@ workloads.
- Resolve provider credentials and inference bundles for sandbox supervisors.
- Coordinate supervisor relay sessions for connect, exec, file sync, and
service forwarding.
- Persist the canonical main-process instance ID and normalized exit code on
sandbox status. Any main process exit transitions the sandbox to `Error`,
including exit code zero.

The gateway does not enforce agent network policy at request time. That happens
inside each sandbox, where the supervisor and proxy can observe local process
identity.

The live supervisor session is the readiness authority for its main-process
instance. The supervisor reports its normalized result through the
sandbox-authenticated `ReportMainProcessExit` RPC, and the gateway rejects
results from stale instance IDs.

## Protocol and Auth

The gateway listens on one service port and multiplexes gRPC and HTTP traffic.
Expand Down
17 changes: 12 additions & 5 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ only when the set is already empty; any other outcome fails the spawn.
gateway, depending on mode.
3. It prepares filesystem access, process restrictions, network namespace
routing, trust stores, provider credential resolution, and inference routes.
4. It starts the policy proxy and local SSH server.
5. It opens a supervisor session back to the gateway for connect, exec, file
4. It launches the persisted canonical main-process argv and retains its PTY
or pipes in the main-session multiplexer.
5. It starts the policy proxy and local SSH server.
6. It opens a supervisor session back to the gateway for connect, exec, file
sync, config polling, and log push.
6. It launches the agent command as the resolved restricted identity.

## Isolation Layers

Expand Down Expand Up @@ -326,8 +327,10 @@ The supervisor runs an SSH server on a Unix socket inside the sandbox. The
gateway reaches it through the outbound supervisor relay, not by dialing the
sandbox workload directly. The relay supports:

- Interactive shell sessions.
- Command execution.
- Attachment to the canonical main process through the `openshell-main` SSH
subsystem. The supervisor owns its retained PTY or pipes, a 1 MiB replay
buffer, and a single stdin lease across client disconnects.
- Independent shell and command execution sessions.
- Tar-based file sync.
- Port forwarding where supported by the CLI/TUI surface.

Expand Down Expand Up @@ -403,3 +406,7 @@ engine with a gateway policy revision.
re-evaluate.
- If the supervisor relay drops, the sandbox can keep running, but connect and
exec operations fail until the supervisor registers again.
- If the canonical main process exits, including with code 0, the supervisor
reports its normalized exit code before shutdown. The gateway persists the
code on sandbox status, records `MainProcessExited`, and makes the sandbox
terminal `Error`; runtime restart policies must not replace the process.
62 changes: 61 additions & 1 deletion crates/openshell-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1348,7 +1348,12 @@ enum SandboxCommands {
/// working directory.
/// `.gitignore` rules are applied by default; use `--no-git-ignore` to
/// upload everything.
#[arg(long, value_hint = ValueHint::AnyPath, help_heading = "UPLOAD FLAGS")]
#[arg(
long,
value_hint = ValueHint::AnyPath,
help_heading = "UPLOAD FLAGS",
conflicts_with = "command"
)]
upload: Vec<String>,

/// Disable `.gitignore` filtering for `--upload`.
Expand Down Expand Up @@ -1418,6 +1423,10 @@ enum SandboxCommands {
#[arg(long, overrides_with = "tty")]
no_tty: bool,

/// Start the canonical main process without attaching to it.
#[arg(long, conflicts_with_all = ["editor", "no_keep"])]
detach: bool,

/// Auto-create missing providers from local credentials.
///
/// Without this flag, an interactive prompt asks per-provider;
Expand Down Expand Up @@ -2977,6 +2986,7 @@ async fn run_async() -> Result<()> {
forward,
tty,
no_tty,
detach,
auto_providers,
no_auto_providers,
labels,
Expand Down Expand Up @@ -3072,6 +3082,7 @@ async fn run_async() -> Result<()> {
environment: env_map,
approval_mode: &approval_mode,
output: output.as_str(),
detach,
},
&cli.workspace,
&tls,
Expand Down Expand Up @@ -5125,6 +5136,55 @@ mod tests {
}
}

#[test]
fn sandbox_create_detach_parses_with_main_command() {
let cli = Cli::try_parse_from([
"openshell",
"sandbox",
"create",
"--detach",
"--",
"worker",
"--serve",
])
.expect("sandbox create --detach should parse");

match cli.command {
Some(Commands::Sandbox {
command:
Some(SandboxCommands::Create {
detach, command, ..
}),
..
}) => {
assert!(detach);
assert_eq!(command, ["worker", "--serve"]);
}
other => panic!("expected SandboxCommands::Create, got: {other:?}"),
}
}

#[test]
fn sandbox_create_detach_rejects_ephemeral_sandbox() {
let result =
Cli::try_parse_from(["openshell", "sandbox", "create", "--detach", "--no-keep"]);
assert!(result.is_err());
}

#[test]
fn sandbox_create_rejects_upload_with_main_command() {
let result = Cli::try_parse_from([
"openshell",
"sandbox",
"create",
"--upload",
".",
"--",
"./run-uploaded-app",
]);
assert!(result.is_err());
}

/// `sandbox create` defaults `--approval-mode` to `"manual"`. The CLI
/// always sends an explicit value so the wire form is human-readable
/// (the gateway treats `""` as `"manual"` too, but the CLI's job is to
Expand Down
Loading
Loading