diff --git a/.agents/skills/openshell-cli/SKILL.md b/.agents/skills/openshell-cli/SKILL.md index dd1bd1051b..eb5c467cb6 100644 --- a/.agents/skills/openshell-cli/SKILL.md +++ b/.agents/skills/openshell-cli/SKILL.md @@ -74,6 +74,12 @@ 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. +An explicit trailing command is foreground even when stdin or stdout is not a +terminal. The CLI streams its stdout and stderr and returns its exact exit +status. Exit code 0 leaves a retained sandbox in `Completed`; nonzero leaves it +in `Error` with `MainProcessFailed`. Use `--no-keep` to delete either result +after output drains, or `--detach` for a long-running service. + 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. **Shortcut for known tools**: When the trailing command is a recognized tool, the CLI auto-creates the required provider from local credentials: @@ -248,11 +254,16 @@ Key flags: - `--approval-mode manual|auto`: Control handling of agent-authored policy proposals; `manual` is the default - `--upload [:]`: 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 +- `--no-keep`: Delete the sandbox after main output and the exit result drain - `--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 +`--detach` adds no attachment grace period. When the canonical process exits, +its terminal phase is reported immediately. A foreground create declares one +expected main-process SSH attachment; cleanup finalizes after that connection +closes naturally. + 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. @@ -363,7 +374,10 @@ openshell sandbox start [name] Both commands default to the last-used sandbox. Stop stops background forwards and waits for `Stopped`; start waits for `Ready`. Connect, exec, file transfer, forwarding, and exposed services are unavailable while -stopped. Delete remains the operation that removes retained state. +stopped or completed. Starting a retained `Completed` or +`Error/MainProcessFailed` sandbox launches a fresh canonical-main instance and +invalidates SSH sessions from the previous runtime generation. Delete remains +the operation that removes retained state. --- diff --git a/.agents/skills/openshell-cli/cli-reference.md b/.agents/skills/openshell-cli/cli-reference.md index 856fe32d07..4d39143d8d 100644 --- a/.agents/skills/openshell-cli/cli-reference.md +++ b/.agents/skills/openshell-cli/cli-reference.md @@ -207,18 +207,28 @@ identity provider. Requires an authenticated gateway connection. Create a sandbox through the selected gateway and launch its canonical main 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. +without one, the default is `/bin/bash -l` with a PTY. Explicit commands remain +foreground in non-interactive automation: stdout and stderr stream to the +caller and the CLI returns the command's exact status. Exit 0 leaves +`Completed`; nonzero leaves `Error/MainProcessFailed`. +Starting either retained terminal result invalidates SSH sessions from the +previous runtime generation. | Flag | Description | |------|-------------| | `--name ` | Sandbox name (auto-generated if omitted) | | `--from ` | Community name, Dockerfile path, directory, or image reference (BYOC) | -| `--no-keep` | Delete the sandbox after the initial command or shell exits | +| `--no-keep` | Delete the sandbox after main output and the result drain | | `--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 ` | CPU limit (for example: `500m`, `1`, `2.5`) | | `--memory ` | Memory limit (for example: `512Mi`, `4Gi`, `8G`) | + +`--detach` adds no attachment grace period: the sandbox reports the canonical +process result immediately when it exits. Foreground creation declares one +expected main-process SSH attachment; cleanup finalizes after that connection +drains and closes naturally. | `--driver-config-json ` | Experimental driver-keyed configuration object | | `--provider ` | Provider to attach (repeatable) | | `--policy ` | Custom policy YAML; overrides the built-in default and `OPENSHELL_SANDBOX_POLICY` | @@ -269,8 +279,9 @@ and waits for the `Stopped` phase. ### `openshell sandbox start [name]` -Start a stopped sandbox and wait for `Ready`. The name defaults to the -last-used sandbox. +Start a stopped, failed, or completed sandbox and wait for `Ready`. This +launches a fresh canonical-main instance. The name defaults to the last-used +sandbox. ### `openshell sandbox exec [OPTIONS] -- COMMAND...` diff --git a/architecture/compute-runtimes.md b/architecture/compute-runtimes.md index 2a36073486..3821d48afe 100644 --- a/architecture/compute-runtimes.md +++ b/architecture/compute-runtimes.md @@ -137,6 +137,14 @@ The gateway persists lifecycle intent before mutating compute: Ready -> Stopping -> Stopped -> Starting -> Ready ``` +A canonical main process that exits successfully follows `Ready -> Completed`. +A nonzero or signal-normalized result follows `Ready -> Error` with a +`MainProcessFailed` condition. Both retained results may be started explicitly, +which creates a fresh main-process instance. Drivers must not automatically +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. + `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 diff --git a/architecture/gateway.md b/architecture/gateway.md index dcb1dd5414..594a940d7d 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -16,8 +16,10 @@ workloads. - 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. + sandbox status. Exit code zero transitions the sandbox to `Completed`; + nonzero results transition it to `Error/MainProcessFailed`. Infrastructure + failures also use `Error`, with a distinct reason and no fabricated command + result. 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 @@ -26,7 +28,15 @@ 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. +results from stale instance IDs. Foreground creation carries a one-shot +attachment intent to the process supervisor. The supervisor durably reports the +result immediately, accepts that declared SSH attachment even when the process +has already exited, sends the retained output and exit status, and waits for the +peer's channel close before finalizing the result for ephemeral cleanup. +Detached commands carry no attachment intent, so they finalize and exit +immediately without a grace period. Finalization is persisted separately from +the exit result; the gateway deletes an ephemeral sandbox only after the +finalized supervisor session disconnects. ## Protocol and Auth diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 786ed5194d..b1209167c4 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -481,7 +481,15 @@ 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. +- If the canonical main process exits, the supervisor durably reports the + normalized result immediately. A foreground create declares a one-shot main + attachment, so the supervisor accepts it even after a fast process exits, + sends the retained output and SSH exit status, waits for the peer's channel + close, and then finalizes ephemeral cleanup. With no declared or active + attachment, it finalizes and exits without a grace period. The gateway waits + 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. diff --git a/crates/openshell-cli/src/commands/common.rs b/crates/openshell-cli/src/commands/common.rs index 75dc260ba7..2af950cd01 100644 --- a/crates/openshell-cli/src/commands/common.rs +++ b/crates/openshell-cli/src/commands/common.rs @@ -65,6 +65,7 @@ pub fn phase_name(phase: i32) -> &'static str { Ok(SandboxPhase::Stopping) => "Stopping", Ok(SandboxPhase::Stopped) => "Stopped", Ok(SandboxPhase::Starting) => "Starting", + Ok(SandboxPhase::Completed) => "Completed", Ok(SandboxPhase::Unknown) | Err(_) => "Unknown", } } diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index aaabf26625..d17e830969 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -3071,7 +3071,7 @@ async fn run_async() -> Result<()> { let endpoint = &ctx.endpoint; let mut tls = tls.with_gateway_name(&ctx.name); apply_auth(&mut tls, &ctx.name); - Box::pin(run::sandbox_create( + let exit_code = Box::pin(run::sandbox_create( endpoint, &ctx.name, run::SandboxCreateConfig { @@ -3100,6 +3100,9 @@ async fn run_async() -> Result<()> { &tls, )) .await?; + if exit_code != 0 { + std::process::exit(exit_code); + } } SandboxCommands::Upload { name, @@ -3225,7 +3228,12 @@ async fn run_async() -> Result<()> { ) .await?; } else { - run::sandbox_connect(endpoint, &name, &tls, &cli.workspace).await?; + let exit_code = + run::sandbox_connect(endpoint, &name, &tls, &cli.workspace) + .await?; + if exit_code != 0 { + std::process::exit(exit_code); + } } let _ = save_last_sandbox(&ctx.name, &cli.workspace, &name); } diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 0a0b21a7f4..c894f9c9ed 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -224,6 +224,23 @@ fn sandbox_should_persist(keep: bool, forward: Option<&ForwardSpec>) -> bool { keep || forward.is_some() } +fn has_main_process_result(sandbox: &Sandbox) -> bool { + let Some(status) = sandbox.status.as_ref() else { + return false; + }; + if status.exit_code.is_none() { + return false; + } + + let phase = SandboxPhase::try_from(sandbox.phase()).unwrap_or(SandboxPhase::Unknown); + phase != SandboxPhase::Error + || status.conditions.iter().any(|condition| { + condition.r#type == "Ready" + && condition.status.eq_ignore_ascii_case("false") + && condition.reason == "MainProcessFailed" + }) +} + fn build_sandbox_resource_limits( cpu: Option<&str>, memory: Option<&str>, @@ -339,19 +356,21 @@ async fn finalize_sandbox_create_session( server: &str, sandbox_name: &str, persist: bool, - session_result: Result<()>, + session_result: Result, workspace: &str, tls: &TlsOptions, gateway: &str, -) -> Result<()> { +) -> Result { if persist { return session_result; } let names = [sandbox_name.to_string()]; if let Err(err) = sandbox_delete(server, &names, false, workspace, tls, gateway).await { - if session_result.is_ok() { - return Err(err); + if let Ok(exit_code) = session_result.as_ref() { + return Err(miette::miette!( + "sandbox command exited with status {exit_code}, but ephemeral cleanup failed: {err}" + )); } eprintln!("Failed to delete sandbox {sandbox_name}: {err}"); } @@ -422,7 +441,7 @@ pub async fn sandbox_create( config: SandboxCreateConfig<'_>, workspace: &str, tls: &TlsOptions, -) -> Result<()> { +) -> Result { let SandboxCreateConfig { name, from, @@ -456,6 +475,11 @@ pub async fn sandbox_create( "--upload cannot be combined with a trailing main command yet because uploads complete after the canonical process starts" )); } + if output != "table" && !command.is_empty() && !detach { + return Err(miette::miette!( + "structured output cannot be combined with an attached trailing command; use table output to stream the command or add --detach" + )); + } // Check port availability *before* creating the sandbox so we don't // leave an orphaned sandbox behind when the forward would fail. @@ -538,6 +562,20 @@ pub async fn sandbox_create( } else { command.to_vec() }; + let persist = sandbox_should_persist(keep, forward.as_ref()); + let create_detaches = detach + || (persist + && command.is_empty() + && (!std::io::stdin().is_terminal() || !std::io::stdout().is_terminal())); + let await_main_process_attachment = output == "table" && editor.is_none() && !create_detaches; + let annotations = if persist { + HashMap::new() + } else { + HashMap::from([( + "openshell.nvidia.com/retention".to_string(), + "ephemeral".to_string(), + )]) + }; let request = CreateSandboxRequest { spec: Some(SandboxSpec { resource_requirements, @@ -551,8 +589,9 @@ pub async fn sandbox_create( }), name: name.unwrap_or_default().to_string(), labels, - annotations: HashMap::new(), + annotations, workspace: workspace.to_string(), + await_main_process_attachment, }; let response = match client.create_sandbox(request).await { @@ -571,7 +610,6 @@ pub async fn sandbox_create( .ok_or_else(|| miette::miette!("sandbox missing from response"))?; let interactive = std::io::stdout().is_terminal(); - let persist = sandbox_should_persist(keep, forward.as_ref()); let sandbox_name = if sandbox.object_name().is_empty() { "unknown".to_string() } else { @@ -748,8 +786,20 @@ pub async fn sandbox_create( saw_non_ready = true; } - // Capture error reason from conditions only when phase is Error - // to avoid showing stale transient error reasons + let main_process_result = has_main_process_result(&s); + if matches!( + phase, + SandboxPhase::Completed | SandboxPhase::Error | SandboxPhase::Stopped + ) && main_process_result + { + if let Some(d) = display.as_interactive_mut() { + d.clear(); + } + break; + } + + // Capture infrastructure error reasons only after excluding a + // canonical-command result, which must attach and drain output. if phase == SandboxPhase::Error && let Some(status) = &s.status { @@ -828,7 +878,11 @@ pub async fn sandbox_create( // If we exited the loop without hitting the Ready break, finish the display. let final_phase = SandboxPhase::try_from(last_phase).unwrap_or(SandboxPhase::Unknown); - if final_phase != SandboxPhase::Ready + let final_has_main_process_result = has_main_process_result(&last_sandbox); + if !(matches!( + final_phase, + SandboxPhase::Ready | SandboxPhase::Completed | SandboxPhase::Stopped + ) || final_phase == SandboxPhase::Error && final_has_main_process_result) && let Some(d) = display.as_interactive_mut() { if final_phase == SandboxPhase::Error { @@ -941,7 +995,7 @@ pub async fn sandbox_create( if structured_output { crate::output::print_output_single(output, &last_sandbox, sandbox_to_json)?; - return Ok(()); + return Ok(0); } if let Some(editor) = editor { @@ -955,18 +1009,18 @@ pub async fn sandbox_create( workspace, ) .await?; - return Ok(()); + return Ok(0); } - // Persistent non-interactive creates detach implicitly. An - // explicitly ephemeral (`--no-keep`) create must still attach so - // it can observe the canonical process and delete the sandbox when - // that session ends. + // An explicit trailing command is foreground regardless of TTY + // detection. Only --detach opts out. Scratch shells retain the + // non-interactive implicit-detach behavior. if detach || (persist + && command.is_empty() && (!std::io::stdin().is_terminal() || !std::io::stdout().is_terminal())) { - return Ok(()); + return Ok(0); } let connect_result = if persist { @@ -992,6 +1046,32 @@ pub async fn sandbox_create( ) .await } + SandboxPhase::Completed | SandboxPhase::Stopped | SandboxPhase::Error + if final_has_main_process_result => + { + drop(stream); + drop(client); + if detach { + return Ok(0); + } + let connect_result = crate::ssh::sandbox_connect_terminal_main( + &effective_server, + &sandbox_name, + &effective_tls, + workspace, + ) + .await; + finalize_sandbox_create_session( + &effective_server, + &sandbox_name, + persist, + connect_result, + workspace, + &effective_tls, + gateway_name, + ) + .await + } SandboxPhase::Error => { drop(stream); drop(client); @@ -1326,6 +1406,9 @@ pub async fn sandbox_get( println!(" {} {}", "Id:".dimmed(), id); println!(" {} {}", "Name:".dimmed(), name); println!(" {} {}", "Phase:".dimmed(), phase_name(sandbox.phase())); + if let Some(exit_code) = sandbox.status.as_ref().and_then(|status| status.exit_code) { + println!(" {} {}", "Exit Code:".dimmed(), exit_code); + } println!( " {} {}", "Resource version:".dimmed(), @@ -2059,8 +2142,16 @@ pub async fn sandbox_list( for sandbox in sandboxes { let phase = phase_name(sandbox.phase()); let phase_colored = match SandboxPhase::try_from(sandbox.phase()) { - Ok(SandboxPhase::Ready) => phase.green().to_string(), + Ok(SandboxPhase::Ready | SandboxPhase::Completed) => phase.green().to_string(), Ok(SandboxPhase::Error) => phase.red().to_string(), + Ok(SandboxPhase::Stopped) + if sandbox + .status + .as_ref() + .is_some_and(|status| status.exit_code.is_some()) => + { + phase.red().to_string() + } Ok(SandboxPhase::Provisioning) => phase.yellow().to_string(), Ok(SandboxPhase::Deleting) => phase.dimmed().to_string(), _ => phase.to_string(), @@ -2104,6 +2195,7 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value { "created_at": format_epoch_ms(meta.map_or(0, |m| m.created_at_ms)), "phase": phase_name(sandbox.phase()), "current_policy_version": sandbox.current_policy_version(), + "exit_code": sandbox.status.as_ref().and_then(|status| status.exit_code), }) } @@ -2393,13 +2485,21 @@ pub async fn sandbox_delete( } } - let response = client + let response = match client .delete_sandbox(DeleteSandboxRequest { name: name.clone(), workspace: workspace.to_string(), }) .await - .into_diagnostic()?; + { + Ok(response) => response, + Err(status) if status.code() == Code::NotFound => { + clear_last_sandbox_if_matches(gateway, workspace, name); + println!("{} Sandbox {name} already deleted", "✓".green().bold()); + continue; + } + Err(status) => return Err(status).into_diagnostic(), + }; let deleted = response.into_inner().deleted; if deleted { @@ -2478,9 +2578,9 @@ async fn wait_for_lifecycle_phase( return Ok(sandbox); } if current == SandboxPhase::Error { - return Err(miette!( - "sandbox entered Error while waiting for {target:?}" - )); + let detail = ready_false_condition_message(sandbox.status.as_ref()) + .unwrap_or_else(|| "sandbox entered Error".to_string()); + return Err(miette!("{detail} while waiting for {target:?}")); } let timeout = Duration::from_secs( @@ -7406,13 +7506,14 @@ mod tests { use super::{ PolicyGetView, ProvisioningStep, build_sandbox_resource_limits, dockerfile_sources_supported_for_gateway, format_endpoint, - format_provider_attachment_table, git_sync_files, inferred_provider_type, - parse_cli_setting_value, parse_credential_expiry_cli_value, parse_credential_expiry_pairs, - parse_credential_pairs, parse_driver_config_json, parse_secret_material_env_pairs, - policy_revision_to_json, provider_profile_allows_empty_credentials, - provisioning_timeout_message, ready_false_condition_message, refresh_status_header, - refresh_status_row, resolve_from, sandbox_should_persist, sandbox_upload_plan, - service_expose_status_error, service_url_for_gateway, + format_provider_attachment_table, git_sync_files, has_main_process_result, + inferred_provider_type, parse_cli_setting_value, parse_credential_expiry_cli_value, + parse_credential_expiry_pairs, parse_credential_pairs, parse_driver_config_json, + parse_secret_material_env_pairs, policy_revision_to_json, + provider_profile_allows_empty_credentials, provisioning_timeout_message, + ready_false_condition_message, refresh_status_header, refresh_status_row, resolve_from, + sandbox_should_persist, sandbox_upload_plan, service_expose_status_error, + service_url_for_gateway, }; use crate::TEST_ENV_LOCK; use crate::commands::common::progress_step_from_metadata; @@ -7961,6 +8062,48 @@ mod tests { assert!(sandbox_should_persist(false, Some(&spec))); } + #[test] + fn infrastructure_error_with_observed_exit_is_not_a_main_process_result() { + let mut sandbox = Sandbox { + status: Some(SandboxStatus { + exit_code: Some(137), + conditions: vec![SandboxCondition { + r#type: "Ready".to_string(), + status: "False".to_string(), + reason: "ComputeResourceMissing".to_string(), + message: "sandbox runtime disappeared".to_string(), + ..Default::default() + }], + ..Default::default() + }), + ..Default::default() + }; + sandbox.set_phase(SandboxPhase::Error as i32); + + assert!(!has_main_process_result(&sandbox)); + } + + #[test] + fn main_process_failed_condition_identifies_command_result() { + let mut sandbox = Sandbox { + status: Some(SandboxStatus { + exit_code: Some(7), + conditions: vec![SandboxCondition { + r#type: "Ready".to_string(), + status: "False".to_string(), + reason: "MainProcessFailed".to_string(), + message: "canonical main process exited with status 7".to_string(), + ..Default::default() + }], + ..Default::default() + }), + ..Default::default() + }; + sandbox.set_phase(SandboxPhase::Error as i32); + + assert!(has_main_process_result(&sandbox)); + } + #[test] fn resolve_from_classifies_existing_dockerfile_path() { let temp = tempfile::tempdir().expect("failed to create tempdir"); diff --git a/crates/openshell-cli/src/ssh.rs b/crates/openshell-cli/src/ssh.rs index 4768dc27f2..1e1e48257a 100644 --- a/crates/openshell-cli/src/ssh.rs +++ b/crates/openshell-cli/src/ssh.rs @@ -29,6 +29,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::process::{Child, Command as TokioCommand}; use tokio_stream::wrappers::ReceiverStream; +use tonic::Code; /// Time budget for the local listener to become reachable after `ssh` starts. /// This is a user-visible readiness deadline for both foreground and background @@ -39,6 +40,10 @@ const FORWARD_LISTENER_PROBE_INTERVAL: Duration = Duration::from_millis(50); /// Per-attempt connect timeout, so one hung probe cannot consume the whole /// grace period. const FORWARD_LISTENER_CONNECT_TIMEOUT: Duration = Duration::from_millis(200); +/// Time budget for the supervisor relay to register after a fast canonical +/// command has already reported its terminal result. +const TERMINAL_RELAY_REGISTRATION_TIMEOUT: Duration = Duration::from_secs(5); +const TERMINAL_RELAY_REGISTRATION_INTERVAL: Duration = Duration::from_millis(50); #[derive(Clone, Copy, Debug)] pub enum Editor { @@ -79,6 +84,7 @@ async fn ssh_session_config( name: &str, tls: &TlsOptions, workspace: &str, + terminal_relay_registration_timeout: Option, ) -> Result { let mut client = grpc_client(server, tls).await?; @@ -94,12 +100,26 @@ async fn ssh_session_config( .sandbox .ok_or_else(|| miette::miette!("sandbox not found"))?; - let response = client - .create_ssh_session(CreateSshSessionRequest { - sandbox_id: sandbox.object_id().to_string(), - }) - .await - .into_diagnostic()?; + let relay_registration_deadline = + terminal_relay_registration_timeout.map(|timeout| tokio::time::Instant::now() + timeout); + let response = loop { + match client + .create_ssh_session(CreateSshSessionRequest { + sandbox_id: sandbox.object_id().to_string(), + }) + .await + { + Ok(response) => break response, + Err(status) + if status.code() == Code::FailedPrecondition + && relay_registration_deadline + .is_some_and(|deadline| tokio::time::Instant::now() < deadline) => + { + tokio::time::sleep(TERMINAL_RELAY_REGISTRATION_INTERVAL).await; + } + Err(status) => return Err(status).into_diagnostic(), + } + }; let session = response.into_inner(); validate_ssh_session_response(&session) .map_err(|err| miette::miette!("gateway returned invalid SSH session response: {err}"))?; @@ -229,7 +249,7 @@ fn reset_transient_tty_signals(command: &mut Command) { } } -fn exec_or_wait(mut command: Command, replace_process: bool) -> Result<()> { +fn exec_or_wait(mut command: Command, replace_process: bool) -> Result { if replace_process && std::io::stdin().is_terminal() { #[cfg(unix)] { @@ -248,11 +268,7 @@ fn exec_or_wait(mut command: Command, replace_process: bool) -> Result<()> { let status = command.status().into_diagnostic()?; - if !status.success() { - return Err(miette::miette!("ssh exited with status {status}")); - } - - Ok(()) + Ok(status.code().unwrap_or(1)) } async fn sandbox_connect_with_mode( @@ -261,8 +277,16 @@ async fn sandbox_connect_with_mode( tls: &TlsOptions, replace_process: bool, workspace: &str, -) -> Result<()> { - let session = ssh_session_config(server, name, tls, workspace).await?; + terminal_relay_registration_timeout: Option, +) -> Result { + let session = ssh_session_config( + server, + name, + tls, + workspace, + terminal_relay_registration_timeout, + ) + .await?; let mut command = ssh_base_command(&session.proxy_command); if session.main_terminal { @@ -280,11 +304,11 @@ async fn sandbox_connect_with_mode( .stdout(Stdio::inherit()) .stderr(Stdio::inherit()); - tokio::task::spawn_blocking(move || exec_or_wait(command, replace_process)) + let exit_code = tokio::task::spawn_blocking(move || exec_or_wait(command, replace_process)) .await .into_diagnostic()??; - Ok(()) + Ok(exit_code) } /// Connect to a sandbox via SSH. @@ -293,8 +317,8 @@ pub async fn sandbox_connect( name: &str, tls: &TlsOptions, workspace: &str, -) -> Result<()> { - sandbox_connect_with_mode(server, name, tls, true, workspace).await +) -> Result { + sandbox_connect_with_mode(server, name, tls, true, workspace, None).await } pub(crate) async fn sandbox_connect_without_exec( @@ -302,8 +326,25 @@ pub(crate) async fn sandbox_connect_without_exec( name: &str, tls: &TlsOptions, workspace: &str, -) -> Result<()> { - sandbox_connect_with_mode(server, name, tls, false, workspace).await +) -> Result { + sandbox_connect_with_mode(server, name, tls, false, workspace, None).await +} + +pub(crate) async fn sandbox_connect_terminal_main( + server: &str, + name: &str, + tls: &TlsOptions, + workspace: &str, +) -> Result { + sandbox_connect_with_mode( + server, + name, + tls, + false, + workspace, + Some(TERMINAL_RELAY_REGISTRATION_TIMEOUT), + ) + .await } pub async fn sandbox_connect_editor( @@ -314,7 +355,7 @@ pub async fn sandbox_connect_editor( tls: &TlsOptions, workspace: &str, ) -> Result<()> { - let session = ssh_session_config(server, name, tls, workspace).await?; + let session = ssh_session_config(server, name, tls, workspace, None).await?; let workspace_root = discover_workspace_root(&session).await?; let host_alias = host_alias(name, workspace); @@ -343,7 +384,7 @@ pub async fn sandbox_forward( ) -> Result<()> { openshell_core::forward::check_port_available(spec)?; - let session = ssh_session_config(server, name, tls, workspace).await?; + let session = ssh_session_config(server, name, tls, workspace, None).await?; let mut command = TokioCommand::from(ssh_base_command(&session.proxy_command)); command @@ -558,7 +599,7 @@ async fn sandbox_exec_with_mode( return Err(miette::miette!("no command provided")); } - let session = ssh_session_config(server, name, tls, workspace).await?; + let session = ssh_session_config(server, name, tls, workspace, None).await?; let mut ssh = ssh_base_command(&session.proxy_command); if tty { @@ -771,7 +812,7 @@ async fn ssh_tar_upload( tls: &TlsOptions, workspace: &str, ) -> Result<()> { - let session = ssh_session_config(server, name, tls, workspace).await?; + let session = ssh_session_config(server, name, tls, workspace, None).await?; let dest_dir = dest_dir.unwrap_or("."); let escaped_dest = shell_escape(dest_dir); @@ -1204,7 +1245,7 @@ pub async fn sandbox_sync_down( tls: &TlsOptions, workspace: &str, ) -> Result<()> { - let session = ssh_session_config(server, name, tls, workspace).await?; + let session = ssh_session_config(server, name, tls, workspace, None).await?; let sandbox_path = resolve_sandbox_source_path(&session, sandbox_path).await?; let kind = probe_sandbox_source_kind(&session, &sandbox_path).await?; @@ -1486,7 +1527,7 @@ pub async fn sandbox_ssh_proxy_by_name( tls: &TlsOptions, workspace: &str, ) -> Result<()> { - let session = ssh_session_config(server, name, tls, workspace).await?; + let session = ssh_session_config(server, name, tls, workspace, None).await?; sandbox_ssh_proxy( &session.gateway_url, &session.sandbox_id, diff --git a/crates/openshell-cli/tests/ensure_providers_integration.rs b/crates/openshell-cli/tests/ensure_providers_integration.rs index 8192989375..9999a7c083 100644 --- a/crates/openshell-cli/tests/ensure_providers_integration.rs +++ b/crates/openshell-cli/tests/ensure_providers_integration.rs @@ -88,6 +88,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not used by this test server")) } + async fn finalize_main_process_exit( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this test server")) + } + async fn get_current_user( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/mtls_integration.rs b/crates/openshell-cli/tests/mtls_integration.rs index 91d520d1af..e4735ac4c1 100644 --- a/crates/openshell-cli/tests/mtls_integration.rs +++ b/crates/openshell-cli/tests/mtls_integration.rs @@ -41,6 +41,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not used by this test server")) } + async fn finalize_main_process_exit( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this test server")) + } + async fn get_current_user( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index 2c71e0b39f..73e646b57b 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -106,6 +106,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not used by this test server")) } + async fn finalize_main_process_exit( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this test server")) + } + async fn get_current_user( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index bcc07619ee..118daff902 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -48,8 +48,12 @@ struct SandboxState { deleted_names: Arc>>>, create_requests: Arc>>, vm_error_after_started: Arc, + vm_error_with_observed_exit: Arc, vm_slow_progress_before_ready: Arc, vm_log_churn_before_ready: Arc, + terminal_before_relay: Arc, + ssh_session_failures_remaining: Arc, + ssh_session_requests: Arc, global_settings: Arc>>, gateway_config_requests: Arc, } @@ -68,6 +72,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not used by this test server")) } + async fn finalize_main_process_exit( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this test server")) + } + async fn get_current_user( &self, _request: tonic::Request, @@ -236,6 +247,19 @@ impl OpenShell for TestOpenShell { &self, request: tonic::Request, ) -> Result, Status> { + self.state + .ssh_session_requests + .fetch_add(1, Ordering::SeqCst); + if self + .state + .ssh_session_failures_remaining + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |remaining| { + remaining.checked_sub(1) + }) + .is_ok() + { + return Err(Status::failed_precondition("sandbox is not ready")); + } let sandbox_id = request.into_inner().sandbox_id; Ok(Response::new(CreateSshSessionResponse { sandbox_id, @@ -409,11 +433,16 @@ impl OpenShell for TestOpenShell { let sandbox_id = request.into_inner().id; let (tx, rx) = mpsc::channel(4); let vm_error_after_started = self.state.vm_error_after_started.load(Ordering::SeqCst); + let vm_error_with_observed_exit = self + .state + .vm_error_with_observed_exit + .load(Ordering::SeqCst); let vm_slow_progress_before_ready = self .state .vm_slow_progress_before_ready .load(Ordering::SeqCst); let vm_log_churn_before_ready = self.state.vm_log_churn_before_ready.load(Ordering::SeqCst); + let terminal_before_relay = self.state.terminal_before_relay.load(Ordering::SeqCst); tokio::spawn(async move { let mut provisioning = Sandbox { @@ -445,8 +474,17 @@ impl OpenShell for TestOpenShell { ..provisioning.clone() }; error.set_phase(SandboxPhase::Error as i32); + if vm_error_with_observed_exit { + error.status.as_mut().unwrap().exit_code = Some(137); + } let mut ready = provisioning.clone(); ready.set_phase(SandboxPhase::Ready as i32); + let mut completed = provisioning.clone(); + completed.status = Some(SandboxStatus { + exit_code: Some(0), + ..SandboxStatus::default() + }); + completed.set_phase(SandboxPhase::Completed as i32); let _ = tx .send(Ok(SandboxStreamEvent { @@ -496,6 +534,14 @@ impl OpenShell for TestOpenShell { .await; return; } + if terminal_before_relay { + let _ = tx + .send(Ok(SandboxStreamEvent { + payload: Some(sandbox_stream_event::Payload::Sandbox(completed)), + })) + .await; + return; + } if vm_slow_progress_before_ready { tokio::time::sleep(Duration::from_millis(600)).await; let _ = tx @@ -1341,6 +1387,35 @@ async fn sandbox_create_persists_exact_trailing_argv_as_main_process() { .expect("sandbox spec should be persisted at create time"); assert_eq!(spec.command, command); assert!(!spec.tty); + assert!(requests[0].await_main_process_attachment); +} + +#[tokio::test] +async fn detached_command_does_not_declare_main_process_attachment() { + let server = run_server().await; + let fake_ssh_dir = tempfile::tempdir().unwrap(); + let xdg_dir = tempfile::tempdir().unwrap(); + let _env = test_env(&fake_ssh_dir, &xdg_dir); + let tls = test_tls(&server); + install_fake_ssh(&fake_ssh_dir); + + run::sandbox_create( + &server.endpoint, + "openshell", + run::SandboxCreateConfig { + name: Some("detached-main"), + command: &["echo".into(), "OK".into()], + detach: true, + ..test_config() + }, + "default", + &tls, + ) + .await + .expect("detached sandbox create should succeed"); + + let requests = create_requests(&server).await; + assert!(!requests[0].await_main_process_attachment); } #[tokio::test] @@ -1558,6 +1633,44 @@ async fn sandbox_create_returns_vm_error_without_waiting_for_timeout() { assert!(!rendered.contains("timed out")); } +#[tokio::test] +async fn sandbox_create_preserves_vm_error_when_exit_code_is_observed() { + let server = run_server().await; + server + .openshell + .state + .vm_error_after_started + .store(true, Ordering::SeqCst); + server + .openshell + .state + .vm_error_with_observed_exit + .store(true, Ordering::SeqCst); + let fake_ssh_dir = tempfile::tempdir().unwrap(); + let xdg_dir = tempfile::tempdir().unwrap(); + let _env = test_env(&fake_ssh_dir, &xdg_dir); + let tls = test_tls(&server); + install_fake_ssh(&fake_ssh_dir); + + let err = run::sandbox_create( + &server.endpoint, + "openshell", + run::SandboxCreateConfig { + name: Some("vm-error-with-exit"), + command: &["echo".into(), "OK".into()], + ..test_config() + }, + "default", + &tls, + ) + .await + .expect_err("an observed process exit must not hide the infrastructure error"); + + let rendered = err.to_string(); + assert!(rendered.contains("sandbox entered error phase while provisioning")); + assert!(rendered.contains("ProcessExited: VM process exited with status 0")); +} + #[tokio::test] async fn sandbox_create_keeps_waiting_while_vm_progress_arrives() { let server = run_server().await; @@ -1631,6 +1744,50 @@ async fn sandbox_create_times_out_when_only_logs_arrive() { assert!(err.to_string().contains("sandbox provisioning timed out")); } +#[tokio::test] +async fn sandbox_create_retries_terminal_attachment_until_relay_registers() { + let server = run_server().await; + server + .openshell + .state + .terminal_before_relay + .store(true, Ordering::SeqCst); + server + .openshell + .state + .ssh_session_failures_remaining + .store(1, Ordering::SeqCst); + let fake_ssh_dir = tempfile::tempdir().unwrap(); + let xdg_dir = tempfile::tempdir().unwrap(); + let _env = test_env(&fake_ssh_dir, &xdg_dir); + let tls = test_tls(&server); + install_fake_ssh(&fake_ssh_dir); + + let exit_code = run::sandbox_create( + &server.endpoint, + "openshell", + run::SandboxCreateConfig { + name: Some("fast-command"), + command: &["echo".into(), "OK".into()], + ..test_config() + }, + "default", + &tls, + ) + .await + .expect("sandbox create should wait for the declared terminal attachment relay"); + + assert_eq!(exit_code, 0); + assert_eq!( + server + .openshell + .state + .ssh_session_requests + .load(Ordering::SeqCst), + 2 + ); +} + #[tokio::test] async fn sandbox_create_deletes_command_sessions_with_no_keep() { let server = run_server().await; @@ -1659,6 +1816,14 @@ async fn sandbox_create_deletes_command_sessions_with_no_keep() { deleted_names(&server).await, vec![vec!["ephemeral-command".to_string()]] ); + let requests = create_requests(&server).await; + assert_eq!( + requests[0] + .annotations + .get("openshell.nvidia.com/retention") + .map(String::as_str), + Some("ephemeral") + ); assert_eq!( load_last_sandbox("openshell", "default"), None, @@ -1666,6 +1831,37 @@ async fn sandbox_create_deletes_command_sessions_with_no_keep() { ); } +#[tokio::test] +async fn sandbox_create_returns_exact_main_status_after_no_keep_cleanup() { + let server = run_server().await; + let fake_ssh_dir = tempfile::tempdir().unwrap(); + let xdg_dir = tempfile::tempdir().unwrap(); + let _env = test_env(&fake_ssh_dir, &xdg_dir); + let tls = test_tls(&server); + install_executable_script(&fake_ssh_dir, "ssh", "#!/bin/sh\nexit 7\n"); + + let exit_code = run::sandbox_create( + &server.endpoint, + "openshell", + run::SandboxCreateConfig { + name: Some("ephemeral-failure"), + keep: false, + command: &["sh".into(), "-c".into(), "exit 7".into()], + ..test_config() + }, + "default", + &tls, + ) + .await + .expect("a main-process failure is a command result, not a cleanup error"); + + assert_eq!(exit_code, 7); + assert_eq!( + deleted_names(&server).await, + vec![vec!["ephemeral-failure".to_string()]] + ); +} + #[tokio::test] async fn sandbox_create_deletes_shell_sessions_with_no_keep() { let server = run_server().await; diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index 7e2cf74f50..ee72728aa9 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -56,6 +56,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not used by this test server")) } + async fn finalize_main_process_exit( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this test server")) + } + async fn get_current_user( &self, _request: tonic::Request, diff --git a/crates/openshell-core/src/sandbox_env.rs b/crates/openshell-core/src/sandbox_env.rs index 40a7f0a72f..8da9a7d2a8 100644 --- a/crates/openshell-core/src/sandbox_env.rs +++ b/crates/openshell-core/src/sandbox_env.rs @@ -40,6 +40,8 @@ pub struct MainProcessConfig { pub version: u32, pub command: Vec, pub tty: bool, + #[serde(default)] + pub await_main_process_attachment: bool, } impl MainProcessConfig { @@ -51,6 +53,7 @@ impl MainProcessConfig { version: Self::VERSION, command: vec!["/bin/bash".to_string(), "-l".to_string()], tty: true, + await_main_process_attachment: false, } } @@ -61,6 +64,7 @@ impl MainProcessConfig { version: Self::VERSION, command: spec.command.clone(), tty: spec.tty, + await_main_process_attachment: spec.await_main_process_attachment, }, None | Some(_) => Self::scratch(), } @@ -233,12 +237,14 @@ mod tests { let spec = crate::proto::compute::v1::DriverSandboxSpec { command: vec!["/bin/sh".into(), "-c".into(), "printf '%s' 'a b'".into()], tty: false, + await_main_process_attachment: true, ..Default::default() }; let encoded = MainProcessConfig::encode_driver_spec(Some(&spec)).unwrap(); let decoded = MainProcessConfig::decode(&encoded).unwrap(); assert_eq!(decoded.command, spec.command); assert!(!decoded.tty); + assert!(decoded.await_main_process_attachment); } #[test] diff --git a/crates/openshell-driver-docker/src/tests.rs b/crates/openshell-driver-docker/src/tests.rs index b52cb87836..fb5bc61e5c 100644 --- a/crates/openshell-driver-docker/src/tests.rs +++ b/crates/openshell-driver-docker/src/tests.rs @@ -47,6 +47,7 @@ fn test_sandbox() -> DriverSandbox { sandbox_token: String::new(), command: Vec::new(), tty: false, + await_main_process_attachment: false, }), status: None, workspace: String::new(), diff --git a/crates/openshell-gateway-interceptors/src/proto_json.rs b/crates/openshell-gateway-interceptors/src/proto_json.rs index f6aecbcf67..b0d6b0f53d 100644 --- a/crates/openshell-gateway-interceptors/src/proto_json.rs +++ b/crates/openshell-gateway-interceptors/src/proto_json.rs @@ -316,6 +316,7 @@ mod tests { labels: HashMap::from([("team".to_string(), "agent".to_string())]), annotations: HashMap::new(), workspace: String::new(), + await_main_process_attachment: false, }; let bytes = request.encode_to_vec(); let json = codec diff --git a/crates/openshell-gateway-interceptors/src/runtime.rs b/crates/openshell-gateway-interceptors/src/runtime.rs index 4e7f5ba613..02afddab75 100644 --- a/crates/openshell-gateway-interceptors/src/runtime.rs +++ b/crates/openshell-gateway-interceptors/src/runtime.rs @@ -1075,6 +1075,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + await_main_process_attachment: false, }; let bytes = request.encode_to_vec(); diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index b1c226cebd..e70aac7a9f 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -103,6 +103,7 @@ pub async fn run_sandbox( workdir: Option, timeout_secs: u64, interactive: bool, + await_main_process_attachment: bool, sandbox_id: Option, sandbox: Option, openshell_endpoint: Option, @@ -889,35 +890,54 @@ pub async fn run_sandbox( } else { None }; - let sidecar_exit_tx = - if process_uses_sidecar_control && let Some(writer) = process_control_writer.clone() { - let exit_ack = Arc::clone(&process_exit_ack); - let (tx, mut rx) = tokio::sync::mpsc::channel::< - openshell_supervisor_process::run::SidecarExitReport, - >(1); - tokio::spawn(async move { - while let Some((instance_id, exit_code, ack)) = rx.recv().await { - let (durable_tx, durable_rx) = tokio::sync::oneshot::channel(); - *exit_ack.lock().await = Some((instance_id.clone(), durable_tx)); - let result = match sidecar_control::send_main_process_exited( - &writer, + let sidecar_exit_tx = if process_uses_sidecar_control + && let Some(writer) = process_control_writer.clone() + { + let exit_ack = Arc::clone(&process_exit_ack); + let (tx, mut rx) = tokio::sync::mpsc::channel::< + openshell_supervisor_process::run::SidecarExitReport, + >(1); + tokio::spawn(async move { + while let Some(report) = rx.recv().await { + match report { + openshell_supervisor_process::run::SidecarExitReport::Exited { instance_id, exit_code, - ) - .await - { - Ok(()) => durable_rx.await.map_err(|_| { - "sidecar durable exit acknowledgement closed".to_string() - }), - Err(error) => Err(error.to_string()), - }; - let _ = ack.send(result); + ack, + } => { + let (durable_tx, durable_rx) = tokio::sync::oneshot::channel(); + *exit_ack.lock().await = Some((instance_id.clone(), durable_tx)); + let result = match sidecar_control::send_main_process_exited( + &writer, + instance_id, + exit_code, + ) + .await + { + Ok(()) => durable_rx.await.map_err(|_| { + "sidecar durable exit acknowledgement closed".to_string() + }), + Err(error) => Err(error.to_string()), + }; + let _ = ack.send(result); + } + openshell_supervisor_process::run::SidecarExitReport::Finalized { + instance_id, + ack, + } => { + let result = + sidecar_control::send_main_process_finalized(&writer, instance_id) + .await + .map_err(|error| error.to_string()); + let _ = ack.send(result); + } } - }); - Some(tx) - } else { - None - }; + } + }); + Some(tx) + } else { + None + }; let process = openshell_supervisor_process::run::run_process( program, @@ -925,6 +945,7 @@ pub async fn run_sandbox( workspace, timeout_secs, interactive, + await_main_process_attachment, sandbox_id.as_deref(), openshell_endpoint.as_deref(), ssh_socket_path, @@ -1313,11 +1334,39 @@ fn spawn_sidecar_entrypoint_handler( control_publisher, } = handler; let mut session_started = false; + let mut session_task: Option> = None; let mut trusted_supervisor_pid = None; let terminating = Arc::new(AtomicBool::new(false)); while let Some(started) = entrypoint_rx.recv().await { - if let Some(exit_code) = started.exit_code { + if started.finalized { + if let (Some(endpoint), Some(id)) = + (openshell_endpoint.as_ref(), sandbox_id.as_ref()) + { + let mut delay = Duration::from_millis(250); + loop { + match openshell_supervisor_process::supervisor_session::finalize_main_process_exit( + endpoint, + id, + &started.instance_id, + ) + .await + { + Ok(()) => break, + Err(error) => { + warn!(%error, "sidecar main-process finalization failed; retrying"); + tokio::time::sleep(delay).await; + delay = (delay * 2).min(Duration::from_secs(2)); + } + } + } + } terminating.store(true, Ordering::Release); + if let Some(task) = session_task.take() { + task.abort(); + } + break; + } + if let Some(exit_code) = started.exit_code { if let (Some(endpoint), Some(id)) = (openshell_endpoint.as_ref(), sandbox_id.as_ref()) { @@ -1343,7 +1392,7 @@ fn spawn_sidecar_entrypoint_handler( publisher.publish_main_process_exit_ack(started.instance_id.clone()); } } - break; + continue; } entrypoint_pid.store(started.pid, Ordering::Release); if started.start_session { @@ -1386,7 +1435,7 @@ fn spawn_sidecar_entrypoint_handler( ); continue; }; - openshell_supervisor_process::supervisor_session::spawn( + session_task = Some(openshell_supervisor_process::supervisor_session::spawn( endpoint.clone(), id.clone(), trusted_ssh_socket_path.clone(), @@ -1394,7 +1443,7 @@ fn spawn_sidecar_entrypoint_handler( Some(supervisor_pid), Arc::clone(&terminating), started.instance_id.clone(), - ); + )); session_started = true; info!("sidecar supervisor session task spawned"); } diff --git a/crates/openshell-sandbox/src/main.rs b/crates/openshell-sandbox/src/main.rs index 6d244fb6bc..7108378654 100644 --- a/crates/openshell-sandbox/src/main.rs +++ b/crates/openshell-sandbox/src/main.rs @@ -659,15 +659,23 @@ fn main() -> Result<()> { // drivers otherwise provide a versioned JSON transport so argument // boundaries are never reconstructed with shell parsing. let workdir = args.workdir.clone(); - let (command, interactive) = if !args.command.is_empty() { - (args.command, args.interactive) + let (command, interactive, await_main_process_attachment) = if !args.command.is_empty() { + (args.command, args.interactive, false) } else if let Ok(json) = std::env::var(openshell_core::sandbox_env::MAIN_PROCESS_SPEC) { let config = openshell_core::sandbox_env::MainProcessConfig::decode(&json) .map_err(|error| miette::miette!("{error}"))?; - (config.command, config.tty) + ( + config.command, + config.tty, + config.await_main_process_attachment, + ) } else { let config = openshell_core::sandbox_env::MainProcessConfig::scratch(); - (config.command, config.tty) + ( + config.command, + config.tty, + config.await_main_process_attachment, + ) }; info!(command = ?command, "Starting sandbox"); @@ -689,6 +697,7 @@ fn main() -> Result<()> { workdir, args.timeout, interactive, + await_main_process_attachment, args.sandbox_id, args.sandbox, args.openshell_endpoint, diff --git a/crates/openshell-sandbox/src/sidecar_control.rs b/crates/openshell-sandbox/src/sidecar_control.rs index 658a20132c..fdf88f050b 100644 --- a/crates/openshell-sandbox/src/sidecar_control.rs +++ b/crates/openshell-sandbox/src/sidecar_control.rs @@ -38,6 +38,7 @@ pub struct EntrypointStarted { pub start_session: bool, pub instance_id: String, pub exit_code: Option, + pub finalized: bool, } #[derive(Debug, Clone, Copy)] @@ -176,6 +177,7 @@ enum WireClientMessage { BootstrapRequest { supervisor_pid: u32 }, EntrypointStarted { pid: u32, instance_id: String }, MainProcessExited { instance_id: String, exit_code: i32 }, + MainProcessFinalized { instance_id: String }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -466,12 +468,14 @@ async fn handle_connection( start_session: false, instance_id: String::new(), exit_code: None, + finalized: false, }) .await .map_err(|_| miette::miette!("sidecar entrypoint receiver closed"))?; } WireClientMessage::EntrypointStarted { .. } - | WireClientMessage::MainProcessExited { .. } => { + | WireClientMessage::MainProcessExited { .. } + | WireClientMessage::MainProcessFinalized { .. } => { return Err(miette::miette!( "sidecar control client sent entrypoint event before bootstrap" )); @@ -509,6 +513,7 @@ async fn handle_connection( start_session: true, instance_id, exit_code: None, + finalized: false, }) .await .map_err(|_| miette::miette!("sidecar entrypoint receiver closed"))?; @@ -523,6 +528,19 @@ async fn handle_connection( start_session: false, instance_id, exit_code: Some(exit_code), + finalized: false, + }) + .await + .map_err(|_| miette::miette!("sidecar entrypoint receiver closed"))?; + } + WireClientMessage::MainProcessFinalized { instance_id } => { + entrypoint_tx + .send(EntrypointStarted { + pid: 0, + start_session: false, + instance_id, + exit_code: None, + finalized: true, }) .await .map_err(|_| miette::miette!("sidecar entrypoint receiver closed"))?; @@ -640,6 +658,15 @@ pub async fn send_main_process_exited( write_json_line(&mut *writer, &message).await } +pub async fn send_main_process_finalized( + writer: &Arc>, + instance_id: String, +) -> Result<()> { + let message = WireClientMessage::MainProcessFinalized { instance_id }; + let mut writer = writer.lock().await; + write_json_line(&mut *writer, &message).await +} + async fn write_json_line(writer: &mut W, value: &T) -> Result<()> where W: AsyncWrite + Unpin + Send, @@ -893,6 +920,7 @@ mod tests { .unwrap() .unwrap(); assert_eq!(terminal.exit_code, Some(0)); + assert!(!terminal.finalized); assert!( tokio::time::timeout(Duration::from_millis(20), connection.updates.recv()) @@ -909,6 +937,16 @@ mod tests { ack, ControlUpdate::MainProcessExitAck { instance_id } if instance_id == "instance-1" )); + + send_main_process_finalized(&connection.writer, "instance-1".to_string()) + .await + .unwrap(); + let delivered = tokio::time::timeout(Duration::from_secs(1), entrypoint_rx.recv()) + .await + .unwrap() + .unwrap(); + assert!(delivered.exit_code.is_none()); + assert!(delivered.finalized); } #[tokio::test] diff --git a/crates/openshell-sdk/src/client.rs b/crates/openshell-sdk/src/client.rs index f95b7ee111..fd2b7d5273 100644 --- a/crates/openshell-sdk/src/client.rs +++ b/crates/openshell-sdk/src/client.rs @@ -246,15 +246,19 @@ impl OpenShellClient { } /// Poll [`OpenShellClient::get_sandbox`] until the sandbox reaches - /// [`SandboxPhase::Ready`] or the `timeout` elapses. + /// [`SandboxPhase::Ready`] or successful [`SandboxPhase::Completed`], or + /// until the `timeout` elapses. /// /// Returns the terminal sandbox snapshot on success. Returns an /// [`SdkError::Connect`] when the timeout expires, or whatever error /// the gateway returns if the sandbox transitions into - /// [`SandboxPhase::Error`]. + /// [`SandboxPhase::Stopped`] or [`SandboxPhase::Error`]. pub async fn wait_ready(&self, name: &str, timeout: Duration) -> Result { self.wait_for(name, timeout, |phase| match phase { - SandboxPhase::Ready => Some(Ok(())), + SandboxPhase::Ready | SandboxPhase::Completed => Some(Ok(())), + SandboxPhase::Stopped => Some(Err(SdkError::connect(format!( + "sandbox '{name}' main process failed" + )))), SandboxPhase::Error => Some(Err(SdkError::connect(format!( "sandbox '{name}' entered error phase" )))), @@ -644,15 +648,22 @@ impl WorkspaceScopedClient { sandbox_from_response(response.sandbox) } - /// Poll until the sandbox reaches [`SandboxPhase::Ready`] or the timeout - /// elapses. + /// Poll until the sandbox reaches [`SandboxPhase::Ready`] or successful + /// [`SandboxPhase::Completed`], or the timeout elapses. pub async fn wait_ready(&self, name: &str, timeout: Duration) -> Result { let deadline = Instant::now() + timeout; let mut delay = Duration::from_millis(250); loop { let snapshot = self.get_sandbox(name).await?; match snapshot.phase { - SandboxPhase::Ready => return Ok(snapshot), + SandboxPhase::Ready | SandboxPhase::Completed => return Ok(snapshot), + SandboxPhase::Stopped => { + let detail = snapshot.exit_code.map_or_else( + || "stopped before becoming ready".to_string(), + |code| format!("main process failed with status {code}"), + ); + return Err(SdkError::connect(format!("sandbox '{name}' {detail}"))); + } SandboxPhase::Error => { return Err(SdkError::connect(format!( "sandbox '{name}' entered error phase" @@ -823,6 +834,7 @@ fn create_sandbox_request(spec: SandboxSpec) -> proto::CreateSandboxRequest { labels, annotations: HashMap::new(), workspace: String::new(), + await_main_process_attachment: false, } } diff --git a/crates/openshell-sdk/src/types.rs b/crates/openshell-sdk/src/types.rs index 3715c7fdd6..f7f1547115 100644 --- a/crates/openshell-sdk/src/types.rs +++ b/crates/openshell-sdk/src/types.rs @@ -65,6 +65,7 @@ pub enum SandboxPhase { Stopping, Stopped, Starting, + Completed, } impl From for SandboxPhase { @@ -79,6 +80,7 @@ impl From for SandboxPhase { proto::SandboxPhase::Stopping => Self::Stopping, proto::SandboxPhase::Stopped => Self::Stopped, proto::SandboxPhase::Starting => Self::Starting, + proto::SandboxPhase::Completed => Self::Completed, } } } diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 4b06b9c1e5..1cdac7da41 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -107,6 +107,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not used by this test server")) } + async fn finalize_main_process_exit( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this test server")) + } + async fn get_current_user( &self, _request: tonic::Request, @@ -908,6 +915,41 @@ async fn wait_ready_transitions_through_phases() { assert!(state.get_calls.load(Ordering::SeqCst) >= 3); } +#[tokio::test] +async fn wait_ready_accepts_successful_completion() { + let state = Arc::new(MockState { + phase_sequence: vec![ + proto::SandboxPhase::Provisioning, + proto::SandboxPhase::Completed, + ], + ..Default::default() + }); + let endpoint = start_mock(state).await; + let client = connect(&endpoint).await; + + let sandbox = client + .wait_ready("short-job", std::time::Duration::from_secs(5)) + .await + .unwrap(); + assert_eq!(sandbox.phase, SandboxPhase::Completed); +} + +#[tokio::test] +async fn wait_ready_surfaces_stopped_phase_without_timing_out() { + let state = Arc::new(MockState { + phase_sequence: vec![proto::SandboxPhase::Stopped], + ..Default::default() + }); + let endpoint = start_mock(state).await; + let client = connect(&endpoint).await; + + let err = client + .wait_ready("failed-job", std::time::Duration::from_secs(5)) + .await + .unwrap_err(); + assert_eq!(err.code(), "connect"); +} + #[tokio::test] async fn wait_ready_surfaces_error_phase() { let state = Arc::new(MockState { diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index b85641c986..ef311523a9 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -941,6 +941,7 @@ impl ComputeRuntime { &self, sandbox: Sandbox, sandbox_token: Option, + await_main_process_attachment: bool, ) -> Result { let sandbox_id = sandbox.object_id().to_string(); let mut driver_sandbox = driver_sandbox_from_public(&sandbox, &self.driver_info.name) @@ -989,6 +990,9 @@ impl ComputeRuntime { { spec.sandbox_token = token; } + if let Some(spec) = driver_sandbox.spec.as_mut() { + spec.await_main_process_attachment = await_main_process_attachment; + } // A1: stage the typed SandboxPolicy out-of-band into the MXC backend's // side channel, keyed by sandbox id (== DriverSandbox.id), immediately // before dispatch. The driver removes/consumes it in create_sandbox. The @@ -1079,7 +1083,9 @@ impl ComputeRuntime { } let phase = SandboxPhase::try_from(current.phase()).unwrap_or(SandboxPhase::Unknown); - if phase == SandboxPhase::Stopped { + if matches!(phase, SandboxPhase::Stopped | SandboxPhase::Completed) + || is_failed_main_process_result(¤t) + { self.cleanup_stopped_sandbox_sessions(¤t) .await .map_err(Status::internal)?; @@ -1237,12 +1243,22 @@ impl ComputeRuntime { if phase == SandboxPhase::Ready { return Ok(current); } - if !matches!(phase, SandboxPhase::Stopped | SandboxPhase::Starting) { + if !matches!( + phase, + SandboxPhase::Stopped | SandboxPhase::Completed | SandboxPhase::Starting + ) && !is_failed_main_process_result(¤t) + { return Err(Status::failed_precondition(format!( - "sandbox must be Stopped to start (current phase: {phase:?})" + "sandbox must be Stopped, Completed, or a failed main-process Error to start (current phase: {phase:?})" ))); } + if phase == SandboxPhase::Completed || is_failed_main_process_result(¤t) { + self.cleanup_stopped_sandbox_sessions(¤t) + .await + .map_err(Status::internal)?; + } + let (previous, starting) = if phase == SandboxPhase::Starting { // Acquiring the lifecycle gate proves that no local worker still // owns this transition. Retry the idempotent driver operation. @@ -2335,11 +2351,16 @@ impl ComputeRuntime { }; let phase = SandboxPhase::try_from(sandbox.phase()).unwrap_or(SandboxPhase::Unknown); match phase { - SandboxPhase::Stopped => { + SandboxPhase::Stopped | SandboxPhase::Completed => { if let Err(err) = self.cleanup_stopped_sandbox_sessions(&sandbox).await { warn!(sandbox_id = %sandbox.object_id(), error = %err, "Failed to complete recovered sandbox session cleanup"); } } + SandboxPhase::Error if is_failed_main_process_result(&sandbox) => { + if let Err(err) = self.cleanup_stopped_sandbox_sessions(&sandbox).await { + warn!(sandbox_id = %sandbox.object_id(), error = %err, "Failed to complete recovered failed-main session cleanup"); + } + } SandboxPhase::Stopping => { let sandbox_id = sandbox.object_id().to_string(); let sandbox_name = sandbox.object_name().to_string(); @@ -2857,12 +2878,16 @@ impl ComputeRuntime { sandbox_id: &str, instance_id: &str, ) -> Result<(), String> { - self.set_supervisor_session_state(sandbox_id, true, Some(instance_id)) + self.set_supervisor_session_state(sandbox_id, true, Some(instance_id), false) .await } - pub async fn supervisor_session_disconnected(&self, sandbox_id: &str) -> Result<(), String> { - self.set_supervisor_session_state(sandbox_id, false, None) + pub async fn supervisor_session_disconnected( + &self, + sandbox_id: &str, + terminal_delivery_finalized: bool, + ) -> Result<(), String> { + self.set_supervisor_session_state(sandbox_id, false, None, terminal_delivery_finalized) .await } @@ -2871,8 +2896,9 @@ impl ComputeRuntime { sandbox_id: &str, connected: bool, instance_id: Option<&str>, + terminal_delivery_finalized: bool, ) -> Result<(), String> { - let _guard = self.sync_lock.lock().await; + let guard = self.sync_lock.lock().await; let Some(existing) = self .store @@ -2884,12 +2910,21 @@ impl ComputeRuntime { }; let current_phase = SandboxPhase::try_from(existing.phase()).unwrap_or(SandboxPhase::Unknown); + if !connected + && matches!(current_phase, SandboxPhase::Error | SandboxPhase::Completed) + && terminal_delivery_finalized + { + drop(guard); + self.schedule_ephemeral_sandbox_delete(&existing); + return Ok(()); + } if matches!( current_phase, SandboxPhase::Deleting | SandboxPhase::Error | SandboxPhase::Stopping | SandboxPhase::Stopped + | SandboxPhase::Completed ) { return Ok(()); } @@ -2941,13 +2976,23 @@ impl ComputeRuntime { Ok(()) } - /// Persist a terminal canonical-process result. Exit code zero is still a - /// sandbox error because the canonical process defines sandbox health. + /// Persist a terminal canonical-process result. Successful completion is + /// distinct from a nonzero command result and from infrastructure error. pub async fn main_process_exited( &self, sandbox_id: &str, instance_id: &str, exit_code: i32, + ) -> Result<(), String> { + self.report_main_process_exit(sandbox_id, instance_id, exit_code) + .await + } + + pub async fn report_main_process_exit( + &self, + sandbox_id: &str, + instance_id: &str, + exit_code: i32, ) -> Result<(), String> { let _guard = self.sync_lock.lock().await; let Some(existing) = self @@ -2999,6 +3044,7 @@ impl ComputeRuntime { reported_exit_code = exit_code, "ignoring conflicting duplicate main-process exit report" ); + return Ok(()); } return Ok(()); } @@ -3016,6 +3062,60 @@ impl ComputeRuntime { Ok(()) } + /// Permit cleanup after the main-process terminal transport has finished. + pub async fn finalize_main_process_exit( + &self, + sandbox_id: &str, + instance_id: &str, + ) -> Result<(), String> { + let _guard = self.sync_lock.lock().await; + let Some(sandbox) = self + .store + .get_message::(sandbox_id) + .await + .map_err(|error| error.to_string())? + else { + return Ok(()); + }; + let Some(status) = sandbox.status.as_ref() else { + return Err("main-process exit has not been reported".to_string()); + }; + if status.exit_code.is_none() { + return Err("main-process exit has not been reported".to_string()); + } + if !status.main_process_instance_id.is_empty() + && status.main_process_instance_id != instance_id + { + return Err("main-process instance does not match the terminal result".to_string()); + } + Ok(()) + } + + fn schedule_ephemeral_sandbox_delete(&self, sandbox: &Sandbox) { + let ephemeral = sandbox.metadata.as_ref().is_some_and(|metadata| { + metadata + .annotations + .get("openshell.nvidia.com/retention") + .is_some_and(|value| value == "ephemeral") + }); + if !ephemeral { + return; + } + + let runtime = self.clone(); + let workspace = sandbox.object_workspace().to_string(); + let name = sandbox.object_name().to_string(); + tokio::spawn(async move { + if let Err(error) = runtime.delete_sandbox(&workspace, &name).await { + tracing::warn!( + sandbox_name = %name, + error = %error, + "Failed to delete completed ephemeral sandbox" + ); + } + }); + } + async fn apply_deleted(&self, sandbox_id: &str) -> Result<(), String> { let _guard = self.sync_lock.lock().await; self.apply_deleted_locked(sandbox_id).await @@ -3283,6 +3383,11 @@ impl ComputeRuntime { let sandbox = decode_sandbox_record(¤t_record)?; let phase = SandboxPhase::try_from(sandbox.phase()).unwrap_or(SandboxPhase::Unknown); + if phase == SandboxPhase::Completed || is_failed_main_process_result(&sandbox) { + // A terminal canonical process may legitimately have removed its + // transient compute object. Keep the durable command result. + return Ok(()); + } if matches!( phase, SandboxPhase::Stopping | SandboxPhase::Stopped | SandboxPhase::Starting @@ -3372,24 +3477,53 @@ impl ComputeRuntime { fn apply_main_process_exit(sandbox: &mut Sandbox, instance_id: &str, exit_code: i32) { let sandbox_name = sandbox.object_name().to_string(); + let preserve_infrastructure_error = sandbox.phase() == SandboxPhase::Error as i32; let status = sandbox.status.get_or_insert_with(|| SandboxStatus { sandbox_name: sandbox_name.clone(), ..Default::default() }); status.main_process_instance_id = instance_id.to_string(); status.exit_code = Some(exit_code); + if preserve_infrastructure_error { + return; + } + let (phase, reason, message) = if exit_code == 0 { + ( + SandboxPhase::Completed, + "MainProcessCompleted", + "Canonical main process completed successfully".to_string(), + ) + } else { + ( + SandboxPhase::Error, + "MainProcessFailed", + format!("Canonical main process exited with status {exit_code}"), + ) + }; upsert_ready_condition( &mut sandbox.status, &sandbox_name, SandboxCondition { r#type: "Ready".to_string(), status: "False".to_string(), - reason: "MainProcessExited".to_string(), - message: "Canonical main process exited".to_string(), + reason: reason.to_string(), + message, last_transition_time: String::new(), }, ); - sandbox.set_phase(SandboxPhase::Error as i32); + sandbox.set_phase(phase as i32); +} + +fn is_failed_main_process_result(sandbox: &Sandbox) -> bool { + sandbox.phase() == SandboxPhase::Error as i32 + && sandbox.status.as_ref().is_some_and(|status| { + status.exit_code.is_some() + && status.conditions.iter().any(|condition| { + condition.r#type == "Ready" + && condition.status.eq_ignore_ascii_case("false") + && condition.reason == "MainProcessFailed" + }) + }) } /// Connect to an unmanaged remote compute driver that is already listening on @@ -3486,6 +3620,7 @@ fn driver_sandbox_spec_from_public( sandbox_token: String::new(), command: spec.command.clone(), tty: spec.tty, + await_main_process_attachment: false, }) } @@ -3766,10 +3901,10 @@ fn apply_driver_snapshot(sandbox: &mut Sandbox, incoming: &DriverSandbox, sessio let old_phase = SandboxPhase::try_from(sandbox.phase()).unwrap_or(SandboxPhase::Unknown); let sandbox_name = &incoming.name; - // Error is terminal until an explicit future lifecycle operation changes - // desired state. In particular, a still-running backend snapshot must not - // revive a sandbox whose canonical process has exited. - if old_phase == SandboxPhase::Error { + // Infrastructure errors and successful main-process completions are + // sticky until an explicit lifecycle operation changes desired state. A + // late backend snapshot must not revive either result. + if matches!(old_phase, SandboxPhase::Error | SandboxPhase::Completed) { if let Some(metadata) = sandbox.metadata.as_mut() { metadata.name.clone_from(sandbox_name); } @@ -3816,6 +3951,7 @@ fn apply_driver_snapshot(sandbox: &mut Sandbox, incoming: &DriverSandbox, sessio } SandboxPhase::Stopping if phase != SandboxPhase::Error => SandboxPhase::Stopping, SandboxPhase::Stopped => SandboxPhase::Stopped, + SandboxPhase::Completed => SandboxPhase::Completed, SandboxPhase::Starting if !matches!(phase, SandboxPhase::Ready | SandboxPhase::Error) => { SandboxPhase::Starting } @@ -5044,13 +5180,13 @@ mod tests { } #[test] - fn main_process_exit_zero_is_terminal_error() { + fn main_process_exit_zero_is_completed() { let mut sandbox = sandbox_record("sb-1", "sandbox-a", SandboxPhase::Ready); apply_main_process_exit(&mut sandbox, "instance-1", 0); assert_eq!( SandboxPhase::try_from(sandbox.phase()), - Ok(SandboxPhase::Error) + Ok(SandboxPhase::Completed) ); let status = sandbox.status.as_ref().unwrap(); assert_eq!(status.exit_code, Some(0)); @@ -5058,7 +5194,26 @@ mod tests { assert!(status.conditions.iter().any(|condition| { condition.r#type == "Ready" && condition.status == "False" - && condition.reason == "MainProcessExited" + && condition.reason == "MainProcessCompleted" + })); + } + + #[test] + fn main_process_nonzero_exit_is_error() { + let mut sandbox = sandbox_record("sb-1", "sandbox-a", SandboxPhase::Ready); + apply_main_process_exit(&mut sandbox, "instance-1", 7); + + assert_eq!( + SandboxPhase::try_from(sandbox.phase()), + Ok(SandboxPhase::Error) + ); + let status = sandbox.status.as_ref().unwrap(); + assert_eq!(status.exit_code, Some(7)); + assert_eq!(status.main_process_instance_id, "instance-1"); + assert!(status.conditions.iter().any(|condition| { + condition.r#type == "Ready" + && condition.status == "False" + && condition.reason == "MainProcessFailed" })); } @@ -5126,6 +5281,55 @@ mod tests { assert_eq!(stored.status.unwrap().exit_code, Some(9)); } + #[tokio::test] + async fn ephemeral_cleanup_waits_for_terminal_finalization() { + let driver = ControlledDriver::new(); + let runtime = test_runtime(driver.clone()).await; + let mut sandbox = sandbox_record("sb-1", "sandbox-a", SandboxPhase::Provisioning); + sandbox.metadata.as_mut().unwrap().annotations.insert( + "openshell.nvidia.com/retention".to_string(), + "ephemeral".to_string(), + ); + runtime.store.put_message(&sandbox).await.unwrap(); + runtime + .supervisor_session_connected("sb-1", "instance-1") + .await + .unwrap(); + + runtime + .report_main_process_exit("sb-1", "instance-1", 0) + .await + .unwrap(); + assert_eq!(driver.delete_calls(), 0); + assert_eq!( + runtime + .store + .get_message::("sb-1") + .await + .unwrap() + .unwrap() + .phase(), + SandboxPhase::Completed as i32 + ); + + runtime + .finalize_main_process_exit("sb-1", "instance-1") + .await + .unwrap(); + assert_eq!(driver.delete_calls(), 0); + runtime + .supervisor_session_disconnected("sb-1", true) + .await + .unwrap(); + tokio::time::timeout(Duration::from_secs(1), async { + while driver.delete_calls() == 0 { + tokio::task::yield_now().await; + } + }) + .await + .expect("terminal finalization should release ephemeral cleanup"); + } + #[tokio::test] async fn conflicting_duplicate_main_process_exit_is_acknowledged() { let runtime = test_runtime(Arc::new(TestDriver::default())).await; @@ -5852,7 +6056,7 @@ mod tests { let traced = test_exporter::install_traced(); async { runtime - .create_sandbox(sandbox, None) + .create_sandbox(sandbox, None, false) .await .expect("create succeeds"); } @@ -5997,7 +6201,7 @@ mod tests { let traced = test_exporter::install_traced(); async { runtime - .create_sandbox(sandbox, None) + .create_sandbox(sandbox, None, false) .await .expect_err("driver refuses the create"); } @@ -6089,6 +6293,94 @@ mod tests { assert_eq!(driver.start_calls(), 2, "ready start is idempotent"); } + #[tokio::test] + async fn completed_sandbox_can_start_a_fresh_main_instance() { + let driver = ControlledDriver::new(); + let runtime = test_runtime(driver.clone()).await; + let mut sandbox = + sandbox_record("sb-completed", "sandbox-completed", SandboxPhase::Completed); + sandbox.status = Some(SandboxStatus { + phase: SandboxPhase::Completed as i32, + main_process_instance_id: "instance-old".to_string(), + exit_code: Some(0), + ..Default::default() + }); + runtime.store.put_message(&sandbox).await.unwrap(); + let session = ssh_session_record("completed-session", sandbox.object_id()); + runtime.store.put_message(&session).await.unwrap(); + register_test_supervisor_session(&runtime, sandbox.object_id()); + + let starting = runtime + .start_sandbox("default", sandbox.object_name()) + .await + .unwrap(); + + assert_eq!(starting.phase(), SandboxPhase::Starting as i32); + let status = starting.status.unwrap(); + assert_eq!(status.main_process_instance_id, "instance-old"); + assert_eq!(status.exit_code, None); + assert_eq!(driver.start_calls(), 1); + assert!(!runtime.supervisor_sessions.has_session(sandbox.object_id())); + assert!( + runtime + .store + .get_message::(session.object_id()) + .await + .unwrap() + .is_none(), + "restart revokes SSH sessions from the completed instance" + ); + } + + #[tokio::test] + async fn failed_main_process_error_can_start_a_fresh_instance() { + let driver = ControlledDriver::new(); + let runtime = test_runtime(driver.clone()).await; + let mut sandbox = sandbox_record("sb-failed", "sandbox-failed", SandboxPhase::Ready); + apply_main_process_exit(&mut sandbox, "instance-old", 130); + runtime.store.put_message(&sandbox).await.unwrap(); + let session = ssh_session_record("failed-session", sandbox.object_id()); + runtime.store.put_message(&session).await.unwrap(); + register_test_supervisor_session(&runtime, sandbox.object_id()); + + let starting = runtime + .start_sandbox("default", sandbox.object_name()) + .await + .unwrap(); + + assert_eq!(starting.phase(), SandboxPhase::Starting as i32); + let status = starting.status.unwrap(); + assert_eq!(status.main_process_instance_id, "instance-old"); + assert_eq!(status.exit_code, None); + assert_eq!(driver.start_calls(), 1); + assert!(!runtime.supervisor_sessions.has_session(sandbox.object_id())); + assert!( + runtime + .store + .get_message::(session.object_id()) + .await + .unwrap() + .is_none(), + "restart revokes SSH sessions from the failed instance" + ); + } + + #[tokio::test] + async fn infrastructure_error_cannot_be_started_as_a_command_result() { + let driver = ControlledDriver::new(); + let runtime = test_runtime(driver.clone()).await; + let sandbox = sandbox_record("sb-error", "sandbox-error", SandboxPhase::Error); + runtime.store.put_message(&sandbox).await.unwrap(); + + let error = runtime + .start_sandbox("default", sandbox.object_name()) + .await + .unwrap_err(); + + assert_eq!(error.code(), Code::FailedPrecondition); + assert_eq!(driver.start_calls(), 0); + } + #[tokio::test] async fn retained_stopping_transition_retries_driver_operation() { let driver = ControlledDriver::new(); @@ -6734,7 +7026,7 @@ mod tests { let mut watch_rx = runtime.sandbox_watch_bus.subscribe("sb-1"); runtime - .supervisor_session_disconnected("sb-1") + .supervisor_session_disconnected("sb-1", false) .await .unwrap(); @@ -6795,7 +7087,7 @@ mod tests { ..Default::default() }); - runtime.create_sandbox(sandbox, None).await.unwrap(); + runtime.create_sandbox(sandbox, None, false).await.unwrap(); runtime .apply_sandbox_update(ready_driver_sandbox("sb-1", "sandbox-a")) .await @@ -7830,6 +8122,38 @@ mod tests { assert_eq!(status.exit_code, None); } + #[tokio::test] + async fn late_driver_exit_preserves_completed_main_result() { + let runtime = test_runtime(Arc::new(TestDriver::default())).await; + let mut sandbox = sandbox_record("sb-1", "sandbox-a", SandboxPhase::Completed); + sandbox.status = Some(SandboxStatus { + sandbox_name: "sandbox-a".to_string(), + phase: SandboxPhase::Completed as i32, + main_process_instance_id: "instance-1".to_string(), + exit_code: Some(0), + ..Default::default() + }); + runtime.store.put_message(&sandbox).await.unwrap(); + let mut exited = ready_driver_sandbox("sb-1", "sandbox-a"); + exited.status = Some(make_driver_status(make_driver_condition( + "ContainerExited", + "container exited after the canonical process completed", + ))); + + runtime.apply_sandbox_update(exited).await.unwrap(); + + let stored = runtime + .store + .get_message::("sb-1") + .await + .unwrap() + .unwrap(); + assert_eq!(stored.phase(), SandboxPhase::Completed as i32); + let status = stored.status.unwrap(); + assert_eq!(status.main_process_instance_id, "instance-1"); + assert_eq!(status.exit_code, Some(0)); + } + #[tokio::test] async fn apply_sandbox_update_without_status_preserves_existing_status() { let runtime = test_runtime(Arc::new(TestDriver::default())).await; @@ -7977,7 +8301,7 @@ mod tests { runtime.store.put_message(&sandbox).await.unwrap(); runtime - .supervisor_session_disconnected("sb-1") + .supervisor_session_disconnected("sb-1", false) .await .unwrap(); @@ -8221,7 +8545,7 @@ mod tests { // Session drops. runtime.supervisor_sessions.cleanup_sandbox("sb-1"); runtime - .supervisor_session_disconnected("sb-1") + .supervisor_session_disconnected("sb-1", false) .await .unwrap(); let stored = runtime @@ -9365,7 +9689,7 @@ mod tests { }); runtime.validate_sandbox_create(&sandbox).await.unwrap(); - runtime.create_sandbox(sandbox, None).await.unwrap(); + runtime.create_sandbox(sandbox, None, false).await.unwrap(); let calls = driver.calls(); assert_eq!(calls.len(), 4, "unexpected calls: {calls:?}"); let validated = match &calls[2] { @@ -9478,7 +9802,7 @@ mod tests { deletion_timestamp_ms: 0, }); - let created = runtime.create_sandbox(sandbox, None).await.unwrap(); + let created = runtime.create_sandbox(sandbox, None, false).await.unwrap(); assert_eq!( created.metadata.as_ref().unwrap().resource_version, @@ -9513,7 +9837,7 @@ mod tests { .labels .insert("env".to_string(), "prod".to_string()); - runtime.create_sandbox(sandbox, None).await.unwrap(); + runtime.create_sandbox(sandbox, None, false).await.unwrap(); let matching = runtime .store @@ -9537,11 +9861,13 @@ mod tests { // Spawn two concurrent creation attempts for the same sandbox let runtime1 = runtime.clone(); let sandbox1 = sandbox.clone(); - let handle1 = tokio::spawn(async move { runtime1.create_sandbox(sandbox1, None).await }); + let handle1 = + tokio::spawn(async move { runtime1.create_sandbox(sandbox1, None, false).await }); let runtime2 = runtime.clone(); let sandbox2 = sandbox.clone(); - let handle2 = tokio::spawn(async move { runtime2.create_sandbox(sandbox2, None).await }); + let handle2 = + tokio::spawn(async move { runtime2.create_sandbox(sandbox2, None, false).await }); // Wait for both to complete let result1 = handle1.await.unwrap(); diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index f502369bcf..957a77cbac 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -25,8 +25,9 @@ use openshell_core::proto::{ DetachSandboxProviderRequest, DetachSandboxProviderResponse, EditDraftChunkRequest, EditDraftChunkResponse, ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, - ExposeServiceRequest, GatewayMessage, GetCurrentUserRequest, GetCurrentUserResponse, - GetDraftHistoryRequest, GetDraftHistoryResponse, GetDraftPolicyRequest, GetDraftPolicyResponse, + ExposeServiceRequest, FinalizeMainProcessExitRequest, FinalizeMainProcessExitResponse, + GatewayMessage, GetCurrentUserRequest, GetCurrentUserResponse, GetDraftHistoryRequest, + GetDraftHistoryResponse, GetDraftPolicyRequest, GetDraftPolicyResponse, GetGatewayConfigRequest, GetGatewayConfigResponse, GetGatewayInfoRequest, GetGatewayInfoResponse, GetProviderProfileRequest, GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse, GetProviderRequest, GetSandboxConfigRequest, @@ -695,6 +696,13 @@ impl OpenShell for OpenShellService { crate::supervisor_session::handle_report_main_process_exit(&self.state, request).await } + async fn finalize_main_process_exit( + &self, + request: Request, + ) -> Result, Status> { + crate::supervisor_session::handle_finalize_main_process_exit(&self.state, request).await + } + type RelayStreamStream = Pin> + Send + 'static>>; diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 89f8c942ea..403bece8b9 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -216,6 +216,7 @@ async fn handle_create_sandbox_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); + let await_main_process_attachment = request.await_main_process_attachment; let mut spec = request .spec .ok_or_else(|| Status::invalid_argument("spec is required"))?; @@ -365,7 +366,10 @@ async fn handle_create_sandbox_inner( None => None, }; - let sandbox = state.compute.create_sandbox(sandbox, sandbox_token).await?; + let sandbox = state + .compute + .create_sandbox(sandbox, sandbox_token, await_main_process_attachment) + .await?; info!( sandbox_id = %id, @@ -1032,7 +1036,7 @@ pub(super) async fn handle_watch_sandbox( if stop_on_terminal { let phase = SandboxPhase::try_from(sandbox.phase()) .unwrap_or(SandboxPhase::Unknown); - if phase == SandboxPhase::Ready { + if is_watch_terminal(phase) { return; } } @@ -1106,7 +1110,7 @@ pub(super) async fn handle_watch_sandbox( } if stop_on_terminal { let phase = SandboxPhase::try_from(sandbox.phase()).unwrap_or(SandboxPhase::Unknown); - if phase == SandboxPhase::Ready { + if is_watch_terminal(phase) { return; } } @@ -1179,6 +1183,13 @@ pub(super) async fn handle_watch_sandbox( Ok(Response::new(WatchSandboxStream::new(rx, producer))) } +fn is_watch_terminal(phase: SandboxPhase) -> bool { + matches!( + phase, + SandboxPhase::Ready | SandboxPhase::Completed | SandboxPhase::Stopped | SandboxPhase::Error + ) +} + // --------------------------------------------------------------------------- // Exec handler // --------------------------------------------------------------------------- @@ -1316,7 +1327,10 @@ pub(super) async fn handle_forward_tcp( let sandbox = fetch_and_authorize_sandbox(state, &principal, &init.sandbox_id).await?; - if SandboxPhase::try_from(sandbox.phase()).ok() != Some(SandboxPhase::Ready) { + // The main process may finish between minting the SSH token and opening + // its transport. Keep the relay reachable until terminal delivery is + // finalized so fast commands can attach without a readiness race. + if !sandbox_relay_reachable(state, &sandbox) { return Err(Status::failed_precondition("sandbox is not ready")); } @@ -1697,6 +1711,16 @@ pub(super) async fn handle_exec_sandbox_interactive( // SSH session handlers // --------------------------------------------------------------------------- +fn sandbox_relay_reachable(state: &ServerState, sandbox: &Sandbox) -> bool { + let phase = SandboxPhase::try_from(sandbox.phase()).ok(); + matches!(phase, Some(SandboxPhase::Ready)) + || (matches!(phase, Some(SandboxPhase::Completed | SandboxPhase::Error)) + && state.supervisor_sessions.has_session(sandbox.object_id()) + && !state + .supervisor_sessions + .terminal_delivery_finalized(sandbox.object_id())) +} + pub(super) async fn handle_create_ssh_session( state: &Arc, request: Request, @@ -1709,7 +1733,7 @@ pub(super) async fn handle_create_ssh_session( let sandbox = fetch_and_authorize_sandbox(state, &principal, &req.sandbox_id).await?; - if SandboxPhase::try_from(sandbox.phase()).ok() != Some(SandboxPhase::Ready) { + if !sandbox_relay_reachable(state, &sandbox) { return Err(Status::failed_precondition("sandbox is not ready")); } @@ -2458,6 +2482,30 @@ mod tests { // ---- shell_escape ---- + #[test] + fn watch_terminal_phases_include_command_results_and_errors() { + for phase in [ + SandboxPhase::Ready, + SandboxPhase::Completed, + SandboxPhase::Stopped, + SandboxPhase::Error, + ] { + assert!(is_watch_terminal(phase), "{phase:?} should stop the watch"); + } + for phase in [ + SandboxPhase::Provisioning, + SandboxPhase::Starting, + SandboxPhase::Stopping, + SandboxPhase::Deleting, + SandboxPhase::Unknown, + ] { + assert!( + !is_watch_terminal(phase), + "{phase:?} should keep the watch open" + ); + } + } + #[test] fn telemetry_compute_driver_uses_resolved_driver_kind() { assert_eq!( @@ -3361,6 +3409,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + await_main_process_attachment: false, }), ) .await @@ -3384,6 +3433,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + await_main_process_attachment: false, }), ) .await @@ -3419,6 +3469,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + await_main_process_attachment: false, }), ) .await @@ -3443,6 +3494,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::from([(annotation_key.clone(), annotation_value.clone())]), workspace: String::new(), + await_main_process_attachment: false, }), ) .await @@ -3503,6 +3555,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + await_main_process_attachment: false, }), ) .await @@ -3568,6 +3621,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + await_main_process_attachment: false, }), ) .await @@ -3598,6 +3652,7 @@ mod tests { labels: HashMap::from([("team".to_string(), "x".repeat(512))]), annotations: HashMap::new(), workspace: String::new(), + await_main_process_attachment: false, }), ) .await @@ -3630,6 +3685,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + await_main_process_attachment: false, }), ) .await @@ -3926,6 +3982,40 @@ mod tests { assert!(session2.is_some()); } + #[tokio::test] + async fn create_ssh_session_allows_terminal_sandbox_while_supervisor_is_reachable() { + let state = test_server_state().await; + let mut sandbox = test_sandbox("work", Vec::new()); + sandbox.set_phase(SandboxPhase::Completed as i32); + state.store.put_message(&sandbox).await.unwrap(); + + let (tx, _rx) = mpsc::channel(1); + let (shutdown_tx, _shutdown_rx) = oneshot::channel(); + let _ = state.supervisor_sessions.register( + "sandbox-work".to_string(), + "session-1".to_string(), + tx, + shutdown_tx, + ); + + let response = handle_create_ssh_session( + &state, + authed_request(CreateSshSessionRequest { + sandbox_id: "sandbox-work".to_string(), + }), + ) + .await; + + assert!(response.is_ok()); + + assert!( + state + .supervisor_sessions + .finalize_main_process_exit("sandbox-work") + ); + assert!(!sandbox_relay_reachable(&state, &sandbox)); + } + #[tokio::test] async fn concurrent_revoke_ssh_session_handles_cas_properly() { let state = test_server_state().await; diff --git a/crates/openshell-server/src/supervisor_session.rs b/crates/openshell-server/src/supervisor_session.rs index ed8dbdb958..c8491dc1eb 100644 --- a/crates/openshell-server/src/supervisor_session.rs +++ b/crates/openshell-server/src/supervisor_session.rs @@ -58,6 +58,9 @@ struct LiveSession { /// the old session's `tx` just before supersede could still enqueue a /// `RelayOpen` onto the stale stream and sit until the relay timeout. shutdown: oneshot::Sender<()>, + /// Set after the supervisor confirms that every expected foreground + /// attachment has closed and terminal output delivery is complete. + terminal_delivery_finalized: bool, #[allow(dead_code)] connected_at: Instant, } @@ -125,6 +128,7 @@ impl SupervisorSessionRegistry { session_id, tx, shutdown, + terminal_delivery_finalized: false, connected_at: Instant::now(), }, ); @@ -163,15 +167,17 @@ impl SupervisorSessionRegistry { /// This guards against the supersede race: an old session's task may /// finish long after a new session has taken its place. The old task's /// cleanup must not evict the new registration. - fn remove_if_current(&self, sandbox_id: &str, session_id: &str) -> bool { + fn remove_if_current(&self, sandbox_id: &str, session_id: &str) -> Option { let mut sessions = self.sessions.lock().unwrap(); let is_current = sessions .get(sandbox_id) .is_some_and(|s| s.session_id == session_id); if is_current { - sessions.remove(sandbox_id); + return sessions + .remove(sandbox_id) + .map(|session| session.terminal_delivery_finalized); } - is_current + None } /// Look up the sender for a supervisor session, waiting up to `timeout` @@ -210,6 +216,23 @@ impl SupervisorSessionRegistry { self.sessions.lock().unwrap().contains_key(sandbox_id) } + pub fn terminal_delivery_finalized(&self, sandbox_id: &str) -> bool { + self.sessions + .lock() + .unwrap() + .get(sandbox_id) + .is_some_and(|session| session.terminal_delivery_finalized) + } + + pub fn finalize_main_process_exit(&self, sandbox_id: &str) -> bool { + let mut sessions = self.sessions.lock().unwrap(); + let Some(session) = sessions.get_mut(sandbox_id) else { + return false; + }; + session.terminal_delivery_finalized = true; + true + } + pub fn is_current_session(&self, sandbox_id: &str, session_id: &str) -> bool { self.sessions .lock() @@ -797,17 +820,17 @@ pub async fn handle_connect_supervisor( shutdown_rx, ) .await; - let still_ours = state_clone + let terminal_finalized = state_clone .supervisor_sessions .remove_if_current(&sandbox_id_clone, &session_id); - if still_ours { + if let Some(terminal_finalized) = terminal_finalized { info!(sandbox_id = %sandbox_id_clone, session_id = %session_id, "supervisor session: ended"); state_clone .telemetry .sandbox_session_disconnected(&sandbox_id_clone); if let Err(err) = state_clone .compute - .supervisor_session_disconnected(&sandbox_id_clone) + .supervisor_session_disconnected(&sandbox_id_clone, terminal_finalized) .await { warn!( @@ -848,12 +871,45 @@ pub async fn handle_report_main_process_exit( } state .compute - .main_process_exited(&report.sandbox_id, &report.instance_id, report.exit_code) + .report_main_process_exit(&report.sandbox_id, &report.instance_id, report.exit_code) .await .map_err(Status::failed_precondition)?; Ok(Response::new(ReportMainProcessExitResponse {})) } +pub async fn handle_finalize_main_process_exit( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = request.extensions().get::().cloned(); + let report = request.into_inner(); + if report.sandbox_id.is_empty() { + return Err(Status::invalid_argument("sandbox_id is required")); + } + if report.instance_id.is_empty() { + return Err(Status::invalid_argument("instance_id is required")); + } + if let Some(principal) = principal.as_ref() { + crate::auth::guard::ensure_sandbox_principal_scope(principal, &report.sandbox_id)?; + } + state + .compute + .finalize_main_process_exit(&report.sandbox_id, &report.instance_id) + .await + .map_err(Status::failed_precondition)?; + if !state + .supervisor_sessions + .finalize_main_process_exit(&report.sandbox_id) + { + return Err(Status::failed_precondition( + "supervisor session is not connected", + )); + } + Ok(Response::new( + openshell_core::proto::FinalizeMainProcessExitResponse {}, + )) +} + async fn run_session_loop( state: &Arc, sandbox_id: &str, @@ -1108,7 +1164,7 @@ mod tests { let (tx, _rx) = mpsc::channel(1); registry.register("sbx".to_string(), "s1".to_string(), tx, make_shutdown()); - assert!(registry.remove_if_current("sbx", "s1")); + assert_eq!(registry.remove_if_current("sbx", "s1"), Some(false)); assert!(!registry.sessions.lock().unwrap().contains_key("sbx")); } @@ -1134,7 +1190,7 @@ mod tests { // Cleanup from the old session task runs late. It must NOT evict the // newly registered session. - assert!(!registry.remove_if_current("sbx", "s-old")); + assert_eq!(registry.remove_if_current("sbx", "s-old"), None); let sessions = registry.sessions.lock().unwrap(); assert!( sessions.contains_key("sbx"), @@ -1146,7 +1202,18 @@ mod tests { #[test] fn remove_if_current_unknown_sandbox_is_noop() { let registry = SupervisorSessionRegistry::new(); - assert!(!registry.remove_if_current("sbx-does-not-exist", "s1")); + assert_eq!(registry.remove_if_current("sbx-does-not-exist", "s1"), None); + } + + #[test] + fn remove_if_current_returns_terminal_finalization_state() { + let registry = SupervisorSessionRegistry::new(); + let (tx, _rx) = mpsc::channel(1); + registry.register("sbx".to_string(), "s1".to_string(), tx, make_shutdown()); + + assert!(registry.finalize_main_process_exit("sbx")); + assert!(registry.terminal_delivery_finalized("sbx")); + assert_eq!(registry.remove_if_current("sbx", "s1"), Some(true)); } // ---- open_relay: happy path and wait semantics ---- diff --git a/crates/openshell-server/tests/common/mod.rs b/crates/openshell-server/tests/common/mod.rs index 96b620a230..9e3957abb6 100644 --- a/crates/openshell-server/tests/common/mod.rs +++ b/crates/openshell-server/tests/common/mod.rs @@ -60,6 +60,13 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("not used by this test server")) } + async fn finalize_main_process_exit( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this test server")) + } + async fn get_current_user( &self, _request: tonic::Request, diff --git a/crates/openshell-server/tests/supervisor_relay_integration.rs b/crates/openshell-server/tests/supervisor_relay_integration.rs index 32ef513adb..6cfd3b009b 100644 --- a/crates/openshell-server/tests/supervisor_relay_integration.rs +++ b/crates/openshell-server/tests/supervisor_relay_integration.rs @@ -55,6 +55,13 @@ impl OpenShell for RelayGateway { Err(Status::unimplemented("not used by this test server")) } + async fn finalize_main_process_exit( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("not used by this test server")) + } + async fn get_current_user( &self, _request: tonic::Request, diff --git a/crates/openshell-supervisor-process/src/main_session.rs b/crates/openshell-supervisor-process/src/main_session.rs index fc5c9693e6..00dd2ea53c 100644 --- a/crates/openshell-supervisor-process/src/main_session.rs +++ b/crates/openshell-supervisor-process/src/main_session.rs @@ -54,6 +54,8 @@ struct OutputLogState { struct OutputLog { state: Mutex, version: watch::Sender, + terminal_reported: std::sync::atomic::AtomicBool, + terminal_reported_notify: Notify, } impl OutputLog { @@ -66,6 +68,8 @@ impl OutputLog { next_sequence: 0, }), version, + terminal_reported: std::sync::atomic::AtomicBool::new(false), + terminal_reported_notify: Notify::new(), }) } @@ -106,6 +110,20 @@ impl OutputLog { } } +#[derive(Debug)] +struct TerminalAttachmentState { + active: usize, + process_finished: bool, + expectation: AttachmentExpectation, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum AttachmentExpectation { + None, + Pending, + Satisfied, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct MainOutputLagged { pub skipped: u64, @@ -170,6 +188,9 @@ pub struct MainSession { pty_master: Option>, readers_remaining: AtomicUsize, readers_done: Notify, + finished: std::sync::atomic::AtomicBool, + terminal_attachments: Mutex, + terminal_attachments_done: Notify, } impl MainSession { @@ -186,6 +207,13 @@ impl MainSession { pty_master: None, readers_remaining: AtomicUsize::new(0), readers_done: Notify::new(), + finished: std::sync::atomic::AtomicBool::new(false), + terminal_attachments: Mutex::new(TerminalAttachmentState { + active: 0, + process_finished: false, + expectation: AttachmentExpectation::None, + }), + terminal_attachments_done: Notify::new(), }) } @@ -230,6 +258,13 @@ impl MainSession { pty_master, readers_remaining: AtomicUsize::new(if terminal { 1 } else { 2 }), readers_done: Notify::new(), + finished: std::sync::atomic::AtomicBool::new(false), + terminal_attachments: Mutex::new(TerminalAttachmentState { + active: 0, + process_finished: false, + expectation: AttachmentExpectation::None, + }), + terminal_attachments_done: Notify::new(), }); Self::start_io(&session, io, input_rx); session @@ -340,18 +375,113 @@ impl MainSession { } } - pub async fn finish(&self, exit_code: i32) { + /// Publish the terminal event and retain the transport only when a real + /// foreground attachment exists or the creating client declared one. + /// + /// Returns whether terminal delivery must complete before shutdown. + pub async fn finish(&self, exit_code: i32, attachment_expected: bool) -> bool { let notified = self.readers_done.notified(); if self.readers_remaining.load(Ordering::Acquire) != 0 { - let _ = tokio::time::timeout(std::time::Duration::from_secs(2), notified).await; + notified.await; } + let delivery_pending = { + let mut state = self + .terminal_attachments + .lock() + .expect("terminal attachment lock poisoned"); + state.process_finished = true; + state.expectation = if attachment_expected { + if state.active == 0 && state.expectation != AttachmentExpectation::Satisfied { + AttachmentExpectation::Pending + } else { + AttachmentExpectation::Satisfied + } + } else { + AttachmentExpectation::None + }; + attachment_expected || state.active != 0 + }; + self.finished.store(true, Ordering::Release); self.publish(MainOutput::Exit(exit_code)); + delivery_pending } pub fn subscribe(&self) -> MainOutputCursor { self.output.subscribe() } + /// Wait until the gateway durably acknowledges the main-process result. + pub async fn wait_for_terminal_reported(&self) { + let notified = self.output.terminal_reported_notify.notified(); + if self.output.terminal_reported.load(Ordering::Acquire) { + return; + } + notified.await; + } + + /// Release attached clients to receive their SSH exit status after the + /// durable sandbox phase and exit code have been recorded. + pub fn mark_terminal_reported(&self) { + self.output.terminal_reported.store(true, Ordering::Release); + self.output.terminal_reported_notify.notify_waiters(); + } + + /// Register a foreground main attachment while the process is live. + pub fn begin_terminal_attachment(&self) -> Result<(), &'static str> { + let mut state = self + .terminal_attachments + .lock() + .expect("terminal attachment lock poisoned"); + if state.process_finished && state.expectation != AttachmentExpectation::Pending { + return Err("canonical main process already finished"); + } + state.active = state + .active + .checked_add(1) + .expect("terminal attachment count exhausted"); + state.expectation = AttachmentExpectation::Satisfied; + self.terminal_attachments_done.notify_waiters(); + Ok(()) + } + + /// Release a foreground main attachment after its SSH channel closes. + pub fn end_terminal_attachment(&self) { + let completed = { + let mut state = self + .terminal_attachments + .lock() + .expect("terminal attachment lock poisoned"); + debug_assert!(state.active != 0, "terminal attachment count underflow"); + if state.active == 0 { + return; + } + state.active -= 1; + state.active == 0 + }; + if completed { + self.terminal_attachments_done.notify_waiters(); + } + } + + /// Wait for the declared foreground attachment to start, then for every + /// accepted attachment to close naturally. + pub async fn wait_for_terminal_attachments(&self) { + loop { + let notified = self.terminal_attachments_done.notified(); + let complete = { + let state = self + .terminal_attachments + .lock() + .expect("terminal attachment lock poisoned"); + state.active == 0 && state.expectation != AttachmentExpectation::Pending + }; + if complete { + return; + } + notified.await; + } + } + pub fn acquire_input(&self) -> Result<(u64, tokio::sync::mpsc::Sender>), &'static str> { let mut owner = self.input_owner.lock().expect("main input lock poisoned"); if owner.is_some() { @@ -394,6 +524,11 @@ impl MainSession { pub const fn terminal(&self) -> bool { self.terminal } + + #[must_use] + pub fn finished(&self) -> bool { + self.finished.load(Ordering::Acquire) + } } fn set_nonblocking(file: &std::fs::File) -> Result<(), nix::errno::Errno> { @@ -440,9 +575,94 @@ mod tests { } #[tokio::test] - async fn exit_is_replayed_once_to_late_subscribers() { + async fn finish_without_attachment_does_not_defer_shutdown() { + let session = MainSession::inert(); + assert!(!session.finish(0, false).await); + assert!(session.finished()); + assert!(session.begin_terminal_attachment().is_err()); + } + + #[tokio::test] + async fn terminal_report_acknowledgement_is_independent_from_delivery() { + let session = MainSession::inert(); + + assert!( + tokio::time::timeout( + std::time::Duration::from_millis(10), + session.wait_for_terminal_reported(), + ) + .await + .is_err(), + "draining output must not imply durable gateway persistence" + ); + + session.mark_terminal_reported(); + tokio::time::timeout( + std::time::Duration::from_secs(1), + session.wait_for_terminal_reported(), + ) + .await + .expect("durable report acknowledgement should wake waiter"); + } + + #[tokio::test] + async fn finish_waits_for_an_active_attachment_to_close_naturally() { + let session = MainSession::inert(); + session + .begin_terminal_attachment() + .expect("begin terminal attachment"); + assert!(session.finish(0, false).await); + + assert!( + tokio::time::timeout( + std::time::Duration::from_millis(10), + session.wait_for_terminal_attachments(), + ) + .await + .is_err(), + "an active attachment must keep terminal delivery open" + ); + + session.end_terminal_attachment(); + tokio::time::timeout( + std::time::Duration::from_secs(1), + session.wait_for_terminal_attachments(), + ) + .await + .expect("closing the attachment should wake the waiter"); + } + + #[tokio::test] + async fn declared_attachment_waits_for_connection_then_natural_close() { + let session = MainSession::inert(); + assert!(session.finish(0, true).await); + + assert!( + tokio::time::timeout( + std::time::Duration::from_millis(10), + session.wait_for_terminal_attachments(), + ) + .await + .is_err(), + "declared attachment must connect before delivery is complete" + ); + + session + .begin_terminal_attachment() + .expect("declared post-exit attachment"); + session.end_terminal_attachment(); + tokio::time::timeout( + std::time::Duration::from_secs(1), + session.wait_for_terminal_attachments(), + ) + .await + .expect("natural attachment close should complete delivery"); + } + + #[tokio::test] + async fn exit_is_retained_in_the_output_log() { let session = MainSession::inert(); - session.finish(0).await; + let _ = session.finish(0, false).await; let mut output = session.subscribe(); assert!(matches!( diff --git a/crates/openshell-supervisor-process/src/run.rs b/crates/openshell-supervisor-process/src/run.rs index f0e792306d..b2820d9588 100644 --- a/crates/openshell-supervisor-process/src/run.rs +++ b/crates/openshell-supervisor-process/src/run.rs @@ -39,11 +39,17 @@ use crate::process::{ ResolvedWorkspace, }; -pub type SidecarExitReport = ( - String, - i32, - tokio::sync::oneshot::Sender>, -); +pub enum SidecarExitReport { + Exited { + instance_id: String, + exit_code: i32, + ack: tokio::sync::oneshot::Sender>, + }, + Finalized { + instance_id: String, + ack: tokio::sync::oneshot::Sender>, + }, +} fn ocsf_ctx() -> &'static openshell_ocsf::SandboxContext { openshell_ocsf::ctx::ctx() @@ -63,6 +69,7 @@ pub async fn run_process( workspace: ResolvedWorkspace, timeout_secs: u64, interactive: bool, + await_main_process_attachment: bool, sandbox_id: Option<&str>, openshell_endpoint: Option<&str>, ssh_socket_path: Option, @@ -322,9 +329,9 @@ pub async fn run_process( } }); - // Wait for the SSH server to bind its socket before spawning the - // entrypoint process. This prevents exec requests from racing against - // SSH server startup when Kubernetes marks the pod Ready. + // Wait for the SSH server to bind before advertising its relay. The + // main process is already supervised; MainSession retains any output + // produced while this endpoint is being prepared. match timeout(Duration::from_secs(10), ssh_ready_rx).await { Ok(Ok(Ok(()))) => { ocsf_emit!( @@ -354,16 +361,14 @@ pub async fn run_process( let supervisor_terminating = Arc::new(AtomicBool::new(false)); // A canonical process may have completed while the SSH socket was being - // prepared. Never open a readiness-bearing supervisor session for a child - // that is already terminal. + // prepared. Detect that exit before entering the main wait path. let early_exit = handle.try_wait().into_diagnostic()?; // Spawn the persistent supervisor session if we have a gateway endpoint // and sandbox identity. The session provides relay channels for SSH // connect and ExecSandbox through the gateway. - let supervisor_session_task = if early_exit.is_none() - && let (Some(endpoint), Some(id), Some(socket)) = - (openshell_endpoint, sandbox_id, ssh_socket_path.as_ref()) + let supervisor_session_task = if let (Some(endpoint), Some(id), Some(socket)) = + (openshell_endpoint, sandbox_id, ssh_socket_path.as_ref()) { let task = crate::supervisor_session::spawn( endpoint.to_string(), @@ -382,9 +387,7 @@ pub async fn run_process( // Store the entrypoint PID so the proxy can resolve TCP peer identity entrypoint_pid.store(handle.pid(), Ordering::Release); - if early_exit.is_none() - && let Some(tx) = entrypoint_started_tx - { + if let Some(tx) = entrypoint_started_tx { let _ = tx.send((handle.pid(), main_instance_id.clone())); } ocsf_emit!( @@ -407,8 +410,8 @@ pub async fn run_process( .await? }; - let rendered_code = match outcome { - ProcessWaitOutcome::Exited(status) => status.code(), + let (rendered_code, drain_terminal) = match outcome { + ProcessWaitOutcome::Exited(status) => (status.code(), true), ProcessWaitOutcome::TimedOut => { ocsf_emit!( ProcessActivityBuilder::new(ocsf_ctx()) @@ -420,7 +423,7 @@ pub async fn run_process( .message("Process timed out, killing") .build() ); - 124 + (124, false) } ProcessWaitOutcome::ShutdownSignal { signal, status } => { info!( @@ -428,11 +431,15 @@ pub async fn run_process( exit_code = status.code(), "Entrypoint exited after supervisor shutdown signal" ); - status.code() + (status.code(), false) } }; - supervisor_terminating.store(true, Ordering::Release); - main_session.finish(rendered_code).await; + let terminal_delivery_pending = main_session + .finish( + rendered_code, + drain_terminal && await_main_process_attachment, + ) + .await; ocsf_emit!( ProcessActivityBuilder::new(ocsf_ctx()) @@ -446,22 +453,30 @@ pub async fn run_process( .build() ); - if let Some(task) = supervisor_session_task { - task.abort(); - } - if let Some(tx) = sidecar_exit_tx { - let (ack_tx, ack_rx) = tokio::sync::oneshot::channel(); - tx.send((main_instance_id.clone(), rendered_code, ack_tx)) - .await - .map_err(|_| miette::miette!("sidecar exit reporter closed"))?; - ack_rx - .await - .map_err(|_| miette::miette!("sidecar exit reporter dropped acknowledgement"))? - .map_err(|error| miette::miette!(error))?; + if let Some(tx) = sidecar_exit_tx.as_ref() { + report_sidecar_main_process_exit(tx, &main_instance_id, rendered_code).await?; } else if let (Some(endpoint), Some(id)) = (openshell_endpoint, sandbox_id) { report_main_process_exit_until_ack(endpoint, id, &main_instance_id, rendered_code).await; info!(instance_id = %main_instance_id, "main-process exit acknowledged"); } + main_session.mark_terminal_reported(); + if drain_terminal && terminal_delivery_pending { + // The peer's SSH channel-close confirms that the terminal frames sent + // above traversed russh and the relay. Detached commands have no active + // attachment and never enter this wait. + main_session.wait_for_terminal_attachments().await; + } + if let Some(tx) = sidecar_exit_tx.as_ref() { + finalize_sidecar_main_process_exit(tx, &main_instance_id).await?; + } else if let (Some(endpoint), Some(id)) = (openshell_endpoint, sandbox_id) { + finalize_main_process_exit_until_ack(endpoint, id, &main_instance_id).await; + info!(instance_id = %main_instance_id, "main-process terminal delivery finalized"); + } + + supervisor_terminating.store(true, Ordering::Release); + if let Some(task) = supervisor_session_task { + task.abort(); + } Ok(rendered_code) } @@ -492,6 +507,62 @@ async fn report_main_process_exit_until_ack( } } +async fn finalize_main_process_exit_until_ack(endpoint: &str, sandbox_id: &str, instance_id: &str) { + let mut retry_delay = Duration::from_millis(250); + loop { + match crate::supervisor_session::finalize_main_process_exit( + endpoint, + sandbox_id, + instance_id, + ) + .await + { + Ok(()) => return, + Err(error) => { + tracing::warn!(%error, "main-process terminal finalization failed; retrying"); + tokio::time::sleep(retry_delay).await; + retry_delay = (retry_delay * 2).min(Duration::from_secs(2)); + } + } + } +} + +async fn report_sidecar_main_process_exit( + tx: &tokio::sync::mpsc::Sender, + instance_id: &str, + exit_code: i32, +) -> Result<()> { + let (ack_tx, ack_rx) = tokio::sync::oneshot::channel(); + tx.send(SidecarExitReport::Exited { + instance_id: instance_id.to_string(), + exit_code, + ack: ack_tx, + }) + .await + .map_err(|_| miette::miette!("sidecar exit reporter closed"))?; + ack_rx + .await + .map_err(|_| miette::miette!("sidecar exit reporter dropped acknowledgement"))? + .map_err(|error| miette::miette!(error)) +} + +async fn finalize_sidecar_main_process_exit( + tx: &tokio::sync::mpsc::Sender, + instance_id: &str, +) -> Result<()> { + let (ack_tx, ack_rx) = tokio::sync::oneshot::channel(); + tx.send(SidecarExitReport::Finalized { + instance_id: instance_id.to_string(), + ack: ack_tx, + }) + .await + .map_err(|_| miette::miette!("sidecar exit reporter closed"))?; + ack_rx + .await + .map_err(|_| miette::miette!("sidecar exit reporter dropped acknowledgement"))? + .map_err(|error| miette::miette!(error)) +} + enum ProcessWaitOutcome { Exited(ProcessStatus), TimedOut, @@ -515,7 +586,6 @@ async fn wait_for_process_exit_or_shutdown( tokio::pin!(deadline); tokio::select! { result = &mut wait => { - terminating.store(true, Ordering::Release); Ok(ProcessWaitOutcome::Exited(result.into_diagnostic()?)) } () = &mut deadline => { @@ -533,7 +603,6 @@ async fn wait_for_process_exit_or_shutdown( } else { tokio::select! { result = &mut wait => { - terminating.store(true, Ordering::Release); Ok(ProcessWaitOutcome::Exited(result.into_diagnostic()?)) } signal = wait_for_supervisor_shutdown_signal() => { diff --git a/crates/openshell-supervisor-process/src/ssh.rs b/crates/openshell-supervisor-process/src/ssh.rs index 893967b2ac..12daff8d0c 100644 --- a/crates/openshell-supervisor-process/src/ssh.rs +++ b/crates/openshell-supervisor-process/src/ssh.rs @@ -460,6 +460,10 @@ struct SshHandler { impl Drop for SshHandler { fn drop(&mut self) { for state in self.channels.values_mut() { + if state.main_attached { + self.main_session.end_terminal_attachment(); + state.main_attached = false; + } if let Some(owner) = state.main_input_owner.take() { self.main_session.release_input(owner); } @@ -537,6 +541,9 @@ impl russh::server::Handler for SshHandler { _session: &mut Session, ) -> Result<(), Self::Error> { if let Some(state) = self.channels.remove(&channel) { + if state.main_attached { + self.main_session.end_terminal_attachment(); + } if let Some(owner) = state.main_input_owner { self.main_session.release_input(owner); } @@ -557,6 +564,12 @@ impl russh::server::Handler for SshHandler { reply: ChannelOpenHandle, _session: &mut Session, ) -> Result<(), Self::Error> { + if self.main_session.finished() { + reply + .reject(ChannelOpenFailure::AdministrativelyProhibited) + .await; + return Ok(()); + } // Validate port range before truncating u32 -> u16. The SSH protocol // uses u32 for ports, but valid TCP ports are 0-65535. Without this // check, port 65537 truncates to port 1 (privileged). @@ -692,6 +705,10 @@ impl russh::server::Handler for SshHandler { channel: ChannelId, session: &mut Session, ) -> Result<(), Self::Error> { + if self.main_session.finished() { + session.channel_failure(channel)?; + return Ok(()); + } session.channel_success(channel)?; // Only allocate a PTY when the client explicitly requested one via // pty_request. VS Code Remote-SSH sends shell_request *without* a @@ -709,6 +726,10 @@ impl russh::server::Handler for SshHandler { data: &[u8], session: &mut Session, ) -> Result<(), Self::Error> { + if self.main_session.finished() { + session.channel_failure(channel)?; + return Ok(()); + } session.channel_success(channel)?; let command = String::from_utf8_lossy(data).trim().to_string(); if command.is_empty() { @@ -725,9 +746,20 @@ impl russh::server::Handler for SshHandler { session: &mut Session, ) -> Result<(), Self::Error> { if name == "openshell-main" { - let state = self.channels.get_mut(&channel).ok_or_else(|| { - anyhow::anyhow!("subsystem_request on unknown channel {channel:?}") - })?; + if !self.channels.contains_key(&channel) { + return Err(anyhow::anyhow!( + "subsystem_request on unknown channel {channel:?}" + )); + } + if self.main_session.begin_terminal_attachment().is_err() { + session.channel_failure(channel)?; + return Ok(()); + } + let state = self + .channels + .get_mut(&channel) + .expect("main channel existence checked above"); + state.main_attached = true; if let Some(pty) = state.pty_request.take() { self.main_session.resize( pty.col_width, @@ -750,10 +782,10 @@ impl russh::server::Handler for SshHandler { } } }; - state.main_attached = true; state.main_detach_prefix_pending = false; state.input_sender = input; let mut output = self.main_session.subscribe(); + let terminal_delivery = Arc::clone(&self.main_session); let handle = session.handle(); session.channel_success(channel)?; if let Some(error) = input_warning { @@ -769,11 +801,13 @@ impl russh::server::Handler for SshHandler { loop { match output.recv().await { Ok(event) => { - let exited = matches!(event, MainOutput::Exit(_)); - send_main_output(&handle, channel, event).await; - if exited { + if let MainOutput::Exit(code) = event { + terminal_delivery.wait_for_terminal_reported().await; + let _ = send_main_output(&handle, channel, MainOutput::Exit(code)) + .await; break; } + let _ = send_main_output(&handle, channel, event).await; } Err(error) => { let _ = handle @@ -796,7 +830,7 @@ impl russh::server::Handler for SshHandler { if let Some(state) = self.channels.get_mut(&channel) { state.main_output_task = Some(output_task.abort_handle()); } - } else if name == "sftp" { + } else if name == "sftp" && !self.main_session.finished() { session.channel_success(channel)?; // sftp-server speaks the SFTP binary protocol over stdin/stdout, // which is exactly what spawn_pipe_exec wires up. This enables @@ -954,20 +988,18 @@ impl russh::server::Handler for SshHandler { } } -async fn send_main_output(handle: &Handle, channel: ChannelId, event: MainOutput) { +async fn send_main_output(handle: &Handle, channel: ChannelId, event: MainOutput) -> bool { match event { - MainOutput::Stdout(data) => { - let _ = handle.data(channel, data).await; - } - MainOutput::Stderr(data) => { - let _ = handle.extended_data(channel, 1, data).await; - } + MainOutput::Stdout(data) => handle.data(channel, data).await.is_ok(), + MainOutput::Stderr(data) => handle.extended_data(channel, 1, data).await.is_ok(), MainOutput::Exit(code) => { - let _ = handle.eof(channel).await; - let _ = handle + let eof_sent = handle.eof(channel).await.is_ok(); + let status_sent = handle .exit_status_request(channel, code.max(0).unsigned_abs()) - .await; - let _ = handle.close(channel).await; + .await + .is_ok(); + let close_sent = handle.close(channel).await.is_ok(); + eof_sent && status_sent && close_sent } } } @@ -980,6 +1012,10 @@ impl SshHandler { error: Option<&str>, ) { if let Some(state) = self.channels.get_mut(&channel) { + if state.main_attached { + self.main_session.end_terminal_attachment(); + state.main_attached = false; + } if let Some(owner) = state.main_input_owner.take() { self.main_session.release_input(owner); } @@ -988,7 +1024,6 @@ impl SshHandler { if let Some(task) = state.main_output_task.take() { task.abort(); } - state.main_attached = false; } if let Some(error) = error { let _ = handle @@ -2521,6 +2556,59 @@ mod tests { .expect("handler drop should release canonical input lease"); } + #[tokio::test] + async fn main_attachment_closes_naturally_after_terminal_delivery() { + let main_session = MainSession::inert(); + let client = authenticated_test_client_with_main(Arc::clone(&main_session)).await; + let mut channel = client.channel_open_session().await.expect("open session"); + channel + .request_subsystem(true, "openshell-main") + .await + .expect("attach main subsystem"); + tokio::time::timeout(Duration::from_secs(1), async { + loop { + match main_session.acquire_input() { + Err(_) => break, + Ok((owner, _)) => main_session.release_input(owner), + } + tokio::task::yield_now().await; + } + }) + .await + .expect("main subsystem should register its attachment"); + + assert!(main_session.finish(7, false).await); + main_session.mark_terminal_reported(); + + let exit_status = tokio::time::timeout(Duration::from_secs(1), async { + let mut exit_status = None; + loop { + match channel.wait().await { + Some(russh::ChannelMsg::ExitStatus { + exit_status: status, + }) => { + exit_status = Some(status); + } + Some(russh::ChannelMsg::Close) => break exit_status, + None => panic!("main channel ended without a close message"), + Some(_) => {} + } + } + }) + .await + .expect("main channel should deliver its exit status"); + assert_eq!(exit_status, Some(7)); + drop(channel); + drop(client); + + tokio::time::timeout( + Duration::from_secs(1), + main_session.wait_for_terminal_attachments(), + ) + .await + .expect("peer channel close should release terminal delivery"); + } + #[tokio::test] async fn main_subsystem_applies_initial_pty_dimensions() { let (main_session, _slave) = MainSession::terminal_for_test(); diff --git a/crates/openshell-supervisor-process/src/supervisor_session.rs b/crates/openshell-supervisor-process/src/supervisor_session.rs index e8a140e483..98a3c0497b 100644 --- a/crates/openshell-supervisor-process/src/supervisor_session.rs +++ b/crates/openshell-supervisor-process/src/supervisor_session.rs @@ -19,9 +19,9 @@ use std::time::Duration; use openshell_core::proto::open_shell_client::OpenShellClient; use openshell_core::proto::{ - GatewayMessage, RelayFrame, RelayInit, RelayOpen, RelayOpenResult, - ReportMainProcessExitRequest, SupervisorHeartbeat, SupervisorHello, SupervisorMessage, - TcpRelayTarget, gateway_message, relay_open, supervisor_message, + FinalizeMainProcessExitRequest, GatewayMessage, RelayFrame, RelayInit, RelayOpen, + RelayOpenResult, ReportMainProcessExitRequest, SupervisorHeartbeat, SupervisorHello, + SupervisorMessage, TcpRelayTarget, gateway_message, relay_open, supervisor_message, }; use openshell_ocsf::{ ActivityId, ConnectionInfo, Endpoint, NetworkActivityBuilder, OcsfEvent, SandboxContext, @@ -283,7 +283,7 @@ pub fn spawn( terminating: Arc, instance_id: String, ) -> tokio::task::JoinHandle<()> { - tokio::spawn(run_session_loop( + let config = SessionConfig { endpoint, sandbox_id, ssh_socket_path, @@ -291,10 +291,11 @@ pub fn spawn( expected_ssh_peer_pid, terminating, instance_id, - )) + }; + tokio::spawn(run_session_loop(config)) } -async fn run_session_loop( +struct SessionConfig { endpoint: String, sandbox_id: String, ssh_socket_path: std::path::PathBuf, @@ -302,34 +303,29 @@ async fn run_session_loop( expected_ssh_peer_pid: Option, terminating: Arc, instance_id: String, -) { +} + +async fn run_session_loop(config: SessionConfig) { let mut backoff = INITIAL_BACKOFF; let mut attempt: u64 = 0; loop { attempt += 1; - match run_single_session( - &endpoint, - &sandbox_id, - &ssh_socket_path, - netns_fd, - expected_ssh_peer_pid, - Arc::clone(&terminating), - &instance_id, - ) - .await - { + match run_single_session(&config).await { Ok(()) => { - let event = - session_closed_event(openshell_ocsf::ctx::ctx(), &endpoint, &sandbox_id); + let event = session_closed_event( + openshell_ocsf::ctx::ctx(), + &config.endpoint, + &config.sandbox_id, + ); ocsf_emit!(event); break; } Err(e) => { let event = session_failed_event( openshell_ocsf::ctx::ctx(), - &endpoint, + &config.endpoint, attempt, &e.to_string(), ); @@ -342,19 +338,13 @@ async fn run_session_loop( } async fn run_single_session( - endpoint: &str, - sandbox_id: &str, - ssh_socket_path: &std::path::Path, - netns_fd: Option, - expected_ssh_peer_pid: Option, - terminating: Arc, - instance_id: &str, + config: &SessionConfig, ) -> Result<(), Box> { // Connect to the gateway. The same `Channel` is used for both the // long-lived control stream and all data-plane `RelayStream` calls, so // every relay rides the same TCP+TLS+HTTP/2 connection — no new TLS // handshake per relay. - let channel = grpc_client::connect_channel_pub(endpoint) + let channel = grpc_client::connect_channel_pub(&config.endpoint) .await .map_err(|e| format!("connect failed: {e}"))?; let mut client = OpenShellClient::new(channel.clone()); @@ -366,8 +356,8 @@ async fn run_single_session( // Send hello as the first message. tx.send(SupervisorMessage { payload: Some(supervisor_message::Payload::Hello(SupervisorHello { - sandbox_id: sandbox_id.to_string(), - instance_id: instance_id.to_string(), + sandbox_id: config.sandbox_id.clone(), + instance_id: config.instance_id.clone(), })), }) .await @@ -397,12 +387,11 @@ async fn run_single_session( let heartbeat_secs = accepted.heartbeat_interval_secs.max(5); let event = session_established_event( openshell_ocsf::ctx::ctx(), - endpoint, + &config.endpoint, &accepted.session_id, heartbeat_secs, ); ocsf_emit!(event); - // Main loop: receive gateway messages + send heartbeats. let mut heartbeat_interval = tokio::time::interval(Duration::from_secs(u64::from(heartbeat_secs))); @@ -414,19 +403,19 @@ async fn run_single_session( let msg = match map_session_stream_message( msg, "gateway closed stream", - &terminating, + &config.terminating, )? { SessionStreamMessage::Message(msg) => msg, SessionStreamMessage::ExpectedShutdownClose => return Ok(()), }; let context = GatewayMessageContext { - sandbox_id, - ssh_socket_path, - netns_fd, - expected_ssh_peer_pid, + sandbox_id: &config.sandbox_id, + ssh_socket_path: &config.ssh_socket_path, + netns_fd: config.netns_fd, + expected_ssh_peer_pid: config.expected_ssh_peer_pid, channel: &channel, tx: &tx, - terminating: &terminating, + terminating: &config.terminating, }; handle_gateway_message( &msg, @@ -468,6 +457,25 @@ pub async fn report_main_process_exit( Ok(()) } +/// Confirm terminal delivery and permit ephemeral cleanup. +pub async fn finalize_main_process_exit( + endpoint: &str, + sandbox_id: &str, + instance_id: &str, +) -> Result<(), Box> { + let channel = grpc_client::connect_channel_pub(endpoint) + .await + .map_err(|error| format!("connect failed: {error}"))?; + let mut client = OpenShellClient::new(channel); + client + .finalize_main_process_exit(FinalizeMainProcessExitRequest { + sandbox_id: sandbox_id.to_string(), + instance_id: instance_id.to_string(), + }) + .await?; + Ok(()) +} + struct GatewayMessageContext<'a> { sandbox_id: &'a str, ssh_socket_path: &'a std::path::Path, diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index e90cee8b38..625bc8a41a 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -1407,6 +1407,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { labels: HashMap::new(), annotations: HashMap::new(), workspace: workspace.clone(), + await_main_process_attachment: false, }; let sandbox_name = @@ -2705,6 +2706,7 @@ fn phase_label(phase: i32) -> String { x if x == SandboxPhase::Stopping as i32 => "Stopping", x if x == SandboxPhase::Stopped as i32 => "Stopped", x if x == SandboxPhase::Starting as i32 => "Starting", + x if x == SandboxPhase::Completed as i32 => "Completed", _ => "Unknown", } .to_string() @@ -2766,6 +2768,7 @@ mod phase_label_tests { assert_eq!(phase_label(SandboxPhase::Stopping as i32), "Stopping"); assert_eq!(phase_label(SandboxPhase::Stopped as i32), "Stopped"); assert_eq!(phase_label(SandboxPhase::Starting as i32), "Starting"); + assert_eq!(phase_label(SandboxPhase::Completed as i32), "Completed"); } } diff --git a/crates/openshell-tui/src/ui/sandbox_detail.rs b/crates/openshell-tui/src/ui/sandbox_detail.rs index 434f369d39..4ff78617f0 100644 --- a/crates/openshell-tui/src/ui/sandbox_detail.rs +++ b/crates/openshell-tui/src/ui/sandbox_detail.rs @@ -22,7 +22,7 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect) { let age = app.sandbox_ages.get(idx).map_or("-", String::as_str); let phase_style = match phase { - "Ready" => t.status_ok, + "Ready" | "Completed" => t.status_ok, "Provisioning" | "Stopping" | "Starting" => t.status_warn, "Error" => t.status_err, _ => t.muted, @@ -30,6 +30,7 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect) { let status_indicator = match phase { "Ready" => "●", + "Completed" => "✓", "Provisioning" | "Stopping" | "Starting" => "◐", "Error" | "Stopped" => "○", _ => "…", diff --git a/crates/openshell-tui/src/ui/sandboxes.rs b/crates/openshell-tui/src/ui/sandboxes.rs index d927537189..b1d3edc1fe 100644 --- a/crates/openshell-tui/src/ui/sandboxes.rs +++ b/crates/openshell-tui/src/ui/sandboxes.rs @@ -40,7 +40,7 @@ pub fn draw(frame: &mut Frame<'_>, app: &App, area: Rect, focused: bool) { let draft_count = app.sandbox_draft_counts.get(i).copied().unwrap_or(0); let phase_style = match phase { - "Ready" => t.status_ok, + "Ready" | "Completed" => t.status_ok, "Provisioning" | "Stopping" | "Starting" => t.status_warn, "Error" => t.status_err, _ => t.muted, diff --git a/docs/reference/sandbox-compute-drivers.mdx b/docs/reference/sandbox-compute-drivers.mdx index 5773fb2bb1..e453f7f219 100644 --- a/docs/reference/sandbox-compute-drivers.mdx +++ b/docs/reference/sandbox-compute-drivers.mdx @@ -29,7 +29,9 @@ The gateway forwards one exact, persisted main-process specification to every driver. Drivers serialize that specification in `OPENSHELL_MAIN_PROCESS_SPEC`; they do not install an idle `sleep` workload or reconstruct argv with shell parsing. Runtime restart policies are disabled so -an exited canonical process remains a terminal sandbox error. +an exited canonical process remains a terminal sandbox result. Exit code zero +produces `Completed`; a nonzero or signal-normalized exit produces `Error` +with the exact exit code. Driver and supervisor failures remain `Error`. ## Configure a Compute Driver diff --git a/docs/sandboxes/manage-sandboxes.mdx b/docs/sandboxes/manage-sandboxes.mdx index e5a27879f3..b3ed9f6eaa 100644 --- a/docs/sandboxes/manage-sandboxes.mdx +++ b/docs/sandboxes/manage-sandboxes.mdx @@ -21,7 +21,9 @@ openshell sandbox create -- claude ``` The trailing command is the sandbox's canonical main process. OpenShell starts -it once and attaches your terminal to it. With no trailing command, OpenShell +it once, streams its output, and returns its exit status. Exit code 0 leaves a +retained sandbox in `Completed`; a nonzero exit leaves it in `Error` with a +`MainProcessFailed` condition. With no trailing command, OpenShell starts `/bin/bash -l` in a retained pseudo-terminal. Add `--detach` to create the sandbox without attaching: @@ -29,6 +31,19 @@ the sandbox without attaching: openshell sandbox create --name worker --detach -- ./worker ``` +Detached commands have no attachment grace period. When the command exits, +OpenShell records its terminal phase immediately. For a foreground command, +the create request declares one expected SSH attachment. OpenShell retains the +terminal transport until that connection drains and closes naturally, then +finalizes ephemeral cleanup. + +Use `--no-keep` for an ephemeral command. OpenShell drains stdout and stderr, +captures the command result, and deletes the sandbox after the command exits: + +```shell +openshell sandbox create --no-keep -- sh -c 'echo done; exit 0' +``` + `--upload` cannot yet be combined with a trailing main command because uploads finish after the canonical process starts. Create a scratch sandbox, upload the files, then launch the workload with `sandbox exec`, or build the files into the @@ -490,8 +505,11 @@ commands, transfer files, forward ports, or reach exposed services. Policies, provider attachments, settings, service definitions, and persistent workspace data remain associated with the sandbox. -Stop and start are idempotent. Delete a stopped sandbox normally when you -no longer need its retained state. +Stop and start are idempotent. You can also start a retained `Completed` or +`Error/MainProcessFailed` sandbox to launch a fresh instance of its canonical +command. Starting a fresh instance invalidates SSH sessions issued for the +previous runtime generation. Delete an inactive sandbox normally when you no +longer need its retained state. ## Delete Sandboxes @@ -510,9 +528,10 @@ Every sandbox moves through a defined set of phases: | Provisioning | The runtime is setting up the sandbox environment, or the gateway is waiting for the sandbox supervisor to establish its authenticated control session. | | Ready | The sandbox is running and its supervisor control session is connected. You can connect, execute commands, sync files, and view logs. | | Stopping | The gateway accepted a stop request and is stopping compute while retaining persistent state. | -| Stopped | Compute is stopped and access is unavailable. The sandbox record and driver-owned persistent workspace remain. | +| Stopped | Compute was stopped explicitly and access is unavailable. | | Starting | Compute is starting. The sandbox becomes usable only after a fresh supervisor session connects. | -| Error | Provisioning failed or the canonical main process exited unexpectedly. Main-process exit is terminal even with exit code 0. Check logs with `openshell logs`. | +| Completed | The canonical main process exited with code 0. Its normalized result is available in `status.exit_code`. | +| Error | The canonical main process failed, or sandbox infrastructure failed. Inspect the condition reason and `status.exit_code`. | | Deleting | The sandbox is being torn down. The system releases resources and purges credentials. | The compute backend can become ready before the sandbox supervisor connects to @@ -522,10 +541,11 @@ After a gateway restart, an existing sandbox can return to `Provisioning` temporarily while its supervisor reconnects. Wait for the phase to return to `Ready` before you connect to the sandbox or execute commands. -The gateway records a canonical main-process exit as `Ready=False` with reason -`MainProcessExited`. It also sets `status.exit_code`; signal exits use the -standard `128 + signal` convention. Compute runtimes do not automatically -restart that process. +The gateway records a successful canonical main-process exit as `Ready=False` +with reason `MainProcessCompleted`. Nonzero and signal-normalized results use +`MainProcessFailed` and the `Error` phase. It also sets `status.exit_code`; +signal exits use the standard `128 + signal` convention. Compute runtimes do +not automatically restart that process. ## Sandbox Compute Drivers diff --git a/e2e/mcp-conformance.sh b/e2e/mcp-conformance.sh index c1b46fe53a..1bbe5951ff 100755 --- a/e2e/mcp-conformance.sh +++ b/e2e/mcp-conformance.sh @@ -338,6 +338,7 @@ create_client_sandbox() { --from "${CLIENT_IMAGE}" \ --policy "${policy_file}" \ --no-tty \ + --detach \ -- sleep infinity; then rm -f "${policy_file}" return 1 diff --git a/e2e/rust/tests/oidc_pkce.rs b/e2e/rust/tests/oidc_pkce.rs index e6f6067a34..f8edc3d7b2 100644 --- a/e2e/rust/tests/oidc_pkce.rs +++ b/e2e/rust/tests/oidc_pkce.rs @@ -28,8 +28,6 @@ use url::Url; static SANDBOX_LIFECYCLE_LOCK: Mutex<()> = Mutex::const_new(()); -const DURABLE_MAIN_SCRIPT: &str = r#"echo "$1"; exec sleep infinity"#; - #[derive(Clone, Copy)] struct IdentityScenario { gateway_name: &'static str, @@ -915,10 +913,7 @@ async fn workspace_user_cannot_create_sandbox_in_another_workspace() { "oidc-xcreate-denied", "--no-tty", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "denied", ], ) @@ -1388,12 +1383,8 @@ async fn assert_can_create_sandbox(session: &LoginSession, workspace: &str, sand "--name", sandbox_name, "--no-tty", - "--detach", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", &marker, ], ) @@ -1413,6 +1404,10 @@ async fn assert_can_create_sandbox(session: &LoginSession, workspace: &str, sand let list_output = combined_output(&list); let cleanup = run_workspace_cli(session, workspace, &["sandbox", "delete", sandbox_name]).await; + assert!( + create_output.contains(&marker), + "sandbox command output should contain {marker}:\n{create_output}" + ); assert!( list.status.success() && list_output.contains(sandbox_name), "created sandbox {sandbox_name} should appear in the sandbox list:\n{list_output}" @@ -1435,12 +1430,8 @@ async fn assert_can_delete_sandbox(session: &LoginSession, workspace: &str, sand "--name", sandbox_name, "--no-tty", - "--detach", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", &marker, ], ) diff --git a/e2e/rust/tests/sandbox_lifecycle.rs b/e2e/rust/tests/sandbox_lifecycle.rs index 0b0f4e0e66..c920fd42bb 100644 --- a/e2e/rust/tests/sandbox_lifecycle.rs +++ b/e2e/rust/tests/sandbox_lifecycle.rs @@ -220,7 +220,7 @@ async fn sandbox_can_be_deleted_while_stopped() { } #[tokio::test] -async fn canonical_main_exit_transitions_persistent_sandbox_to_error() { +async fn canonical_main_exit_zero_completes_persistent_sandbox() { let mut cmd = openshell_tty_cmd(&["sandbox", "create", "--", "echo", "OK"]); cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); @@ -229,9 +229,10 @@ async fn canonical_main_exit_transitions_persistent_sandbox_to_error() { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let combined = normalize_output(&format!("{stdout}{stderr}")); + assert!(output.status.success(), "create failed:\n{combined}"); assert!( - !output.status.success(), - "main-process exit must fail create" + combined.contains("OK"), + "main output was not streamed:\n{combined}" ); let sandbox_name = extract_sandbox_name(&combined).expect("sandbox name should be present in output"); @@ -260,13 +261,65 @@ async fn canonical_main_exit_transitions_persistent_sandbox_to_error() { "sandbox get failed:\n{details}" ); assert!( - details.contains("Phase: Error"), + details.contains("Phase: Completed"), "expected terminal sandbox phase:\n{details}" ); delete_sandbox(&sandbox_name).await; } +#[tokio::test] +async fn canonical_main_nonzero_exit_preserves_status() { + let mut cmd = openshell_tty_cmd(&[ + "sandbox", + "create", + "--", + "sh", + "-c", + "echo failed-main; exit 7", + ]); + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + + let output = cmd.output().await.expect("spawn openshell sandbox create"); + let combined = normalize_output(&format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + )); + assert_eq!( + output.status.code(), + Some(7), + "unexpected result:\n{combined}" + ); + assert!( + combined.contains("failed-main"), + "main output was not streamed:\n{combined}" + ); + let sandbox_name = + extract_sandbox_name(&combined).expect("sandbox name should be present in output"); + + let mut get_cmd = openshell_cmd(); + get_cmd + .args(["sandbox", "get", &sandbox_name]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let get_output = get_cmd.output().await.expect("spawn openshell sandbox get"); + let details = normalize_output(&format!( + "{}{}", + String::from_utf8_lossy(&get_output.stdout), + String::from_utf8_lossy(&get_output.stderr), + )); + assert!( + details.contains("Phase: Error"), + "unexpected phase:\n{details}" + ); + assert!( + details.contains("Exit Code: 7"), + "missing exit code:\n{details}" + ); + delete_sandbox(&sandbox_name).await; +} + #[tokio::test] async fn canonical_tty_main_uses_sandbox_environment() { let script = r#"printf 'canonical_env home=%s user=%s term=%s\n' "$HOME" "$USER" "$TERM"; while true; do sleep 1; done"#; @@ -431,9 +484,10 @@ async fn sandbox_create_with_no_keep_cleans_up_after_tty_command() { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); let combined = normalize_output(&format!("{stdout}{stderr}")); + assert!(output.status.success(), "create failed:\n{combined}"); assert!( - !output.status.success(), - "main-process exit must fail create" + combined.contains("OK"), + "main output was not streamed:\n{combined}" ); let sandbox_name = extract_sandbox_name(&combined).expect("sandbox name should be present in output"); diff --git a/e2e/rust/tests/workspace_namespace_managed.rs b/e2e/rust/tests/workspace_namespace_managed.rs index b6d0555f3a..eae13e2cf6 100644 --- a/e2e/rust/tests/workspace_namespace_managed.rs +++ b/e2e/rust/tests/workspace_namespace_managed.rs @@ -148,16 +148,16 @@ async fn managed_creates_namespace_with_labels() { &ws, "--name", "mgd-sb", - "--detach", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "managed-ok", ]) .await; assert!(ok, "sandbox create failed: {out}"); + assert!( + out.contains("managed-ok"), + "sandbox output missing expected string: {out}" + ); // Verify the managed namespace was created. let (ok, out) = kubectl(&["get", "namespace", &ns]).await; @@ -281,10 +281,7 @@ async fn managed_namespace_survives_with_remaining_sandboxes() { "--name", "sb-a", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "a", ]) .await; @@ -298,10 +295,7 @@ async fn managed_namespace_survives_with_remaining_sandboxes() { "--name", "sb-b", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "b", ]) .await; @@ -363,10 +357,7 @@ async fn managed_isolates_workspaces_into_separate_namespaces() { "--name", "sb-iso-a", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "a", ]) .await; @@ -380,10 +371,7 @@ async fn managed_isolates_workspaces_into_separate_namespaces() { "--name", "sb-iso-b", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "b", ]) .await; @@ -457,16 +445,16 @@ async fn managed_workspace_delete_removes_namespace() { &ws, "--name", "del-sb", - "--detach", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "del-ok", ]) .await; assert!(ok, "sandbox create failed: {out}"); + assert!( + out.contains("del-ok"), + "sandbox output missing expected string: {out}" + ); let (ok, _) = kubectl(&["get", "namespace", &ns]).await; assert!( @@ -529,16 +517,16 @@ async fn managed_tls_secret_copied_to_namespace() { &ws, "--name", "tls-sb", - "--detach", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "tls-ok", ]) .await; assert!(ok, "sandbox create failed: {out}"); + assert!( + out.contains("tls-ok"), + "sandbox output missing expected string: {out}" + ); let (ok, out) = kubectl(&["get", "secret", "openshell-client-tls", "-n", &ns]).await; assert!( @@ -596,10 +584,7 @@ async fn managed_rejects_namespace_owned_by_different_gateway() { "--name", "conflict-sb", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "nope", ]) .await; @@ -629,10 +614,7 @@ async fn managed_full_lifecycle_with_multiple_sandboxes() { "--name", "lc-a", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "a", ]) .await; @@ -646,10 +628,7 @@ async fn managed_full_lifecycle_with_multiple_sandboxes() { "--name", "lc-b", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "b", ]) .await; @@ -727,6 +706,7 @@ async fn managed_stop_waits_for_workspace_pod_to_disappear() { &ws, "--name", sandbox, + "--detach", "--", "sh", "-c", @@ -780,10 +760,7 @@ async fn managed_rejects_invalid_dns1123_sandbox_name() { "--name", "my_bad_name", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "nope", ]) .await; @@ -804,10 +781,7 @@ async fn managed_rejects_invalid_dns1123_sandbox_name() { "--name", "MyBadName", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "nope", ]) .await; @@ -825,10 +799,7 @@ async fn managed_rejects_invalid_dns1123_sandbox_name() { "--name", "trailing-", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "nope", ]) .await; diff --git a/e2e/rust/tests/workspace_namespace_operator.rs b/e2e/rust/tests/workspace_namespace_operator.rs index 315587bacb..a874c1188b 100644 --- a/e2e/rust/tests/workspace_namespace_operator.rs +++ b/e2e/rust/tests/workspace_namespace_operator.rs @@ -18,7 +18,6 @@ use openshell_e2e::harness::output::strip_ansi; const OPERATOR_LABEL: &str = "openshell.ai/e2e-operator-workspace=true"; const SA_NAME: &str = "openshell-sandbox"; -const DURABLE_MAIN_SCRIPT: &str = r#"echo "$1"; exec sleep infinity"#; fn kube_context() -> String { std::env::var("OPENSHELL_E2E_KUBE_CONTEXT_ACTIVE") @@ -163,7 +162,7 @@ async fn operator_sandbox_in_labeled_namespace() { // Poll until the gateway's namespace watcher discovers the labeled namespace // and sandbox creation succeeds (up to 30s). let deadline = tokio::time::Instant::now() + Duration::from_secs(30); - loop { + let sandbox_out = loop { let (ok, out) = run_cli(&[ "sandbox", "create", @@ -171,23 +170,23 @@ async fn operator_sandbox_in_labeled_namespace() { &ns, "--name", "op-sb", - "--detach", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "operator-ok", ]) .await; if ok { - break; + break out; } if tokio::time::Instant::now() >= deadline { panic!("sandbox create did not succeed within 30s: {out}"); } tokio::time::sleep(Duration::from_secs(2)).await; - } + }; + assert!( + sandbox_out.contains("operator-ok"), + "sandbox output missing expected string: {sandbox_out}" + ); // Verify the sandbox CR lives in the pre-provisioned namespace. let (ok, out) = kubectl(&["get", "sandbox.agents.x-k8s.io", "-n", &ns, "-o", "name"]).await; @@ -255,10 +254,7 @@ async fn operator_rejects_unlabeled_namespace() { "--name", "should-fail", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "nope", ]) .await; @@ -294,10 +290,7 @@ async fn operator_rejects_nonexistent_namespace() { "--name", "should-fail", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "nope", ]) .await; @@ -334,10 +327,7 @@ async fn operator_workspace_delete_preserves_namespace() { "--name", "opdel-sb", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "opdel-ok", ]) .await; @@ -399,10 +389,7 @@ async fn operator_label_removal_blocks_sandbox_creation() { "--name", "lbl-sb1", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "lbl-ok", ]) .await; @@ -439,10 +426,7 @@ async fn operator_label_removal_blocks_sandbox_creation() { "--name", "lbl-sb2", "--", - "sh", - "-c", - DURABLE_MAIN_SCRIPT, - "_", + "echo", "should-fail", ]) .await; diff --git a/proto/compute_driver.proto b/proto/compute_driver.proto index afa93f1b18..e26fafdd6a 100644 --- a/proto/compute_driver.proto +++ b/proto/compute_driver.proto @@ -152,6 +152,9 @@ message DriverSandboxSpec { repeated string command = 12; // Allocate a retained pseudo-terminal for the canonical process. bool tty = 13; + // One-shot launch hint forwarded by the gateway when the creating client + // will attach to the canonical main process. + bool await_main_process_attachment = 14; } message ResourceRequirements { diff --git a/proto/openshell.proto b/proto/openshell.proto index 246fe0626f..32f39adf67 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -471,6 +471,14 @@ service OpenShell { }; } + // Confirm that foreground terminal delivery completed naturally. + rpc FinalizeMainProcessExit(FinalizeMainProcessExitRequest) + returns (FinalizeMainProcessExitResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + // Raw byte relay between supervisor and gateway. // // The supervisor initiates this call after receiving a RelayOpen message @@ -907,7 +915,8 @@ message SandboxStatus { // The gateway uses this to reject stale exit reports after a restart. string main_process_instance_id = 8; // Normalized main process result. Signal exits use 128 + signal number. - // Presence indicates that the main process exited and the sandbox is in Error. + // Presence indicates that the canonical main process exited. Exit code 0 + // produces Completed; nonzero and signal-normalized exits produce Error. optional int32 exit_code = 9; } @@ -939,6 +948,8 @@ enum SandboxPhase { SANDBOX_PHASE_STOPPING = 6; SANDBOX_PHASE_STOPPED = 7; SANDBOX_PHASE_STARTING = 8; + // The canonical main process exited successfully and its result is final. + SANDBOX_PHASE_COMPLETED = 9; } // Public platform event exposed on the sandbox watch stream. @@ -968,6 +979,10 @@ message CreateSandboxRequest { map annotations = 4; // Workspace for the sandbox. Empty defaults to "default". string workspace = 5; + // One-shot launch hint indicating that the creating client will attach to + // the canonical main process. The supervisor keeps the terminal transport + // alive until that attachment connects and closes naturally. + bool await_main_process_attachment = 6; } // Get sandbox request. @@ -1355,7 +1370,8 @@ message WatchSandboxRequest { // Replay the last N platform events (best-effort) before following. uint32 event_tail = 6; - // Stop streaming once the sandbox reaches a terminal phase (READY or ERROR). + // Stop streaming once the sandbox reaches READY or a terminal result phase + // (COMPLETED, STOPPED, or ERROR). bool stop_on_terminal = 7; // Only include log lines with timestamp >= this value (milliseconds since epoch). @@ -2240,6 +2256,15 @@ message ReportMainProcessExitRequest { message ReportMainProcessExitResponse {} +// Terminal-delivery completion reported after all expected foreground SSH +// attachments close. A successful response permits ephemeral cleanup. +message FinalizeMainProcessExitRequest { + string sandbox_id = 1; + string instance_id = 2; +} + +message FinalizeMainProcessExitResponse {} + // Gateway requests the supervisor to open a relay channel. // // On receiving this, the supervisor should initiate a RelayStream RPC to diff --git a/python/openshell/sandbox.py b/python/openshell/sandbox.py index 94f3c57def..58856de454 100644 --- a/python/openshell/sandbox.py +++ b/python/openshell/sandbox.py @@ -887,6 +887,18 @@ def _wait_for_phase( sandbox = self.get(sandbox_name, workspace=workspace) if sandbox.status.phase == target_phase: return sandbox + if ( + target_phase == openshell_pb2.SANDBOX_PHASE_READY + and sandbox.status.phase == openshell_pb2.SANDBOX_PHASE_COMPLETED + ): + return sandbox + if ( + target_phase == openshell_pb2.SANDBOX_PHASE_READY + and sandbox.status.phase == openshell_pb2.SANDBOX_PHASE_STOPPED + ): + raise SandboxError( + f"sandbox {sandbox_name} stopped before becoming ready" + ) if sandbox.status.phase == openshell_pb2.SANDBOX_PHASE_ERROR: raise SandboxError(f"sandbox {sandbox_name} entered error phase") time.sleep(1) diff --git a/python/openshell/sandbox_test.py b/python/openshell/sandbox_test.py index d70aae49cc..23382d1093 100644 --- a/python/openshell/sandbox_test.py +++ b/python/openshell/sandbox_test.py @@ -2146,6 +2146,41 @@ def test_stop_and_start_forward_workspace_and_return_phase() -> None: assert starting.phase == openshell_pb2.SANDBOX_PHASE_STARTING +@pytest.mark.parametrize( + ("phase", "should_succeed"), + [ + (openshell_pb2.SANDBOX_PHASE_COMPLETED, True), + (openshell_pb2.SANDBOX_PHASE_ERROR, False), + ], +) +def test_wait_ready_handles_terminal_main_process_results( + phase: openshell_pb2.SandboxPhase, should_succeed: bool +) -> None: + class TerminalStub(_FakeSandboxStub): + def GetSandbox( + self, + request: openshell_pb2.GetSandboxRequest, + timeout: float | None = None, + ) -> Any: + _ = timeout + return SimpleNamespace( + sandbox=_make_sandbox_proto( + "sandbox-1", + request.name, + phase=phase, + workspace=request.workspace, + ) + ) + + client = _client_with_fake_stub(TerminalStub()) + if should_succeed: + result = client.wait_ready("job-1", workspace="default", timeout_seconds=0.1) + assert result.phase == openshell_pb2.SANDBOX_PHASE_COMPLETED + else: + with pytest.raises(SandboxError, match="entered error phase"): + client.wait_ready("job-1", workspace="default", timeout_seconds=0.1) + + def test_create_without_args_sends_empty_metadata() -> None: stub = _FakeSandboxStub() client = _client_with_fake_stub(stub) diff --git a/sdk/go/openshell/v1/fake/sandbox.go b/sdk/go/openshell/v1/fake/sandbox.go index da32adf4c5..6fa0c316a9 100644 --- a/sdk/go/openshell/v1/fake/sandbox.go +++ b/sdk/go/openshell/v1/fake/sandbox.go @@ -504,8 +504,8 @@ func (c *fakeSandboxClient) WaitReady(ctx context.Context, workspace, name strin // Watch registers a watcher for sandbox events. If name is non-empty, only // events for that sandbox are delivered. When StopOnTerminal is set, the -// watcher auto-closes after delivering a terminal phase event (SandboxReady -// or SandboxError). +// watcher auto-closes after delivering a terminal phase event (SandboxReady, +// SandboxCompleted, SandboxStopped, or SandboxError). func (c *fakeSandboxClient) Watch(ctx context.Context, _, name string, opts ...v1.WatchOptions) (types.WatchInterface[*types.Sandbox], error) { if c.closedFunc() { return nil, &types.StatusError{Code: types.ErrorUnavailable, Message: "client is closed"} @@ -547,7 +547,7 @@ func (c *fakeSandboxClient) Watch(ctx context.Context, _, name string, opts ...v return } if ev.Object != nil && - (ev.Object.Status.Phase == types.SandboxReady || ev.Object.Status.Phase == types.SandboxError) { + (ev.Object.Status.Phase == types.SandboxReady || ev.Object.Status.Phase == types.SandboxCompleted || ev.Object.Status.Phase == types.SandboxStopped || ev.Object.Status.Phase == types.SandboxError) { inner.Stop() return } diff --git a/sdk/go/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index 6481454d6d..ea9add6d21 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -63,8 +63,8 @@ func TestConverterCoversAllProtoFields_SandboxStatus(t *testing.T) { "current_policy_version": true, "exit_code": true, } - // The instance ID is an internal gateway/supervisor fencing token exposed - // only through the raw protobuf API. + // The instance ID coordinates internal gateway/supervisor lifecycle + // fencing. It is exposed only through the raw protobuf API. skipped := fieldSet{"main_process_instance_id": true} assertAllFieldsCovered(t, (&pb.SandboxStatus{}).ProtoReflect().Descriptor(), handled, skipped) diff --git a/sdk/go/openshell/v1/internal/converter/sandbox.go b/sdk/go/openshell/v1/internal/converter/sandbox.go index 5d26a1a7e2..6b61053295 100644 --- a/sdk/go/openshell/v1/internal/converter/sandbox.go +++ b/sdk/go/openshell/v1/internal/converter/sandbox.go @@ -125,6 +125,8 @@ func SandboxPhaseFromProto(phase pb.SandboxPhase) types.SandboxPhase { return types.SandboxStopped case pb.SandboxPhase_SANDBOX_PHASE_STARTING: return types.SandboxStarting + case pb.SandboxPhase_SANDBOX_PHASE_COMPLETED: + return types.SandboxCompleted default: return types.SandboxUnknown } @@ -149,6 +151,8 @@ func SandboxPhaseToProto(phase types.SandboxPhase) pb.SandboxPhase { return pb.SandboxPhase_SANDBOX_PHASE_STOPPED case types.SandboxStarting: return pb.SandboxPhase_SANDBOX_PHASE_STARTING + case types.SandboxCompleted: + return pb.SandboxPhase_SANDBOX_PHASE_COMPLETED default: return pb.SandboxPhase_SANDBOX_PHASE_UNKNOWN } diff --git a/sdk/go/openshell/v1/internal/converter/sandbox_test.go b/sdk/go/openshell/v1/internal/converter/sandbox_test.go index 8293a784c0..9f68013845 100644 --- a/sdk/go/openshell/v1/internal/converter/sandbox_test.go +++ b/sdk/go/openshell/v1/internal/converter/sandbox_test.go @@ -194,6 +194,7 @@ func TestSandboxPhaseFromProto(t *testing.T) { {pb.SandboxPhase_SANDBOX_PHASE_UNKNOWN, v1.SandboxUnknown}, {pb.SandboxPhase_SANDBOX_PHASE_STOPPING, v1.SandboxStopping}, {pb.SandboxPhase_SANDBOX_PHASE_STOPPED, v1.SandboxStopped}, + {pb.SandboxPhase_SANDBOX_PHASE_COMPLETED, v1.SandboxCompleted}, {pb.SandboxPhase_SANDBOX_PHASE_STARTING, v1.SandboxStarting}, {pb.SandboxPhase_SANDBOX_PHASE_UNSPECIFIED, v1.SandboxUnknown}, {pb.SandboxPhase(999), v1.SandboxUnknown}, @@ -216,6 +217,7 @@ func TestSandboxPhaseToProto(t *testing.T) { {v1.SandboxUnknown, pb.SandboxPhase_SANDBOX_PHASE_UNKNOWN}, {v1.SandboxStopping, pb.SandboxPhase_SANDBOX_PHASE_STOPPING}, {v1.SandboxStopped, pb.SandboxPhase_SANDBOX_PHASE_STOPPED}, + {v1.SandboxCompleted, pb.SandboxPhase_SANDBOX_PHASE_COMPLETED}, {v1.SandboxStarting, pb.SandboxPhase_SANDBOX_PHASE_STARTING}, {v1.SandboxPhase("bogus"), pb.SandboxPhase_SANDBOX_PHASE_UNKNOWN}, } diff --git a/sdk/go/openshell/v1/sandbox_client.go b/sdk/go/openshell/v1/sandbox_client.go index 94d6047a01..f31cd40af9 100644 --- a/sdk/go/openshell/v1/sandbox_client.go +++ b/sdk/go/openshell/v1/sandbox_client.go @@ -214,6 +214,13 @@ func checkTerminalPhase(sb *Sandbox, name string, target SandboxPhase) (*Sandbox return sb, nil } switch sb.Status.Phase { + case SandboxCompleted: + if target == SandboxReady { + return sb, nil + } + return nil, &StatusError{Code: ErrorInternal, Message: fmt.Sprintf("sandbox %q completed before reaching %s", name, target)} + case SandboxStopped: + return nil, &StatusError{Code: ErrorInternal, Message: fmt.Sprintf("sandbox %q stopped before reaching %s", name, target)} case SandboxError: return nil, &StatusError{Code: ErrorInternal, Message: fmt.Sprintf("sandbox %q is in error state", name)} case SandboxDeleting: @@ -278,7 +285,7 @@ func (s *sandboxClient) Watch(ctx context.Context, workspace, name string, opts case <-w.done: return } - if watchOpts.StopOnTerminal && (sandbox.Status.Phase == SandboxReady || sandbox.Status.Phase == SandboxError) { + if watchOpts.StopOnTerminal && (sandbox.Status.Phase == SandboxReady || sandbox.Status.Phase == SandboxCompleted || sandbox.Status.Phase == SandboxStopped || sandbox.Status.Phase == SandboxError) { w.Stop() return } diff --git a/sdk/go/openshell/v1/sandbox_client_test.go b/sdk/go/openshell/v1/sandbox_client_test.go index c3725afad6..91e80db148 100644 --- a/sdk/go/openshell/v1/sandbox_client_test.go +++ b/sdk/go/openshell/v1/sandbox_client_test.go @@ -633,6 +633,36 @@ func TestSandboxWaitReady_SandboxFailed(t *testing.T) { require.Error(t, err) } +func TestSandboxWaitReady_SandboxCompleted(t *testing.T) { + mock := newMockSandboxServer() + mock.sandboxes["complete-sb"] = &pb.Sandbox{ + Metadata: &dm.ObjectMeta{Name: "complete-sb"}, + Status: &pb.SandboxStatus{Phase: pb.SandboxPhase_SANDBOX_PHASE_COMPLETED}, + } + client, cleanup := setupSandboxTest(t, mock) + defer cleanup() + + result, err := client.WaitReady(context.Background(), "default", "complete-sb") + + require.NoError(t, err) + assert.Equal(t, SandboxCompleted, result.Status.Phase) +} + +func TestSandboxWaitReady_SandboxStopped(t *testing.T) { + mock := newMockSandboxServer() + mock.sandboxes["stopped-sb"] = &pb.Sandbox{ + Metadata: &dm.ObjectMeta{Name: "stopped-sb"}, + Status: &pb.SandboxStatus{Phase: pb.SandboxPhase_SANDBOX_PHASE_STOPPED}, + } + client, cleanup := setupSandboxTest(t, mock) + defer cleanup() + + _, err := client.WaitReady(context.Background(), "default", "stopped-sb") + + require.Error(t, err) + assert.Contains(t, err.Error(), "stopped") +} + func TestSandboxWaitReady_SandboxDeleting(t *testing.T) { mock := newMockSandboxServer() mock.sandboxes["deleting-sb"] = &pb.Sandbox{ diff --git a/sdk/go/openshell/v1/types.go b/sdk/go/openshell/v1/types.go index dea7872a04..6226aa0707 100644 --- a/sdk/go/openshell/v1/types.go +++ b/sdk/go/openshell/v1/types.go @@ -20,6 +20,7 @@ const ( SandboxStopping = types.SandboxStopping SandboxStopped = types.SandboxStopped SandboxStarting = types.SandboxStarting + SandboxCompleted = types.SandboxCompleted ) // EventType classifies watch events. diff --git a/sdk/go/openshell/v1/types/types.go b/sdk/go/openshell/v1/types/types.go index 4e3b830805..5b32ec3685 100644 --- a/sdk/go/openshell/v1/types/types.go +++ b/sdk/go/openshell/v1/types/types.go @@ -18,6 +18,7 @@ const ( SandboxStopping SandboxPhase = "Stopping" SandboxStopped SandboxPhase = "Stopped" SandboxStarting SandboxPhase = "Starting" + SandboxCompleted SandboxPhase = "Completed" ) // EventType classifies watch events. diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 6b3b740a0b..1b77e0889e 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -44,6 +44,8 @@ const ( SandboxPhase_SANDBOX_PHASE_STOPPING SandboxPhase = 6 SandboxPhase_SANDBOX_PHASE_STOPPED SandboxPhase = 7 SandboxPhase_SANDBOX_PHASE_STARTING SandboxPhase = 8 + // The canonical main process exited successfully and its result is final. + SandboxPhase_SANDBOX_PHASE_COMPLETED SandboxPhase = 9 ) // Enum value maps for SandboxPhase. @@ -58,6 +60,7 @@ var ( 6: "SANDBOX_PHASE_STOPPING", 7: "SANDBOX_PHASE_STOPPED", 8: "SANDBOX_PHASE_STARTING", + 9: "SANDBOX_PHASE_COMPLETED", } SandboxPhase_value = map[string]int32{ "SANDBOX_PHASE_UNSPECIFIED": 0, @@ -69,6 +72,7 @@ var ( "SANDBOX_PHASE_STOPPING": 6, "SANDBOX_PHASE_STOPPED": 7, "SANDBOX_PHASE_STARTING": 8, + "SANDBOX_PHASE_COMPLETED": 9, } ) @@ -1559,7 +1563,8 @@ type SandboxStatus struct { // The gateway uses this to reject stale exit reports after a restart. MainProcessInstanceId string `protobuf:"bytes,8,opt,name=main_process_instance_id,json=mainProcessInstanceId,proto3" json:"main_process_instance_id,omitempty"` // Normalized main process result. Signal exits use 128 + signal number. - // Presence indicates that the main process exited and the sandbox is in Error. + // Presence indicates that the canonical main process exited. Exit code 0 + // produces Completed; nonzero and signal-normalized exits produce Error. ExitCode *int32 `protobuf:"varint,9,opt,name=exit_code,json=exitCode,proto3,oneof" json:"exit_code,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1842,9 +1847,13 @@ type CreateSandboxRequest struct { // Optional annotations for the sandbox (non-selector metadata). Annotations map[string]string `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Workspace for the sandbox. Empty defaults to "default". - Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` + // One-shot launch hint indicating that the creating client will attach to + // the canonical main process. The supervisor keeps the terminal transport + // alive until that attachment connects and closes naturally. + AwaitMainProcessAttachment bool `protobuf:"varint,6,opt,name=await_main_process_attachment,json=awaitMainProcessAttachment,proto3" json:"await_main_process_attachment,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateSandboxRequest) Reset() { @@ -1912,6 +1921,13 @@ func (x *CreateSandboxRequest) GetWorkspace() string { return "" } +func (x *CreateSandboxRequest) GetAwaitMainProcessAttachment() bool { + if x != nil { + return x.AwaitMainProcessAttachment + } + return false +} + // Get sandbox request. type GetSandboxRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4283,7 +4299,8 @@ type WatchSandboxRequest struct { LogTailLines uint32 `protobuf:"varint,5,opt,name=log_tail_lines,json=logTailLines,proto3" json:"log_tail_lines,omitempty"` // Replay the last N platform events (best-effort) before following. EventTail uint32 `protobuf:"varint,6,opt,name=event_tail,json=eventTail,proto3" json:"event_tail,omitempty"` - // Stop streaming once the sandbox reaches a terminal phase (READY or ERROR). + // Stop streaming once the sandbox reaches READY or a terminal result phase + // (COMPLETED, STOPPED, or ERROR). StopOnTerminal bool `protobuf:"varint,7,opt,name=stop_on_terminal,json=stopOnTerminal,proto3" json:"stop_on_terminal,omitempty"` // Only include log lines with timestamp >= this value (milliseconds since epoch). // 0 means no time filter. Applies to both tail replay and live streaming. @@ -10075,6 +10092,96 @@ func (*ReportMainProcessExitResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{138} } +// Terminal-delivery completion reported after all expected foreground SSH +// attachments close. A successful response permits ephemeral cleanup. +type FinalizeMainProcessExitRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + InstanceId string `protobuf:"bytes,2,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinalizeMainProcessExitRequest) Reset() { + *x = FinalizeMainProcessExitRequest{} + mi := &file_openshell_proto_msgTypes[139] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinalizeMainProcessExitRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinalizeMainProcessExitRequest) ProtoMessage() {} + +func (x *FinalizeMainProcessExitRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[139] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinalizeMainProcessExitRequest.ProtoReflect.Descriptor instead. +func (*FinalizeMainProcessExitRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{139} +} + +func (x *FinalizeMainProcessExitRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *FinalizeMainProcessExitRequest) GetInstanceId() string { + if x != nil { + return x.InstanceId + } + return "" +} + +type FinalizeMainProcessExitResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinalizeMainProcessExitResponse) Reset() { + *x = FinalizeMainProcessExitResponse{} + mi := &file_openshell_proto_msgTypes[140] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinalizeMainProcessExitResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinalizeMainProcessExitResponse) ProtoMessage() {} + +func (x *FinalizeMainProcessExitResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[140] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinalizeMainProcessExitResponse.ProtoReflect.Descriptor instead. +func (*FinalizeMainProcessExitResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{140} +} + // Gateway requests the supervisor to open a relay channel. // // On receiving this, the supervisor should initiate a RelayStream RPC to @@ -10101,7 +10208,7 @@ type RelayOpen struct { func (x *RelayOpen) Reset() { *x = RelayOpen{} - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10113,7 +10220,7 @@ func (x *RelayOpen) String() string { func (*RelayOpen) ProtoMessage() {} func (x *RelayOpen) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10126,7 +10233,7 @@ func (x *RelayOpen) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpen.ProtoReflect.Descriptor instead. func (*RelayOpen) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{139} + return file_openshell_proto_rawDescGZIP(), []int{141} } func (x *RelayOpen) GetChannelId() string { @@ -10193,7 +10300,7 @@ type SshRelayTarget struct { func (x *SshRelayTarget) Reset() { *x = SshRelayTarget{} - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10205,7 +10312,7 @@ func (x *SshRelayTarget) String() string { func (*SshRelayTarget) ProtoMessage() {} func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10218,7 +10325,7 @@ func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use SshRelayTarget.ProtoReflect.Descriptor instead. func (*SshRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{140} + return file_openshell_proto_rawDescGZIP(), []int{142} } // TCP target dialed by the supervisor from inside the sandbox. @@ -10234,7 +10341,7 @@ type TcpRelayTarget struct { func (x *TcpRelayTarget) Reset() { *x = TcpRelayTarget{} - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[143] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10246,7 +10353,7 @@ func (x *TcpRelayTarget) String() string { func (*TcpRelayTarget) ProtoMessage() {} func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[143] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10259,7 +10366,7 @@ func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpRelayTarget.ProtoReflect.Descriptor instead. func (*TcpRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{141} + return file_openshell_proto_rawDescGZIP(), []int{143} } func (x *TcpRelayTarget) GetHost() string { @@ -10287,7 +10394,7 @@ type RelayInit struct { func (x *RelayInit) Reset() { *x = RelayInit{} - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10299,7 +10406,7 @@ func (x *RelayInit) String() string { func (*RelayInit) ProtoMessage() {} func (x *RelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10312,7 +10419,7 @@ func (x *RelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayInit.ProtoReflect.Descriptor instead. func (*RelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{142} + return file_openshell_proto_rawDescGZIP(), []int{144} } func (x *RelayInit) GetChannelId() string { @@ -10339,7 +10446,7 @@ type RelayFrame struct { func (x *RelayFrame) Reset() { *x = RelayFrame{} - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10351,7 +10458,7 @@ func (x *RelayFrame) String() string { func (*RelayFrame) ProtoMessage() {} func (x *RelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10364,7 +10471,7 @@ func (x *RelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayFrame.ProtoReflect.Descriptor instead. func (*RelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{143} + return file_openshell_proto_rawDescGZIP(), []int{145} } func (x *RelayFrame) GetPayload() isRelayFrame_Payload { @@ -10423,7 +10530,7 @@ type RelayOpenResult struct { func (x *RelayOpenResult) Reset() { *x = RelayOpenResult{} - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10435,7 +10542,7 @@ func (x *RelayOpenResult) String() string { func (*RelayOpenResult) ProtoMessage() {} func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10448,7 +10555,7 @@ func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpenResult.ProtoReflect.Descriptor instead. func (*RelayOpenResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{144} + return file_openshell_proto_rawDescGZIP(), []int{146} } func (x *RelayOpenResult) GetChannelId() string { @@ -10485,7 +10592,7 @@ type RelayClose struct { func (x *RelayClose) Reset() { *x = RelayClose{} - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[147] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10497,7 +10604,7 @@ func (x *RelayClose) String() string { func (*RelayClose) ProtoMessage() {} func (x *RelayClose) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[147] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10510,7 +10617,7 @@ func (x *RelayClose) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayClose.ProtoReflect.Descriptor instead. func (*RelayClose) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{145} + return file_openshell_proto_rawDescGZIP(), []int{147} } func (x *RelayClose) GetChannelId() string { @@ -10544,7 +10651,7 @@ type L7RequestSample struct { func (x *L7RequestSample) Reset() { *x = L7RequestSample{} - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[148] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10556,7 +10663,7 @@ func (x *L7RequestSample) String() string { func (*L7RequestSample) ProtoMessage() {} func (x *L7RequestSample) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[148] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10569,7 +10676,7 @@ func (x *L7RequestSample) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RequestSample.ProtoReflect.Descriptor instead. func (*L7RequestSample) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{146} + return file_openshell_proto_rawDescGZIP(), []int{148} } func (x *L7RequestSample) GetMethod() string { @@ -10643,7 +10750,7 @@ type DenialSummary struct { func (x *DenialSummary) Reset() { *x = DenialSummary{} - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10655,7 +10762,7 @@ func (x *DenialSummary) String() string { func (*DenialSummary) ProtoMessage() {} func (x *DenialSummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10668,7 +10775,7 @@ func (x *DenialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialSummary.ProtoReflect.Descriptor instead. func (*DenialSummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{147} + return file_openshell_proto_rawDescGZIP(), []int{149} } func (x *DenialSummary) GetSandboxId() string { @@ -10803,7 +10910,7 @@ type DenialGroupCount struct { func (x *DenialGroupCount) Reset() { *x = DenialGroupCount{} - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[150] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10815,7 +10922,7 @@ func (x *DenialGroupCount) String() string { func (*DenialGroupCount) ProtoMessage() {} func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[150] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10828,7 +10935,7 @@ func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialGroupCount.ProtoReflect.Descriptor instead. func (*DenialGroupCount) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{148} + return file_openshell_proto_rawDescGZIP(), []int{150} } func (x *DenialGroupCount) GetDenyGroup() string { @@ -10861,7 +10968,7 @@ type NetworkActivitySummary struct { func (x *NetworkActivitySummary) Reset() { *x = NetworkActivitySummary{} - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[151] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10873,7 +10980,7 @@ func (x *NetworkActivitySummary) String() string { func (*NetworkActivitySummary) ProtoMessage() {} func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[151] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10886,7 +10993,7 @@ func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkActivitySummary.ProtoReflect.Descriptor instead. func (*NetworkActivitySummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{149} + return file_openshell_proto_rawDescGZIP(), []int{151} } func (x *NetworkActivitySummary) GetNetworkActivityCount() uint32 { @@ -10974,7 +11081,7 @@ type PolicyChunk struct { func (x *PolicyChunk) Reset() { *x = PolicyChunk{} - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[152] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10986,7 +11093,7 @@ func (x *PolicyChunk) String() string { func (*PolicyChunk) ProtoMessage() {} func (x *PolicyChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[152] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10999,7 +11106,7 @@ func (x *PolicyChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyChunk.ProtoReflect.Descriptor instead. func (*PolicyChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{150} + return file_openshell_proto_rawDescGZIP(), []int{152} } func (x *PolicyChunk) GetId() string { @@ -11187,7 +11294,7 @@ type DraftPolicyUpdate struct { func (x *DraftPolicyUpdate) Reset() { *x = DraftPolicyUpdate{} - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[153] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11199,7 +11306,7 @@ func (x *DraftPolicyUpdate) String() string { func (*DraftPolicyUpdate) ProtoMessage() {} func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[153] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11212,7 +11319,7 @@ func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftPolicyUpdate.ProtoReflect.Descriptor instead. func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{151} + return file_openshell_proto_rawDescGZIP(), []int{153} } func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { @@ -11270,7 +11377,7 @@ type SubmitPolicyAnalysisRequest struct { func (x *SubmitPolicyAnalysisRequest) Reset() { *x = SubmitPolicyAnalysisRequest{} - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[154] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11282,7 +11389,7 @@ func (x *SubmitPolicyAnalysisRequest) String() string { func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[154] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11295,7 +11402,7 @@ func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{152} + return file_openshell_proto_rawDescGZIP(), []int{154} } func (x *SubmitPolicyAnalysisRequest) GetSummaries() []*DenialSummary { @@ -11358,7 +11465,7 @@ type SubmitPolicyAnalysisResponse struct { func (x *SubmitPolicyAnalysisResponse) Reset() { *x = SubmitPolicyAnalysisResponse{} - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[155] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11370,7 +11477,7 @@ func (x *SubmitPolicyAnalysisResponse) String() string { func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[155] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11383,7 +11490,7 @@ func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{153} + return file_openshell_proto_rawDescGZIP(), []int{155} } func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { @@ -11429,7 +11536,7 @@ type GetDraftPolicyRequest struct { func (x *GetDraftPolicyRequest) Reset() { *x = GetDraftPolicyRequest{} - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[156] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11441,7 +11548,7 @@ func (x *GetDraftPolicyRequest) String() string { func (*GetDraftPolicyRequest) ProtoMessage() {} func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[156] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11454,7 +11561,7 @@ func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyRequest.ProtoReflect.Descriptor instead. func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{154} + return file_openshell_proto_rawDescGZIP(), []int{156} } func (x *GetDraftPolicyRequest) GetName() string { @@ -11494,7 +11601,7 @@ type GetDraftPolicyResponse struct { func (x *GetDraftPolicyResponse) Reset() { *x = GetDraftPolicyResponse{} - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[157] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11506,7 +11613,7 @@ func (x *GetDraftPolicyResponse) String() string { func (*GetDraftPolicyResponse) ProtoMessage() {} func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[157] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11519,7 +11626,7 @@ func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyResponse.ProtoReflect.Descriptor instead. func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{155} + return file_openshell_proto_rawDescGZIP(), []int{157} } func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { @@ -11568,7 +11675,7 @@ type ApproveDraftChunkRequest struct { func (x *ApproveDraftChunkRequest) Reset() { *x = ApproveDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11580,7 +11687,7 @@ func (x *ApproveDraftChunkRequest) String() string { func (*ApproveDraftChunkRequest) ProtoMessage() {} func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11593,7 +11700,7 @@ func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{156} + return file_openshell_proto_rawDescGZIP(), []int{158} } func (x *ApproveDraftChunkRequest) GetName() string { @@ -11636,7 +11743,7 @@ type ApproveDraftChunkResponse struct { func (x *ApproveDraftChunkResponse) Reset() { *x = ApproveDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11648,7 +11755,7 @@ func (x *ApproveDraftChunkResponse) String() string { func (*ApproveDraftChunkResponse) ProtoMessage() {} func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11661,7 +11768,7 @@ func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{157} + return file_openshell_proto_rawDescGZIP(), []int{159} } func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { @@ -11695,7 +11802,7 @@ type RejectDraftChunkRequest struct { func (x *RejectDraftChunkRequest) Reset() { *x = RejectDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11707,7 +11814,7 @@ func (x *RejectDraftChunkRequest) String() string { func (*RejectDraftChunkRequest) ProtoMessage() {} func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11720,7 +11827,7 @@ func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkRequest.ProtoReflect.Descriptor instead. func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{158} + return file_openshell_proto_rawDescGZIP(), []int{160} } func (x *RejectDraftChunkRequest) GetName() string { @@ -11759,7 +11866,7 @@ type RejectDraftChunkResponse struct { func (x *RejectDraftChunkResponse) Reset() { *x = RejectDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11771,7 +11878,7 @@ func (x *RejectDraftChunkResponse) String() string { func (*RejectDraftChunkResponse) ProtoMessage() {} func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11784,7 +11891,7 @@ func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkResponse.ProtoReflect.Descriptor instead. func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{159} + return file_openshell_proto_rawDescGZIP(), []int{161} } // Approve all pending chunks. @@ -11798,7 +11905,7 @@ type DraftChunkApproval struct { func (x *DraftChunkApproval) Reset() { *x = DraftChunkApproval{} - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11810,7 +11917,7 @@ func (x *DraftChunkApproval) String() string { func (*DraftChunkApproval) ProtoMessage() {} func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11823,7 +11930,7 @@ func (x *DraftChunkApproval) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkApproval.ProtoReflect.Descriptor instead. func (*DraftChunkApproval) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{160} + return file_openshell_proto_rawDescGZIP(), []int{162} } func (x *DraftChunkApproval) GetChunkId() string { @@ -11857,7 +11964,7 @@ type ApproveAllDraftChunksRequest struct { func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11869,7 +11976,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11882,7 +11989,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{161} + return file_openshell_proto_rawDescGZIP(), []int{163} } func (x *ApproveAllDraftChunksRequest) GetName() string { @@ -11930,7 +12037,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11942,7 +12049,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11955,7 +12062,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{162} + return file_openshell_proto_rawDescGZIP(), []int{164} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -12003,7 +12110,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12015,7 +12122,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12028,7 +12135,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{163} + return file_openshell_proto_rawDescGZIP(), []int{165} } func (x *EditDraftChunkRequest) GetName() string { @@ -12067,7 +12174,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12079,7 +12186,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12092,7 +12199,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{164} + return file_openshell_proto_rawDescGZIP(), []int{166} } // Reverse an approval (remove merged rule from active policy). @@ -12110,7 +12217,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12122,7 +12229,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12135,7 +12242,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{165} + return file_openshell_proto_rawDescGZIP(), []int{167} } func (x *UndoDraftChunkRequest) GetName() string { @@ -12171,7 +12278,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12183,7 +12290,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12196,7 +12303,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{166} + return file_openshell_proto_rawDescGZIP(), []int{168} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -12226,7 +12333,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12238,7 +12345,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12251,7 +12358,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{167} + return file_openshell_proto_rawDescGZIP(), []int{169} } func (x *ClearDraftChunksRequest) GetName() string { @@ -12278,7 +12385,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12290,7 +12397,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12303,7 +12410,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{168} + return file_openshell_proto_rawDescGZIP(), []int{170} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -12326,7 +12433,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12338,7 +12445,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12351,7 +12458,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{169} + return file_openshell_proto_rawDescGZIP(), []int{171} } func (x *GetDraftHistoryRequest) GetName() string { @@ -12385,7 +12492,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12397,7 +12504,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[172] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12410,7 +12517,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{170} + return file_openshell_proto_rawDescGZIP(), []int{172} } func (x *DraftHistoryEntry) GetTimestampMs() int64 { @@ -12451,7 +12558,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12463,7 +12570,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[173] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12476,7 +12583,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{171} + return file_openshell_proto_rawDescGZIP(), []int{173} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -12505,7 +12612,7 @@ type PolicyRevisionPayload struct { func (x *PolicyRevisionPayload) Reset() { *x = PolicyRevisionPayload{} - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12517,7 +12624,7 @@ func (x *PolicyRevisionPayload) String() string { func (*PolicyRevisionPayload) ProtoMessage() {} func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[174] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12530,7 +12637,7 @@ func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyRevisionPayload.ProtoReflect.Descriptor instead. func (*PolicyRevisionPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{172} + return file_openshell_proto_rawDescGZIP(), []int{174} } func (x *PolicyRevisionPayload) GetPolicy() *sandboxv1.SandboxPolicy { @@ -12609,7 +12716,7 @@ type DraftChunkPayload struct { func (x *DraftChunkPayload) Reset() { *x = DraftChunkPayload{} - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12621,7 +12728,7 @@ func (x *DraftChunkPayload) String() string { func (*DraftChunkPayload) ProtoMessage() {} func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[175] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12634,7 +12741,7 @@ func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkPayload.ProtoReflect.Descriptor instead. func (*DraftChunkPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{173} + return file_openshell_proto_rawDescGZIP(), []int{175} } func (x *DraftChunkPayload) GetRuleName() string { @@ -12782,7 +12889,7 @@ type StoredPolicyRevision struct { func (x *StoredPolicyRevision) Reset() { *x = StoredPolicyRevision{} - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12794,7 +12901,7 @@ func (x *StoredPolicyRevision) String() string { func (*StoredPolicyRevision) ProtoMessage() {} func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[176] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12807,7 +12914,7 @@ func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredPolicyRevision.ProtoReflect.Descriptor instead. func (*StoredPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{174} + return file_openshell_proto_rawDescGZIP(), []int{176} } func (x *StoredPolicyRevision) GetId() string { @@ -12916,7 +13023,7 @@ type StoredDraftChunk struct { func (x *StoredDraftChunk) Reset() { *x = StoredDraftChunk{} - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12928,7 +13035,7 @@ func (x *StoredDraftChunk) String() string { func (*StoredDraftChunk) ProtoMessage() {} func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[177] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12941,7 +13048,7 @@ func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredDraftChunk.ProtoReflect.Descriptor instead. func (*StoredDraftChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{175} + return file_openshell_proto_rawDescGZIP(), []int{177} } func (x *StoredDraftChunk) GetId() string { @@ -13132,7 +13239,7 @@ type CreateWorkspaceRequest struct { func (x *CreateWorkspaceRequest) Reset() { *x = CreateWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13144,7 +13251,7 @@ func (x *CreateWorkspaceRequest) String() string { func (*CreateWorkspaceRequest) ProtoMessage() {} func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[178] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13157,7 +13264,7 @@ func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceRequest.ProtoReflect.Descriptor instead. func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{176} + return file_openshell_proto_rawDescGZIP(), []int{178} } func (x *CreateWorkspaceRequest) GetName() string { @@ -13184,7 +13291,7 @@ type CreateWorkspaceResponse struct { func (x *CreateWorkspaceResponse) Reset() { *x = CreateWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13196,7 +13303,7 @@ func (x *CreateWorkspaceResponse) String() string { func (*CreateWorkspaceResponse) ProtoMessage() {} func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[179] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13209,7 +13316,7 @@ func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceResponse.ProtoReflect.Descriptor instead. func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{177} + return file_openshell_proto_rawDescGZIP(), []int{179} } func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -13230,7 +13337,7 @@ type GetWorkspaceRequest struct { func (x *GetWorkspaceRequest) Reset() { *x = GetWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13242,7 +13349,7 @@ func (x *GetWorkspaceRequest) String() string { func (*GetWorkspaceRequest) ProtoMessage() {} func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[180] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13255,7 +13362,7 @@ func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceRequest.ProtoReflect.Descriptor instead. func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{178} + return file_openshell_proto_rawDescGZIP(), []int{180} } func (x *GetWorkspaceRequest) GetName() string { @@ -13275,7 +13382,7 @@ type GetWorkspaceResponse struct { func (x *GetWorkspaceResponse) Reset() { *x = GetWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13287,7 +13394,7 @@ func (x *GetWorkspaceResponse) String() string { func (*GetWorkspaceResponse) ProtoMessage() {} func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[181] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13300,7 +13407,7 @@ func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceResponse.ProtoReflect.Descriptor instead. func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{179} + return file_openshell_proto_rawDescGZIP(), []int{181} } func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -13323,7 +13430,7 @@ type ListWorkspacesRequest struct { func (x *ListWorkspacesRequest) Reset() { *x = ListWorkspacesRequest{} - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13335,7 +13442,7 @@ func (x *ListWorkspacesRequest) String() string { func (*ListWorkspacesRequest) ProtoMessage() {} func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[182] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13348,7 +13455,7 @@ func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesRequest.ProtoReflect.Descriptor instead. func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{180} + return file_openshell_proto_rawDescGZIP(), []int{182} } func (x *ListWorkspacesRequest) GetLimit() uint32 { @@ -13382,7 +13489,7 @@ type ListWorkspacesResponse struct { func (x *ListWorkspacesResponse) Reset() { *x = ListWorkspacesResponse{} - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13394,7 +13501,7 @@ func (x *ListWorkspacesResponse) String() string { func (*ListWorkspacesResponse) ProtoMessage() {} func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[183] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13407,7 +13514,7 @@ func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesResponse.ProtoReflect.Descriptor instead. func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{181} + return file_openshell_proto_rawDescGZIP(), []int{183} } func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { @@ -13428,7 +13535,7 @@ type DeleteWorkspaceRequest struct { func (x *DeleteWorkspaceRequest) Reset() { *x = DeleteWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13440,7 +13547,7 @@ func (x *DeleteWorkspaceRequest) String() string { func (*DeleteWorkspaceRequest) ProtoMessage() {} func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[184] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13453,7 +13560,7 @@ func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{182} + return file_openshell_proto_rawDescGZIP(), []int{184} } func (x *DeleteWorkspaceRequest) GetName() string { @@ -13473,7 +13580,7 @@ type DeleteWorkspaceResponse struct { func (x *DeleteWorkspaceResponse) Reset() { *x = DeleteWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[185] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13485,7 +13592,7 @@ func (x *DeleteWorkspaceResponse) String() string { func (*DeleteWorkspaceResponse) ProtoMessage() {} func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[185] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13498,7 +13605,7 @@ func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{183} + return file_openshell_proto_rawDescGZIP(), []int{185} } func (x *DeleteWorkspaceResponse) GetDeleted() bool { @@ -13522,7 +13629,7 @@ type WorkspaceMember struct { func (x *WorkspaceMember) Reset() { *x = WorkspaceMember{} - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[186] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13534,7 +13641,7 @@ func (x *WorkspaceMember) String() string { func (*WorkspaceMember) ProtoMessage() {} func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[186] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13547,7 +13654,7 @@ func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceMember.ProtoReflect.Descriptor instead. func (*WorkspaceMember) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{184} + return file_openshell_proto_rawDescGZIP(), []int{186} } func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { @@ -13586,7 +13693,7 @@ type AddWorkspaceMemberRequest struct { func (x *AddWorkspaceMemberRequest) Reset() { *x = AddWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[187] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13598,7 +13705,7 @@ func (x *AddWorkspaceMemberRequest) String() string { func (*AddWorkspaceMemberRequest) ProtoMessage() {} func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[185] + mi := &file_openshell_proto_msgTypes[187] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13611,7 +13718,7 @@ func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{185} + return file_openshell_proto_rawDescGZIP(), []int{187} } func (x *AddWorkspaceMemberRequest) GetWorkspace() string { @@ -13645,7 +13752,7 @@ type AddWorkspaceMemberResponse struct { func (x *AddWorkspaceMemberResponse) Reset() { *x = AddWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[188] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13657,7 +13764,7 @@ func (x *AddWorkspaceMemberResponse) String() string { func (*AddWorkspaceMemberResponse) ProtoMessage() {} func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[186] + mi := &file_openshell_proto_msgTypes[188] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13670,7 +13777,7 @@ func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{186} + return file_openshell_proto_rawDescGZIP(), []int{188} } func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { @@ -13693,7 +13800,7 @@ type RemoveWorkspaceMemberRequest struct { func (x *RemoveWorkspaceMemberRequest) Reset() { *x = RemoveWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[189] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13705,7 +13812,7 @@ func (x *RemoveWorkspaceMemberRequest) String() string { func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[187] + mi := &file_openshell_proto_msgTypes[189] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13718,7 +13825,7 @@ func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{187} + return file_openshell_proto_rawDescGZIP(), []int{189} } func (x *RemoveWorkspaceMemberRequest) GetWorkspace() string { @@ -13745,7 +13852,7 @@ type RemoveWorkspaceMemberResponse struct { func (x *RemoveWorkspaceMemberResponse) Reset() { *x = RemoveWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[190] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13757,7 +13864,7 @@ func (x *RemoveWorkspaceMemberResponse) String() string { func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[188] + mi := &file_openshell_proto_msgTypes[190] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13770,7 +13877,7 @@ func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{188} + return file_openshell_proto_rawDescGZIP(), []int{190} } func (x *RemoveWorkspaceMemberResponse) GetRemoved() bool { @@ -13793,7 +13900,7 @@ type ListWorkspaceMembersRequest struct { func (x *ListWorkspaceMembersRequest) Reset() { *x = ListWorkspaceMembersRequest{} - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[191] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13805,7 +13912,7 @@ func (x *ListWorkspaceMembersRequest) String() string { func (*ListWorkspaceMembersRequest) ProtoMessage() {} func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[189] + mi := &file_openshell_proto_msgTypes[191] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13818,7 +13925,7 @@ func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{189} + return file_openshell_proto_rawDescGZIP(), []int{191} } func (x *ListWorkspaceMembersRequest) GetWorkspace() string { @@ -13852,7 +13959,7 @@ type ListWorkspaceMembersResponse struct { func (x *ListWorkspaceMembersResponse) Reset() { *x = ListWorkspaceMembersResponse{} - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[192] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13864,7 +13971,7 @@ func (x *ListWorkspaceMembersResponse) String() string { func (*ListWorkspaceMembersResponse) ProtoMessage() {} func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[190] + mi := &file_openshell_proto_msgTypes[192] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13877,7 +13984,7 @@ func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{190} + return file_openshell_proto_rawDescGZIP(), []int{192} } func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { @@ -13905,7 +14012,7 @@ type ExtensionServiceCredential struct { func (x *ExtensionServiceCredential) Reset() { *x = ExtensionServiceCredential{} - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[193] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13917,7 +14024,7 @@ func (x *ExtensionServiceCredential) String() string { func (*ExtensionServiceCredential) ProtoMessage() {} func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[191] + mi := &file_openshell_proto_msgTypes[193] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13930,7 +14037,7 @@ func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtensionServiceCredential.ProtoReflect.Descriptor instead. func (*ExtensionServiceCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{191} + return file_openshell_proto_rawDescGZIP(), []int{193} } func (x *ExtensionServiceCredential) GetServiceName() string { @@ -14067,13 +14174,14 @@ const file_openshell_proto_rawDesc = "" + "\bmetadata\x18\x06 \x03(\v2).openshell.v1.PlatformEvent.MetadataEntryR\bmetadata\x1a;\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x91\x03\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd4\x03\n" + "\x14CreateSandboxRequest\x12-\n" + "\x04spec\x18\x01 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12F\n" + "\x06labels\x18\x03 \x03(\v2..openshell.v1.CreateSandboxRequest.LabelsEntryR\x06labels\x12U\n" + "\vannotations\x18\x04 \x03(\v23.openshell.v1.CreateSandboxRequest.AnnotationsEntryR\vannotations\x12\x1c\n" + - "\tworkspace\x18\x05 \x01(\tR\tworkspace\x1a9\n" + + "\tworkspace\x18\x05 \x01(\tR\tworkspace\x12A\n" + + "\x1dawait_main_process_attachment\x18\x06 \x01(\bR\x1aawaitMainProcessAttachment\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + @@ -14707,7 +14815,13 @@ const file_openshell_proto_rawDesc = "" + "\vinstance_id\x18\x02 \x01(\tR\n" + "instanceId\x12\x1b\n" + "\texit_code\x18\x03 \x01(\x05R\bexitCode\"\x1f\n" + - "\x1dReportMainProcessExitResponse\"\xb7\x01\n" + + "\x1dReportMainProcessExitResponse\"`\n" + + "\x1eFinalizeMainProcessExitRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1f\n" + + "\vinstance_id\x18\x02 \x01(\tR\n" + + "instanceId\"!\n" + + "\x1fFinalizeMainProcessExitResponse\"\xb7\x01\n" + "\tRelayOpen\x12\x1d\n" + "\n" + "channel_id\x18\x01 \x01(\tR\tchannelId\x120\n" + @@ -15029,7 +15143,7 @@ const file_openshell_proto_rawDesc = "" + "\x1aExtensionServiceCredential\x12!\n" + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x1a\n" + "\x05token\x18\x02 \x01(\tB\x04\x88\xb5\x18\x01R\x05token\x12\"\n" + - "\rexpires_at_ms\x18\x03 \x01(\x03R\vexpiresAtMs*\x89\x02\n" + + "\rexpires_at_ms\x18\x03 \x01(\x03R\vexpiresAtMs*\xa6\x02\n" + "\fSandboxPhase\x12\x1d\n" + "\x19SANDBOX_PHASE_UNSPECIFIED\x10\x00\x12\x1e\n" + "\x1aSANDBOX_PHASE_PROVISIONING\x10\x01\x12\x17\n" + @@ -15039,7 +15153,8 @@ const file_openshell_proto_rawDesc = "" + "\x15SANDBOX_PHASE_UNKNOWN\x10\x05\x12\x1a\n" + "\x16SANDBOX_PHASE_STOPPING\x10\x06\x12\x19\n" + "\x15SANDBOX_PHASE_STOPPED\x10\a\x12\x1a\n" + - "\x16SANDBOX_PHASE_STARTING\x10\b*\xce\x01\n" + + "\x16SANDBOX_PHASE_STARTING\x10\b\x12\x1b\n" + + "\x17SANDBOX_PHASE_COMPLETED\x10\t*\xce\x01\n" + " ProviderCredentialTokenGrantType\x124\n" + "0PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED\x10\x00\x12;\n" + "7PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS\x10\x01\x127\n" + @@ -15081,7 +15196,7 @@ const file_openshell_proto_rawDesc = "" + "1PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_RETRY\x10\x01\x12;\n" + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_REAUTHORIZE\x10\x02\x12A\n" + "=PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_FIX_CONFIGURATION\x10\x03\x12;\n" + - "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE\x10\x042\xacF\n" + + "7PROVIDER_CREDENTIAL_REFRESH_RECOVERY_ACTION_INVESTIGATE\x10\x042\xb4G\n" + "\tOpenShell\x12Z\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\"\x15\x82\xb5\x18\x11\n" + "\x0funauthenticated\x12i\n" + @@ -15181,6 +15296,8 @@ const file_openshell_proto_rawDesc = "" + "\x11ConnectSupervisor\x12\x1f.openshell.v1.SupervisorMessage\x1a\x1c.openshell.v1.GatewayMessage\"\r\x82\xb5\x18\t\n" + "\asandbox(\x010\x01\x12\x7f\n" + "\x15ReportMainProcessExit\x12*.openshell.v1.ReportMainProcessExitRequest\x1a+.openshell.v1.ReportMainProcessExitResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12\x85\x01\n" + + "\x17FinalizeMainProcessExit\x12,.openshell.v1.FinalizeMainProcessExitRequest\x1a-.openshell.v1.FinalizeMainProcessExitResponse\"\r\x82\xb5\x18\t\n" + "\asandbox\x12T\n" + "\vRelayStream\x12\x18.openshell.v1.RelayFrame\x1a\x18.openshell.v1.RelayFrame\"\r\x82\xb5\x18\t\n" + "\asandbox(\x010\x01\x12w\n" + @@ -15236,7 +15353,7 @@ func file_openshell_proto_rawDescGZIP() []byte { } var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 8) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 217) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 219) var file_openshell_proto_goTypes = []any{ (SandboxPhase)(0), // 0: openshell.v1.SandboxPhase (ProviderCredentialTokenGrantType)(0), // 1: openshell.v1.ProviderCredentialTokenGrantType @@ -15385,155 +15502,157 @@ var file_openshell_proto_goTypes = []any{ (*GatewayHeartbeat)(nil), // 144: openshell.v1.GatewayHeartbeat (*ReportMainProcessExitRequest)(nil), // 145: openshell.v1.ReportMainProcessExitRequest (*ReportMainProcessExitResponse)(nil), // 146: openshell.v1.ReportMainProcessExitResponse - (*RelayOpen)(nil), // 147: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 148: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 149: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 150: openshell.v1.RelayInit - (*RelayFrame)(nil), // 151: openshell.v1.RelayFrame - (*RelayOpenResult)(nil), // 152: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 153: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 154: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 155: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 156: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 157: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 158: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 159: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 160: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 161: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 162: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 163: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 164: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 165: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 166: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 167: openshell.v1.RejectDraftChunkResponse - (*DraftChunkApproval)(nil), // 168: openshell.v1.DraftChunkApproval - (*ApproveAllDraftChunksRequest)(nil), // 169: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 170: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 171: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 172: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 173: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 174: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 175: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 176: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 177: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 178: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 179: openshell.v1.GetDraftHistoryResponse - (*PolicyRevisionPayload)(nil), // 180: openshell.v1.PolicyRevisionPayload - (*DraftChunkPayload)(nil), // 181: openshell.v1.DraftChunkPayload - (*StoredPolicyRevision)(nil), // 182: openshell.v1.StoredPolicyRevision - (*StoredDraftChunk)(nil), // 183: openshell.v1.StoredDraftChunk - (*CreateWorkspaceRequest)(nil), // 184: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 185: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 186: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 187: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 188: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 189: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 190: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 191: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 192: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 193: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 194: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 195: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 196: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 197: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 198: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 199: openshell.v1.ExtensionServiceCredential - nil, // 200: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 201: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 202: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 203: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 204: openshell.v1.PlatformEvent.MetadataEntry - nil, // 205: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 206: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 207: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 208: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 209: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - nil, // 210: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - nil, // 211: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - nil, // 212: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry - nil, // 213: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 214: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 215: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 216: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - nil, // 217: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 218: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 219: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 220: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 221: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 222: openshell.v1.PolicyRevisionPayload.ProvenanceEntry - nil, // 223: openshell.v1.StoredPolicyRevision.ProvenanceEntry - nil, // 224: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*datamodelv1.ObjectMeta)(nil), // 225: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 226: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 227: google.protobuf.Struct - (*datamodelv1.Provider)(nil), // 228: openshell.datamodel.v1.Provider - (*datamodelv1.CredentialHandle)(nil), // 229: openshell.datamodel.v1.CredentialHandle - (*sandboxv1.NetworkEndpoint)(nil), // 230: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 231: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 232: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 233: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 234: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 235: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 236: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 237: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 238: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 239: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 240: openshell.sandbox.v1.GetGatewayConfigResponse + (*FinalizeMainProcessExitRequest)(nil), // 147: openshell.v1.FinalizeMainProcessExitRequest + (*FinalizeMainProcessExitResponse)(nil), // 148: openshell.v1.FinalizeMainProcessExitResponse + (*RelayOpen)(nil), // 149: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 150: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 151: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 152: openshell.v1.RelayInit + (*RelayFrame)(nil), // 153: openshell.v1.RelayFrame + (*RelayOpenResult)(nil), // 154: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 155: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 156: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 157: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 158: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 159: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 160: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 161: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 162: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 163: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 164: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 165: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 166: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 167: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 168: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 169: openshell.v1.RejectDraftChunkResponse + (*DraftChunkApproval)(nil), // 170: openshell.v1.DraftChunkApproval + (*ApproveAllDraftChunksRequest)(nil), // 171: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 172: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 173: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 174: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 175: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 176: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 177: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 178: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 179: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 180: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 181: openshell.v1.GetDraftHistoryResponse + (*PolicyRevisionPayload)(nil), // 182: openshell.v1.PolicyRevisionPayload + (*DraftChunkPayload)(nil), // 183: openshell.v1.DraftChunkPayload + (*StoredPolicyRevision)(nil), // 184: openshell.v1.StoredPolicyRevision + (*StoredDraftChunk)(nil), // 185: openshell.v1.StoredDraftChunk + (*CreateWorkspaceRequest)(nil), // 186: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 187: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 188: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 189: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 190: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 191: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 192: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 193: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 194: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 195: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 196: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 197: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 198: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 199: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 200: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 201: openshell.v1.ExtensionServiceCredential + nil, // 202: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 203: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 204: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 205: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 206: openshell.v1.PlatformEvent.MetadataEntry + nil, // 207: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 208: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 209: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 210: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 211: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + nil, // 212: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + nil, // 213: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + nil, // 214: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + nil, // 215: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 216: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 217: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 218: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + nil, // 219: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 220: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 221: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 222: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 223: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 224: openshell.v1.PolicyRevisionPayload.ProvenanceEntry + nil, // 225: openshell.v1.StoredPolicyRevision.ProvenanceEntry + nil, // 226: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*datamodelv1.ObjectMeta)(nil), // 227: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 228: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 229: google.protobuf.Struct + (*datamodelv1.Provider)(nil), // 230: openshell.datamodel.v1.Provider + (*datamodelv1.CredentialHandle)(nil), // 231: openshell.datamodel.v1.CredentialHandle + (*sandboxv1.NetworkEndpoint)(nil), // 232: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 233: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 234: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 235: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 236: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 237: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 238: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 239: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 240: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 241: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 242: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 199, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 201, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential 5, // 1: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus 5, // 2: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus 18, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo 19, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 225, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 227, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 21, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec 25, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 200, // 8: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 202, // 8: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry 24, // 9: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 226, // 10: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 228, // 10: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy 22, // 11: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements 23, // 12: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 201, // 13: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 202, // 14: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 203, // 15: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 227, // 16: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 227, // 17: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 203, // 13: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 204, // 14: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 205, // 15: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 229, // 16: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 229, // 17: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct 26, // 18: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 0, // 19: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 204, // 20: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 206, // 20: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry 21, // 21: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 205, // 22: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 206, // 23: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 207, // 22: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 208, // 23: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry 20, // 24: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox 20, // 25: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 228, // 26: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 230, // 26: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider 20, // 27: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox 20, // 28: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox 52, // 29: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 225, // 30: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 227, // 30: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 51, // 31: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 207, // 32: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 209, // 32: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry 56, // 33: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout 57, // 34: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr 58, // 35: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 148, // 36: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 149, // 37: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 150, // 36: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 151, // 37: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget 60, // 38: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit 55, // 39: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest 63, // 40: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 225, // 41: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 227, // 41: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 20, // 42: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox 67, // 43: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine 27, // 44: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent 68, // 45: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 159, // 46: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 208, // 47: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 228, // 48: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 228, // 49: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 209, // 50: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 228, // 51: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 228, // 52: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 161, // 46: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 210, // 47: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 230, // 48: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 230, // 49: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 211, // 50: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + 230, // 51: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 230, // 52: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider 99, // 53: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile 80, // 54: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride 1, // 55: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType @@ -15545,26 +15664,26 @@ var file_openshell_proto_depIdxs = []int32{ 85, // 61: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput 2, // 62: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy 7, // 63: openshell.v1.ProviderCredentialRefreshStatus.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 225, // 64: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 227, // 64: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 2, // 65: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 210, // 66: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - 211, // 67: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - 212, // 68: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry + 212, // 66: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + 213, // 67: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + 214, // 68: openshell.v1.StoredProviderCredentialRefreshState.secret_material_handles:type_name -> openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry 90, // 69: openshell.v1.StoredProviderCredentialRefreshState.pending_secret_deletions:type_name -> openshell.v1.StoredRefreshMaterialDeletion 7, // 70: openshell.v1.StoredProviderCredentialRefreshState.recovery_action:type_name -> openshell.v1.ProviderCredentialRefreshRecoveryAction - 229, // 71: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle + 231, // 71: openshell.v1.StoredRefreshMaterialDeletion.handle:type_name -> openshell.datamodel.v1.CredentialHandle 87, // 72: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus 2, // 73: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 213, // 74: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 215, // 74: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry 87, // 75: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus 87, // 76: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus 3, // 77: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory 83, // 78: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 230, // 79: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 231, // 80: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 232, // 79: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 233, // 80: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary 88, // 81: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 214, // 82: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 225, // 83: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 216, // 82: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 227, // 83: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 99, // 84: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile 99, // 85: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile 99, // 86: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile @@ -15577,74 +15696,74 @@ var file_openshell_proto_depIdxs = []int32{ 78, // 93: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem 79, // 94: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic 113, // 95: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 215, // 96: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 216, // 97: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 217, // 98: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 218, // 99: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 226, // 100: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 232, // 101: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 217, // 96: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 218, // 97: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + 219, // 98: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 220, // 99: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 228, // 100: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 234, // 101: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue 119, // 102: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 219, // 103: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 221, // 103: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry 120, // 104: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule 121, // 105: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint 122, // 106: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule 123, // 107: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules 124, // 108: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules 125, // 109: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 233, // 110: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 234, // 111: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 235, // 112: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 220, // 113: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 235, // 110: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 236, // 111: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 237, // 112: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 222, // 113: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry 133, // 114: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision 133, // 115: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision 4, // 116: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus 4, // 117: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 226, // 118: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 221, // 119: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 228, // 118: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 223, // 119: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry 67, // 120: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine 67, // 121: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine 140, // 122: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello 143, // 123: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 152, // 124: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 153, // 125: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 154, // 124: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 155, // 125: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose 141, // 126: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted 142, // 127: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected 144, // 128: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 147, // 129: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 153, // 130: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 148, // 131: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 149, // 132: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 150, // 133: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 154, // 134: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 156, // 135: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 233, // 136: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 226, // 137: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 226, // 138: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 155, // 139: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 158, // 140: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 157, // 141: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 158, // 142: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 168, // 143: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval - 233, // 144: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 178, // 145: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 226, // 146: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 222, // 147: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry - 233, // 148: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 226, // 149: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 226, // 150: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 223, // 151: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry - 226, // 152: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 226, // 153: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 224, // 154: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 236, // 155: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 236, // 156: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 236, // 157: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 225, // 158: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 149, // 129: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 155, // 130: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 150, // 131: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 151, // 132: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 152, // 133: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 156, // 134: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 158, // 135: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 235, // 136: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 228, // 137: openshell.v1.PolicyChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 228, // 138: openshell.v1.PolicyChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 157, // 139: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 160, // 140: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 159, // 141: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 160, // 142: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 170, // 143: openshell.v1.ApproveAllDraftChunksRequest.approvals:type_name -> openshell.v1.DraftChunkApproval + 235, // 144: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 180, // 145: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 228, // 146: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 224, // 147: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry + 235, // 148: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 228, // 149: openshell.v1.DraftChunkPayload.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 228, // 150: openshell.v1.DraftChunkPayload.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 225, // 151: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry + 228, // 152: openshell.v1.StoredDraftChunk.current_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 228, // 153: openshell.v1.StoredDraftChunk.candidate_effective_policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 226, // 154: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 238, // 155: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 238, // 156: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 238, // 157: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 227, // 158: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta 6, // 159: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole 6, // 160: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 192, // 161: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 192, // 162: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 229, // 163: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle + 194, // 161: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 194, // 162: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 231, // 163: openshell.v1.StoredProviderCredentialRefreshState.SecretMaterialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle 83, // 164: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential 114, // 165: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding 12, // 166: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest @@ -15683,8 +15802,8 @@ var file_openshell_proto_depIdxs = []int32{ 97, // 199: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest 73, // 200: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest 110, // 201: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 237, // 202: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 238, // 203: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 239, // 202: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 240, // 203: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest 118, // 204: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest 127, // 205: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest 129, // 206: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest @@ -15695,96 +15814,98 @@ var file_openshell_proto_depIdxs = []int32{ 135, // 211: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest 138, // 212: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage 145, // 213: openshell.v1.OpenShell.ReportMainProcessExit:input_type -> openshell.v1.ReportMainProcessExitRequest - 151, // 214: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 65, // 215: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 160, // 216: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 162, // 217: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 164, // 218: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 166, // 219: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 169, // 220: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 171, // 221: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 173, // 222: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 175, // 223: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 177, // 224: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 8, // 225: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 10, // 226: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 184, // 227: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 186, // 228: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 188, // 229: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 190, // 230: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 193, // 231: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 195, // 232: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 197, // 233: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 13, // 234: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 15, // 235: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 17, // 236: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 37, // 237: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 37, // 238: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 38, // 239: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 39, // 240: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 40, // 241: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 41, // 242: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 42, // 243: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 37, // 244: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 37, // 245: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 44, // 246: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 52, // 247: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 52, // 248: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 48, // 249: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 50, // 250: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 54, // 251: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 59, // 252: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 61, // 253: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 59, // 254: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 74, // 255: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 74, // 256: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 75, // 257: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 102, // 258: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 101, // 259: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 104, // 260: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 106, // 261: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 108, // 262: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 74, // 263: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 92, // 264: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 94, // 265: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 96, // 266: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 98, // 267: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 109, // 268: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 111, // 269: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 239, // 270: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 240, // 271: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 126, // 272: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 128, // 273: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 130, // 274: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 132, // 275: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 115, // 276: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 117, // 277: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse - 137, // 278: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 136, // 279: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 139, // 280: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 146, // 281: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse - 151, // 282: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 66, // 283: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 161, // 284: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 163, // 285: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 165, // 286: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 167, // 287: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 170, // 288: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 172, // 289: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 174, // 290: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 176, // 291: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 179, // 292: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 9, // 293: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 11, // 294: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 185, // 295: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 187, // 296: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 189, // 297: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 191, // 298: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 194, // 299: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 196, // 300: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 198, // 301: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 234, // [234:302] is the sub-list for method output_type - 166, // [166:234] is the sub-list for method input_type + 147, // 214: openshell.v1.OpenShell.FinalizeMainProcessExit:input_type -> openshell.v1.FinalizeMainProcessExitRequest + 153, // 215: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 65, // 216: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 162, // 217: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 164, // 218: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 166, // 219: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 168, // 220: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 171, // 221: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 173, // 222: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 175, // 223: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 177, // 224: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 179, // 225: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 8, // 226: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 10, // 227: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 186, // 228: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 188, // 229: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 190, // 230: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 192, // 231: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 195, // 232: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 197, // 233: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 199, // 234: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 13, // 235: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 15, // 236: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 17, // 237: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 37, // 238: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 37, // 239: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 38, // 240: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 39, // 241: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 40, // 242: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 41, // 243: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 42, // 244: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 37, // 245: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 37, // 246: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 44, // 247: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 52, // 248: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 52, // 249: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 48, // 250: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 50, // 251: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 54, // 252: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 59, // 253: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 61, // 254: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 59, // 255: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 74, // 256: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 74, // 257: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 75, // 258: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 102, // 259: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 101, // 260: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 104, // 261: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 106, // 262: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 108, // 263: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 74, // 264: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 92, // 265: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 94, // 266: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 96, // 267: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 98, // 268: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 109, // 269: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 111, // 270: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 241, // 271: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 242, // 272: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 126, // 273: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 128, // 274: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 130, // 275: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 132, // 276: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 115, // 277: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 117, // 278: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 137, // 279: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 136, // 280: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 139, // 281: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 146, // 282: openshell.v1.OpenShell.ReportMainProcessExit:output_type -> openshell.v1.ReportMainProcessExitResponse + 148, // 283: openshell.v1.OpenShell.FinalizeMainProcessExit:output_type -> openshell.v1.FinalizeMainProcessExitResponse + 153, // 284: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 66, // 285: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 163, // 286: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 165, // 287: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 167, // 288: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 169, // 289: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 172, // 290: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 174, // 291: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 176, // 292: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 178, // 293: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 181, // 294: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 9, // 295: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 11, // 296: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 187, // 297: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 189, // 298: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 191, // 299: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 193, // 300: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 196, // 301: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 198, // 302: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 200, // 303: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 235, // [235:304] is the sub-list for method output_type + 166, // [166:235] is the sub-list for method input_type 166, // [166:166] is the sub-list for extension type_name 166, // [166:166] is the sub-list for extension extendee 0, // [0:166] is the sub-list for field type_name @@ -15845,23 +15966,23 @@ func file_openshell_proto_init() { (*GatewayMessage_RelayOpen)(nil), (*GatewayMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[139].OneofWrappers = []any{ + file_openshell_proto_msgTypes[141].OneofWrappers = []any{ (*RelayOpen_Ssh)(nil), (*RelayOpen_Tcp)(nil), } - file_openshell_proto_msgTypes[143].OneofWrappers = []any{ + file_openshell_proto_msgTypes[145].OneofWrappers = []any{ (*RelayFrame_Init)(nil), (*RelayFrame_Data)(nil), } - file_openshell_proto_msgTypes[174].OneofWrappers = []any{} - file_openshell_proto_msgTypes[175].OneofWrappers = []any{} + file_openshell_proto_msgTypes[176].OneofWrappers = []any{} + file_openshell_proto_msgTypes[177].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), NumEnums: 8, - NumMessages: 217, + NumMessages: 219, NumExtensions: 0, NumServices: 1, }, diff --git a/sdk/go/proto/openshellv1/openshell_grpc.pb.go b/sdk/go/proto/openshellv1/openshell_grpc.pb.go index 663c09aed5..0d98c66ac8 100644 --- a/sdk/go/proto/openshellv1/openshell_grpc.pb.go +++ b/sdk/go/proto/openshellv1/openshell_grpc.pb.go @@ -71,6 +71,7 @@ const ( OpenShell_PushSandboxLogs_FullMethodName = "/openshell.v1.OpenShell/PushSandboxLogs" OpenShell_ConnectSupervisor_FullMethodName = "/openshell.v1.OpenShell/ConnectSupervisor" OpenShell_ReportMainProcessExit_FullMethodName = "/openshell.v1.OpenShell/ReportMainProcessExit" + OpenShell_FinalizeMainProcessExit_FullMethodName = "/openshell.v1.OpenShell/FinalizeMainProcessExit" OpenShell_RelayStream_FullMethodName = "/openshell.v1.OpenShell/RelayStream" OpenShell_WatchSandbox_FullMethodName = "/openshell.v1.OpenShell/WatchSandbox" OpenShell_SubmitPolicyAnalysis_FullMethodName = "/openshell.v1.OpenShell/SubmitPolicyAnalysis" @@ -218,6 +219,8 @@ type OpenShellClient interface { ConnectSupervisor(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SupervisorMessage, GatewayMessage], error) // Persist the canonical main process result before the supervisor exits. ReportMainProcessExit(ctx context.Context, in *ReportMainProcessExitRequest, opts ...grpc.CallOption) (*ReportMainProcessExitResponse, error) + // Confirm that foreground terminal delivery completed naturally. + FinalizeMainProcessExit(ctx context.Context, in *FinalizeMainProcessExitRequest, opts ...grpc.CallOption) (*FinalizeMainProcessExitResponse, error) // Raw byte relay between supervisor and gateway. // // The supervisor initiates this call after receiving a RelayOpen message @@ -793,6 +796,16 @@ func (c *openShellClient) ReportMainProcessExit(ctx context.Context, in *ReportM return out, nil } +func (c *openShellClient) FinalizeMainProcessExit(ctx context.Context, in *FinalizeMainProcessExitRequest, opts ...grpc.CallOption) (*FinalizeMainProcessExitResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(FinalizeMainProcessExitResponse) + err := c.cc.Invoke(ctx, OpenShell_FinalizeMainProcessExit_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) RelayStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[RelayFrame, RelayFrame], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[5], OpenShell_RelayStream_FullMethodName, cOpts...) @@ -1130,6 +1143,8 @@ type OpenShellServer interface { ConnectSupervisor(grpc.BidiStreamingServer[SupervisorMessage, GatewayMessage]) error // Persist the canonical main process result before the supervisor exits. ReportMainProcessExit(context.Context, *ReportMainProcessExitRequest) (*ReportMainProcessExitResponse, error) + // Confirm that foreground terminal delivery completed naturally. + FinalizeMainProcessExit(context.Context, *FinalizeMainProcessExitRequest) (*FinalizeMainProcessExitResponse, error) // Raw byte relay between supervisor and gateway. // // The supervisor initiates this call after receiving a RelayOpen message @@ -1348,6 +1363,9 @@ func (UnimplementedOpenShellServer) ConnectSupervisor(grpc.BidiStreamingServer[S func (UnimplementedOpenShellServer) ReportMainProcessExit(context.Context, *ReportMainProcessExitRequest) (*ReportMainProcessExitResponse, error) { return nil, status.Error(codes.Unimplemented, "method ReportMainProcessExit not implemented") } +func (UnimplementedOpenShellServer) FinalizeMainProcessExit(context.Context, *FinalizeMainProcessExitRequest) (*FinalizeMainProcessExitResponse, error) { + return nil, status.Error(codes.Unimplemented, "method FinalizeMainProcessExit not implemented") +} func (UnimplementedOpenShellServer) RelayStream(grpc.BidiStreamingServer[RelayFrame, RelayFrame]) error { return status.Error(codes.Unimplemented, "method RelayStream not implemented") } @@ -2242,6 +2260,24 @@ func _OpenShell_ReportMainProcessExit_Handler(srv interface{}, ctx context.Conte return interceptor(ctx, in, info, handler) } +func _OpenShell_FinalizeMainProcessExit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FinalizeMainProcessExitRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).FinalizeMainProcessExit(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_FinalizeMainProcessExit_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).FinalizeMainProcessExit(ctx, req.(*FinalizeMainProcessExitRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_RelayStream_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(OpenShellServer).RelayStream(&grpc.GenericServerStream[RelayFrame, RelayFrame]{ServerStream: stream}) } @@ -2763,6 +2799,10 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "ReportMainProcessExit", Handler: _OpenShell_ReportMainProcessExit_Handler, }, + { + MethodName: "FinalizeMainProcessExit", + Handler: _OpenShell_FinalizeMainProcessExit_Handler, + }, { MethodName: "SubmitPolicyAnalysis", Handler: _OpenShell_SubmitPolicyAnalysis_Handler, diff --git a/sdk/typescript/src/client.test.ts b/sdk/typescript/src/client.test.ts index 1c08e423ca..29a7438c69 100644 --- a/sdk/typescript/src/client.test.ts +++ b/sdk/typescript/src/client.test.ts @@ -291,6 +291,30 @@ describe('create', () => { }); describe('waits', () => { + it('waitReady accepts successful main-process completion', async () => { + const sandbox = client({ + getSandbox: () => ({ + sandbox: { + metadata: { id: 'sb-id', name: 'sb' }, + status: { phase: SandboxPhase.COMPLETED, exitCode: 0 }, + }, + }), + }); + await expect(sandbox.waitReady('sb', 1)).resolves.toMatchObject({ phase: 'completed', exitCode: 0 }); + }); + + it('waitReady rejects stopped main-process results without waiting for timeout', async () => { + const sandbox = client({ + getSandbox: () => ({ + sandbox: { + metadata: { id: 'sb-id', name: 'sb' }, + status: { phase: SandboxPhase.STOPPED, exitCode: 7 }, + }, + }), + }); + await expect(sandbox.waitReady('sb', 30)).rejects.toMatchObject({ code: 'connect' }); + }); + it('waitReady rejects rather than hanging when get() never resolves', async () => { const sandbox = client({ // Only settles when the per-poll deadline signal aborts the call. diff --git a/sdk/typescript/src/client.ts b/sdk/typescript/src/client.ts index b6db97943f..3f3247ca2a 100644 --- a/sdk/typescript/src/client.ts +++ b/sdk/typescript/src/client.ts @@ -59,7 +59,8 @@ export type SandboxPhaseName = | 'unknown' | 'stopping' | 'stopped' - | 'starting'; + | 'starting' + | 'completed'; /** Lowercase mirror of the generated `ServiceStatus` enum. Hand-maintained. */ export type HealthStatus = 'unspecified' | 'healthy' | 'degraded' | 'unhealthy'; @@ -293,6 +294,7 @@ export const PHASE_NAMES: Record = { [SandboxPhase.STOPPING]: 'stopping', [SandboxPhase.STOPPED]: 'stopped', [SandboxPhase.STARTING]: 'starting', + [SandboxPhase.COMPLETED]: 'completed', }; export const STATUS_NAMES: Record = { [ServiceStatus.UNSPECIFIED]: 'unspecified', @@ -632,7 +634,8 @@ export class SandboxClient { } catch (e) { throw mapWaitError(e, name, deadline, signal); } - if (ref.phase === 'ready') return ref; + if (ref.phase === 'ready' || ref.phase === 'completed') return ref; + if (ref.phase === 'stopped') throw new SdkError('connect', `sandbox '${name}' stopped before becoming ready`); if (ref.phase === 'error') throw new SdkError('connect', `sandbox '${name}' entered error phase`); if (Date.now() >= deadline) throw new SdkError('connect', `timed out waiting for sandbox '${name}'`); await waitSleep(delay, deadline, signal);