Skip to content
Merged
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
8 changes: 4 additions & 4 deletions .agents/skills/tui-development/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ match app.screen {
}
```

Within the `Sandbox` screen, the top 20% renders sandbox metadata (`sandbox_detail`), and the bottom 80% dispatches based on focus and tab state:
Within the `Sandbox` screen, `sandbox_detail::required_height` sizes the metadata and restart status pane to its contents. The remaining area dispatches based on focus and tab state:

```rust
match app.focus {
Expand Down Expand Up @@ -262,7 +262,7 @@ The `Theme` struct has 16 `Style` fields, accessed at runtime via `app.theme`:
| `border` | EVERGLADE fg | Light sage fg | Unfocused panel borders |
| `border_focused` | NVIDIA_GREEN fg | NVIDIA_GREEN_DARK fg | Focused panel borders |
| `status_ok` | NVIDIA_GREEN fg | NVIDIA_GREEN_DARK fg | Healthy, INFO, Ready |
| `status_warn` | Yellow fg | Dark yellow fg | Degraded, WARN, Provisioning |
| `status_warn` | Yellow fg | Dark yellow fg | Degraded, WARN, Provisioning, Starting |
| `status_err` | Red fg | Dark red fg | Unhealthy, ERROR |
| `key_hint` | NVIDIA_GREEN fg | NVIDIA_GREEN_DARK fg | Keyboard shortcut labels |
| `log_cursor` | EVERGLADE bg | Light green bg | Selected log line highlight |
Expand Down Expand Up @@ -297,7 +297,7 @@ fn draw_detail_popup(frame: &mut Frame<'_>, data: &MyData, area: Rect, theme: &T

- **Selected row**: Green `▌` left-border marker on the selected row. Active gateway also gets a green `●` dot.
- **Focused panel**: Border changes from `border` to `border_focused` style.
- **Status indicators**: Green for healthy/ready/info, yellow for degraded/provisioning/warn, red for unhealthy/error.
- **Status indicators**: Green for healthy/ready/info, yellow for degraded/provisioning/starting/warn, red for unhealthy/error.
- **Separators**: Muted `│` characters between title bar segments and nav bar sections.
- **Log source labels**: `"sandbox"` source renders in `accent` (green), `"gateway"` in `muted`.

Expand Down Expand Up @@ -423,7 +423,7 @@ All actions are accessible via keyboard shortcuts displayed in the nav bar. The
| `crates/openshell-tui/src/ui/providers.rs` | Provider list table with profile-aware columns: Name, Category, Type, Credentials, Workspace |
| `crates/openshell-tui/src/ui/global_settings.rs` | Global settings table: Key, Type, Value. Includes edit overlay, confirm-set, and confirm-delete popups |
| `crates/openshell-tui/src/ui/sandboxes.rs` | Reusable sandbox table widget with columns: Name, Status, Created, Age, Image, Workspace, Notes |
| `crates/openshell-tui/src/ui/sandbox_detail.rs` | Sandbox metadata view — name, status, image, created, age, providers, policy version |
| `crates/openshell-tui/src/ui/sandbox_detail.rs` | Sandbox metadata view — name, status, image, created, age, restart policy/status, providers, policy version |
| `crates/openshell-tui/src/ui/sandbox_policy.rs` | Policy viewer — rendered policy lines with scroll support, tab title |
| `crates/openshell-tui/src/ui/sandbox_settings.rs` | Sandbox settings table: Key, Type, Value, Scope. Includes edit overlay and confirm popups |
| `crates/openshell-tui/src/ui/sandbox_logs.rs` | Structured log viewer — timestamp, source, level, target, message, key=value fields, scroll position, source filter, visual selection mode, clipboard copy |
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.

8 changes: 5 additions & 3 deletions architecture/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,11 @@ Runtime layout:
`GLIBC_2.28`. Image defaults remain UID 0 and working directory `/`; compute
drivers set the runtime identity and writable mounts. Docker stages private
files with the same numeric identity as the supervisor so archive uploads
preserve access regardless of the base image's default user. Health probes execute
the supervisor binary directly. Base updates require refreshing the
multi-architecture digest and rebuilding the image.
preserve access regardless of the base image's default user. Docker and Podman
health checks execute the supervisor binary directly. Kubernetes uses a kubelet
`tcpSocket` readiness probe on supervisor port 5501 instead, so the probe does
not start a process in every sandbox each period. Base updates require
refreshing the multi-architecture digest and rebuilding the image.

Gateway image builds bake the corresponding supervisor image tag into the
gateway binary so Docker sandboxes do not depend on `:latest` by default.
Expand Down
19 changes: 19 additions & 0 deletions architecture/compute-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ 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.

The gateway also owns canonical main-process restart policy. Drivers disable
native container, pod, or VM restart behavior and implement the existing stop
and start operations. When policy selects a restart, the gateway persists the
`Starting` phase and backoff deadline, stops compute, starts it again after the
deadline, and waits for a new supervisor session before returning to `Ready`.
Restart count, exit code, and deadline distinguish this path from an explicit
start. Driver snapshots cannot override that gateway-owned transition.

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 All @@ -90,6 +98,9 @@ produce the public `SandboxPhase`:
backend_phase = derive_phase(driver_status)

public_phase =
if persisted_phase == Stopped: → Stopped
if persisted_phase == Stopping and backend not stopped: → Stopping
if persisted_phase == Starting and backend not Ready: → Starting
if backend_phase in {Error, Deleting}: → pass through (terminal precedence)
if driver_reports_runtime_readiness && backend_phase == Ready: → Ready
if backend_phase == Ready && session connected: → Ready
Expand Down Expand Up @@ -193,6 +204,14 @@ restart a completed or failed canonical process. Before an explicit restart,
the gateway disconnects the prior supervisor session and deletes its SSH
sessions so credentials cannot cross runtime generations.

When the sandbox restart policy selects replacement, the gateway instead
persists `Starting` with the previous exit result, restart count, and backoff
deadline. The leader claims each due attempt before stopping compute, rotates
launch credentials, starts the driver with the persisted generation, and waits
for a different supervisor instance. Stop and delete intent fence late driver
starts. A fresh supervisor clears the deadline and returns the sandbox to
`Ready`; a successful main process exit under `OnFailure` remains `Completed`.

`StopSandbox` and `StartSandbox` are idempotent driver operations. Stop
retains the driver resource and its persistent workspace boundary while making
exec, SSH, forwarding, and exposed services unavailable. Start reactivates the
Expand Down
22 changes: 17 additions & 5 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,13 @@ cross the authenticated vsock channel. A gateway-host proxy is addressed as

The Docker driver runs `openshell-supervisor` in a separate companion container.
Its private named volume contains supervisor bootstrap and channel material.
The workload container receives only `openshell-sandbox`, public interception
CA material, and the other sandbox half of the authenticated channel.
The driver bounded-reads operator proxy credentials and CA bundles into that
supervisor-only volume and passes fixed container paths to the supervisor; it
never exposes gateway-host paths to sandbox-controlled configuration. The
corporate CA extends supervisor upstream trust and is folded into the public
combined trust bundle generated for workload processes. The workload container
receives only `openshell-sandbox`, public interception and combined CA
material, and the other sandbox half of the authenticated channel.

For Kubernetes, the operator configures a Secret name and key rather than a
gateway-host file path. Kubernetes projects that Secret only into the separate
Expand Down Expand Up @@ -919,9 +924,16 @@ the runtime policy it would enforce.
for that finalized supervisor session to disconnect before deleting an
ephemeral sandbox. Exit code 0 records
`Completed/MainProcessCompleted`; nonzero and signal-normalized exits record
`Error/MainProcessFailed`. Infrastructure failures also use `Error`, with a
distinct condition reason and no fabricated canonical-process result. Runtime
restart policies must not replace the canonical process.
`Error/MainProcessFailed` when the gateway policy is `Never`. `OnFailure`
replaces the runtime after a nonzero result; `Always` replaces it after any
result. The gateway starts the first replacement after terminal delivery, then
persists a 1-second doubling backoff for repeated quick exits, capped at 3
minutes. A 10-second run resets the count. A bounded fallback recovers a
restart when terminal finalization is lost. Each replacement starts a new
runtime generation with fresh launch credentials. Infrastructure failures also
use `Error`, with a distinct condition reason and no fabricated canonical-process
result.
Native runtime restart policies remain disabled.

## Shared Boundary Primitives

Expand Down
55 changes: 55 additions & 0 deletions crates/openshell-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,14 @@ enum SandboxCommands {
#[arg(long, conflicts_with = "editor")]
detach: bool,

/// Restart behavior after the canonical main process exits.
#[arg(
long,
value_parser = ["never", "on-failure", "always"],
default_value = "never"
)]
restart_policy: String,

/// Auto-create missing providers from local credentials.
///
/// Without this flag, an interactive prompt asks per-provider;
Expand Down Expand Up @@ -3314,6 +3322,7 @@ async fn run_async() -> Result<()> {
tty,
no_tty,
detach,
restart_policy,
auto_providers,
no_auto_providers,
labels,
Expand Down Expand Up @@ -3416,6 +3425,7 @@ async fn run_async() -> Result<()> {
output: output.as_str(),
detach,
suppress_credential_warnings: no_credential_warnings,
restart_policy: &restart_policy,
},
&cli.workspace,
&tls,
Expand Down Expand Up @@ -6008,6 +6018,51 @@ mod tests {
}
}

#[test]
fn sandbox_create_restart_policy_defaults_to_never() {
let cli = Cli::try_parse_from(["openshell", "sandbox", "create"]).unwrap();
match cli.command {
Some(Commands::Sandbox {
command: Some(SandboxCommands::Create { restart_policy, .. }),
..
}) => assert_eq!(restart_policy, "never"),
other => panic!("expected SandboxCommands::Create, got: {other:?}"),
}
}

#[test]
fn sandbox_create_restart_policy_accepts_on_failure() {
let cli = Cli::try_parse_from([
"openshell",
"sandbox",
"create",
"--restart-policy",
"on-failure",
])
.unwrap();
match cli.command {
Some(Commands::Sandbox {
command: Some(SandboxCommands::Create { restart_policy, .. }),
..
}) => assert_eq!(restart_policy, "on-failure"),
other => panic!("expected SandboxCommands::Create, got: {other:?}"),
}
}

#[test]
fn sandbox_create_restart_policy_rejects_unknown_value() {
assert!(
Cli::try_parse_from([
"openshell",
"sandbox",
"create",
"--restart-policy",
"unless-stopped",
])
.is_err()
);
}

#[test]
fn sandbox_create_detach_parses_with_main_command() {
let cli = Cli::try_parse_from([
Expand Down
Loading
Loading