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
31 changes: 31 additions & 0 deletions crates/openshell-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,31 @@ enum SandboxCommands {
all: bool,
},

/// Stop a sandbox container without deleting it.
///
/// Workspace volume, provider links, and the sandbox record survive.
/// Use `sandbox start` to bring it back live.
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
Stop {
/// Sandbox names.
#[arg(required_unless_present = "all", num_args = 1.., value_name = "NAME", add = ArgValueCompleter::new(completers::complete_sandbox_names))]
names: Vec<String>,

/// Stop all sandboxes.
#[arg(long, conflicts_with = "names")]
all: bool,
},

/// Start a previously-stopped sandbox container.
///
/// Idempotent: succeeds when the sandbox is already running.
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
Start {
/// Sandbox names.
#[arg(required = true, num_args = 1.., value_name = "NAME", add = ArgValueCompleter::new(completers::complete_sandbox_names))]
names: Vec<String>,
},

/// Execute a command in a running sandbox.
///
/// Runs a command inside an existing sandbox using the gRPC exec endpoint.
Expand Down Expand Up @@ -2672,6 +2697,12 @@ async fn main() -> Result<()> {
SandboxCommands::Delete { names, all } => {
run::sandbox_delete(endpoint, &names, all, &tls, &ctx.name).await?;
}
SandboxCommands::Stop { names, all } => {
run::sandbox_stop(endpoint, &names, all, &tls).await?;
}
SandboxCommands::Start { names } => {
run::sandbox_start(endpoint, &names, &tls).await?;
}
SandboxCommands::Connect { name, editor } => {
let name = resolve_sandbox_name(name, &ctx.name)?;
if let Some(editor) = editor.map(Into::into) {
Expand Down
80 changes: 77 additions & 3 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ use openshell_core::proto::{
ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest,
RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, SandboxPhase, SandboxPolicy,
SandboxSpec, SandboxTemplate, ServiceEndpointResponse, SetClusterInferenceRequest,
SettingScope, SettingValue, TcpForwardFrame, TcpForwardInit, TcpRelayTarget,
UpdateConfigRequest, UpdateProviderRequest, WatchSandboxRequest, exec_sandbox_event,
setting_value, tcp_forward_init,
SettingScope, SettingValue, StartSandboxRequest, StopSandboxRequest, TcpForwardFrame,
TcpForwardInit, TcpRelayTarget, UpdateConfigRequest, UpdateProviderRequest,
WatchSandboxRequest, exec_sandbox_event, setting_value, tcp_forward_init,
};
use openshell_core::settings::{self, SettingValueKind};
use openshell_core::{ObjectId, ObjectName};
Expand Down Expand Up @@ -3428,6 +3428,80 @@ pub async fn sandbox_delete(
Ok(())
}

/// Stop a sandbox by name without deleting it. Workspace volume and
/// provider links survive; use `sandbox start` to bring it back live.
pub async fn sandbox_stop(
server: &str,
names: &[String],
all: bool,
tls: &TlsOptions,
) -> Result<()> {
let mut client = grpc_client(server, tls).await?;

let names_to_stop: Vec<String> = if all {
let response = client
.list_sandboxes(ListSandboxesRequest {
limit: 1000,
offset: 0,
label_selector: String::new(),
})
.await
.into_diagnostic()?;
let sandboxes = response.into_inner().sandboxes;
if sandboxes.is_empty() {
println!("No sandboxes to stop.");
return Ok(());
}
sandboxes
.into_iter()
.map(|s| s.object_name().to_string())
.collect()
} else {
names.to_vec()
};

for name in &names_to_stop {
if let Ok(stopped) = stop_forwards_for_sandbox(name) {
for port in stopped {
eprintln!(
"{} Stopped forward of port {port} for sandbox {name}",
"✓".green().bold(),
);
}
}

client
.stop_sandbox(StopSandboxRequest { name: name.clone() })
.await
.into_diagnostic()?;
println!("{} Stopped sandbox {name}", "✓".green().bold());
}

Ok(())
}

/// Start a previously-stopped sandbox by name. Idempotent: succeeds when
/// the sandbox is already running. Fails if the backend resource has
/// been pruned (e.g. by manual container removal).
pub async fn sandbox_start(server: &str, names: &[String], tls: &TlsOptions) -> Result<()> {
let mut client = grpc_client(server, tls).await?;
for name in names {
let response = client
.start_sandbox(StartSandboxRequest { name: name.clone() })
.await
.into_diagnostic()?;
if response.into_inner().started {
println!("{} Started sandbox {name}", "✓".green().bold());
} else {
println!(
"{} Sandbox {name} record exists but backend resource is missing",
"!".yellow(),
);
}
}
Ok(())
}

/// Return the provider type inferred from the trailing command, if any.
fn inferred_provider_type(command: &[String]) -> Option<String> {
detect_provider_from_command(command).map(str::to_string)
Expand Down
17 changes: 16 additions & 1 deletion crates/openshell-cli/tests/ensure_providers_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ use openshell_core::proto::{
ListProvidersRequest, ListProvidersResponse, ListSandboxProvidersRequest,
ListSandboxProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, Provider,
ProviderResponse, RevokeSshSessionRequest, RevokeSshSessionResponse, SandboxResponse,
SandboxStreamEvent, ServiceStatus, SupervisorMessage, UpdateProviderRequest,
SandboxStreamEvent, ServiceStatus, StartSandboxRequest, StartSandboxResponse,
StopSandboxRequest, StopSandboxResponse, SupervisorMessage, UpdateProviderRequest,
WatchSandboxRequest,
};
use openshell_core::{ObjectId, ObjectName};
Expand Down Expand Up @@ -134,6 +135,20 @@ impl OpenShell for TestOpenShell {
Ok(Response::new(DeleteSandboxResponse { deleted: true }))
}

async fn stop_sandbox(
&self,
_request: tonic::Request<StopSandboxRequest>,
) -> Result<Response<StopSandboxResponse>, Status> {
Ok(Response::new(StopSandboxResponse {}))
}

async fn start_sandbox(
&self,
_request: tonic::Request<StartSandboxRequest>,
) -> Result<Response<StartSandboxResponse>, Status> {
Ok(Response::new(StartSandboxResponse { started: true }))
}

async fn get_sandbox_config(
&self,
_request: tonic::Request<GetSandboxConfigRequest>,
Expand Down
16 changes: 16 additions & 0 deletions crates/openshell-cli/tests/mtls_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ impl OpenShell for TestOpenShell {
))
}

async fn stop_sandbox(
&self,
_request: tonic::Request<openshell_core::proto::StopSandboxRequest>,
) -> Result<Response<openshell_core::proto::StopSandboxResponse>, Status> {
Ok(Response::new(openshell_core::proto::StopSandboxResponse {}))
}

async fn start_sandbox(
&self,
_request: tonic::Request<openshell_core::proto::StartSandboxRequest>,
) -> Result<Response<openshell_core::proto::StartSandboxResponse>, Status> {
Ok(Response::new(openshell_core::proto::StartSandboxResponse {
started: true,
}))
}

async fn get_sandbox_config(
&self,
_request: tonic::Request<openshell_core::proto::GetSandboxConfigRequest>,
Expand Down
17 changes: 16 additions & 1 deletion crates/openshell-cli/tests/provider_commands_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ use openshell_core::proto::{
ProviderCredentialRefreshStrategy, ProviderProfile, ProviderProfileCredential,
ProviderResponse, RevokeSshSessionRequest, RevokeSshSessionResponse,
RotateProviderCredentialRequest, RotateProviderCredentialResponse, Sandbox, SandboxResponse,
SandboxStreamEvent, ServiceStatus, SupervisorMessage, UpdateProviderRequest,
SandboxStreamEvent, ServiceStatus, StartSandboxRequest, StartSandboxResponse,
StopSandboxRequest, StopSandboxResponse, SupervisorMessage, UpdateProviderRequest,
WatchSandboxRequest,
};
use openshell_core::{ObjectId, ObjectName};
Expand Down Expand Up @@ -259,6 +260,20 @@ impl OpenShell for TestOpenShell {
Ok(Response::new(DeleteSandboxResponse { deleted: true }))
}

async fn stop_sandbox(
&self,
_request: tonic::Request<StopSandboxRequest>,
) -> Result<Response<StopSandboxResponse>, Status> {
Ok(Response::new(StopSandboxResponse {}))
}

async fn start_sandbox(
&self,
_request: tonic::Request<StartSandboxRequest>,
) -> Result<Response<StartSandboxResponse>, Status> {
Ok(Response::new(StartSandboxResponse { started: true }))
}

async fn get_sandbox_config(
&self,
_request: tonic::Request<GetSandboxConfigRequest>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ use openshell_core::proto::{
ListSandboxProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, PlatformEvent,
ProviderResponse, RevokeSshSessionRequest, RevokeSshSessionResponse, Sandbox, SandboxCondition,
SandboxLogLine, SandboxPhase, SandboxResponse, SandboxStatus, SandboxStreamEvent,
ServiceStatus, SupervisorMessage, UpdateProviderRequest, WatchSandboxRequest,
ServiceStatus, StartSandboxRequest, StartSandboxResponse, StopSandboxRequest,
StopSandboxResponse, SupervisorMessage, UpdateProviderRequest, WatchSandboxRequest,
sandbox_stream_event,
};
use std::collections::HashMap;
Expand Down Expand Up @@ -154,6 +155,20 @@ impl OpenShell for TestOpenShell {
Ok(Response::new(DeleteSandboxResponse { deleted: true }))
}

async fn stop_sandbox(
&self,
_request: tonic::Request<StopSandboxRequest>,
) -> Result<Response<StopSandboxResponse>, Status> {
Ok(Response::new(StopSandboxResponse {}))
}

async fn start_sandbox(
&self,
_request: tonic::Request<StartSandboxRequest>,
) -> Result<Response<StartSandboxResponse>, Status> {
Ok(Response::new(StartSandboxResponse { started: true }))
}

async fn get_sandbox_config(
&self,
_request: tonic::Request<GetSandboxConfigRequest>,
Expand Down
19 changes: 17 additions & 2 deletions crates/openshell-cli/tests/sandbox_name_fallback_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ use openshell_core::proto::{
GetSandboxProviderEnvironmentResponse, GetSandboxRequest, HealthRequest, HealthResponse,
ListProvidersRequest, ListProvidersResponse, ListSandboxProvidersRequest,
ListSandboxProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, ProviderResponse,
Sandbox, SandboxPolicy, SandboxResponse, SandboxStreamEvent, ServiceStatus, SupervisorMessage,
UpdateProviderRequest, WatchSandboxRequest,
Sandbox, SandboxPolicy, SandboxResponse, SandboxStreamEvent, ServiceStatus,
StartSandboxRequest, StartSandboxResponse, StopSandboxRequest, StopSandboxResponse,
SupervisorMessage, UpdateProviderRequest, WatchSandboxRequest,
};
use std::sync::Arc;
use tempfile::TempDir;
Expand Down Expand Up @@ -119,6 +120,20 @@ impl OpenShell for TestOpenShell {
Ok(Response::new(DeleteSandboxResponse { deleted: true }))
}

async fn stop_sandbox(
&self,
_request: tonic::Request<StopSandboxRequest>,
) -> Result<Response<StopSandboxResponse>, Status> {
Ok(Response::new(StopSandboxResponse {}))
}

async fn start_sandbox(
&self,
_request: tonic::Request<StartSandboxRequest>,
) -> Result<Response<StartSandboxResponse>, Status> {
Ok(Response::new(StartSandboxResponse { started: true }))
}

async fn get_sandbox_config(
&self,
request: tonic::Request<GetSandboxConfigRequest>,
Expand Down
Loading
Loading