diff --git a/architecture/compute-runtimes.md b/architecture/compute-runtimes.md index f122fda5d7..4b60130537 100644 --- a/architecture/compute-runtimes.md +++ b/architecture/compute-runtimes.md @@ -26,6 +26,10 @@ The capability RPC reports driver identity, version, and the default sandbox image used by the gateway. GPU availability stays driver-local and is validated when a sandbox create request asks for GPU resources. +The gateway records driver identity and version from the startup capability +response. Elevated gateway info reports that initialized driver snapshot instead +of re-querying drivers on each request. + ## Runtime Summary | Runtime | Best fit | Sandbox boundary | Notes | diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index e02c7c9cd5..8f98bd9320 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -535,6 +535,14 @@ enum Commands { #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] Status, + /// Show elevated live gateway runtime information. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + Info { + /// Output format. + #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)] + output: OutputFormat, + }, + /// Manage inference configuration. #[command(after_help = INFERENCE_EXAMPLES, help_template = SUBCOMMAND_HELP_TEMPLATE)] Inference { @@ -2100,6 +2108,19 @@ async fn main() -> Result<()> { } } + // ----------------------------------------------------------- + // Top-level info + // ----------------------------------------------------------- + Some(Commands::Info { output }) => { + if let Ok(ctx) = resolve_gateway(&cli.gateway, &cli.gateway_endpoint) { + let mut tls = tls.with_gateway_name(&ctx.name); + apply_auth(&mut tls, &ctx.name); + run::gateway_info(&ctx.name, &ctx.endpoint, &tls, output.as_str()).await?; + } else { + run::gateway_info_not_configured()?; + } + } + // ----------------------------------------------------------- // Top-level forward (was `sandbox forward`) // ----------------------------------------------------------- @@ -3466,6 +3487,19 @@ mod tests { assert!(matches!(cli.command, Some(Commands::Status))); } + #[test] + fn info_accepts_output_json() { + let cli = Cli::try_parse_from(["openshell", "info", "-o", "json"]) + .expect("info -o json should parse"); + + assert!(matches!( + cli.command, + Some(Commands::Info { + output: OutputFormat::Json + }) + )); + } + #[test] fn hidden_aliases_still_parse() { let cli = Cli::try_parse_from(["openshell", "lg", "sandbox-1"]) diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index f56bb71512..e75b1f213b 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -40,20 +40,21 @@ use openshell_core::proto::{ DeleteProviderRefreshRequest, DeleteProviderRequest, DeleteSandboxRequest, DeleteServiceRequest, DetachSandboxProviderRequest, ExecSandboxRequest, ExposeServiceRequest, GetClusterInferenceRequest, GetDraftHistoryRequest, GetDraftPolicyRequest, - GetGatewayConfigRequest, GetProviderProfileRequest, GetProviderRefreshStatusRequest, - GetProviderRequest, GetSandboxConfigRequest, GetSandboxLogsRequest, - GetSandboxPolicyStatusRequest, GetSandboxRequest, GetServiceRequest, GpuResourceRequirements, - HealthRequest, ImportProviderProfilesRequest, LintProviderProfilesRequest, - ListProviderProfilesRequest, ListProvidersRequest, ListSandboxPoliciesRequest, - ListSandboxProvidersRequest, ListSandboxesRequest, ListServicesRequest, PlatformEvent, - PolicySource, PolicyStatus, Provider, ProviderCredentialRefreshStatus, - ProviderCredentialRefreshStrategy, ProviderProfile, ProviderProfileDiagnostic, - ProviderProfileImportItem, RejectDraftChunkRequest, ResourceRequirements, - RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, SandboxPhase, SandboxPolicy, - SandboxSpec, SandboxTemplate, ServiceEndpointResponse, SetClusterInferenceRequest, - SettingScope, SettingValue, TcpForwardFrame, TcpForwardInit, TcpRelayTarget, - UpdateConfigRequest, UpdateProviderProfilesRequest, UpdateProviderRequest, WatchSandboxRequest, - exec_sandbox_event, setting_value, tcp_forward_init, + GetGatewayConfigRequest, GetGatewayInfoRequest, GetProviderProfileRequest, + GetProviderRefreshStatusRequest, GetProviderRequest, GetSandboxConfigRequest, + GetSandboxLogsRequest, GetSandboxPolicyStatusRequest, GetSandboxRequest, GetServiceRequest, + GpuResourceRequirements, HealthRequest, ImportProviderProfilesRequest, + LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest, + ListSandboxPoliciesRequest, ListSandboxProvidersRequest, ListSandboxesRequest, + ListServicesRequest, PlatformEvent, PolicySource, PolicyStatus, Provider, + ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy, ProviderProfile, + ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest, + ResourceRequirements, RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, + SandboxPhase, SandboxPolicy, SandboxSpec, SandboxTemplate, ServiceEndpointResponse, + ServiceStatus, SetClusterInferenceRequest, SettingScope, SettingValue, TcpForwardFrame, + TcpForwardInit, TcpRelayTarget, UpdateConfigRequest, UpdateProviderProfilesRequest, + UpdateProviderRequest, WatchSandboxRequest, exec_sandbox_event, setting_value, + tcp_forward_init, }; use openshell_core::settings::{self, SettingValueKind}; use openshell_core::{ObjectId, ObjectName}; @@ -525,6 +526,28 @@ fn print_sandbox_header(sandbox: &Sandbox, display: Option<&ProvisioningDisplay> } } +#[derive(Debug, Clone)] +struct GatewayInfoView { + gateway: String, + server: String, + auth: Option<&'static str>, + status: String, + version: String, + compute_drivers: Vec, +} + +#[derive(Debug, Clone)] +struct ComputeDriverInfoView { + name: String, + capabilities: ComputeDriverCapabilitiesView, +} + +#[derive(Debug, Clone)] +struct ComputeDriverCapabilitiesView { + driver_name: String, + driver_version: String, +} + /// Show gateway status. #[allow(clippy::branches_sharing_code)] pub async fn gateway_status(gateway_name: &str, server: &str, tls: &TlsOptions) -> Result<()> { @@ -582,6 +605,129 @@ pub async fn gateway_status(gateway_name: &str, server: &str, tls: &TlsOptions) Ok(()) } +fn gateway_service_status_name(status: i32) -> &'static str { + match ServiceStatus::try_from(status) { + Ok(ServiceStatus::Healthy) => "healthy", + Ok(ServiceStatus::Degraded) => "degraded", + Ok(ServiceStatus::Unhealthy) => "unhealthy", + Ok(ServiceStatus::Unspecified) | Err(_) => "unknown", + } +} + +/// Show elevated gateway runtime information. +pub async fn gateway_info( + gateway_name: &str, + server: &str, + tls: &TlsOptions, + output: &str, +) -> Result<()> { + let mut client = grpc_client(server, tls).await?; + let info = client + .get_gateway_info(GetGatewayInfoRequest {}) + .await + .map_err(|err| match err.code() { + Code::Unimplemented => { + miette!("gateway info is not supported by this gateway version") + } + Code::PermissionDenied => miette!("gateway info requires admin privileges: {err}"), + _ => miette!("get_gateway_info failed: {err}"), + })? + .into_inner(); + + let view = GatewayInfoView { + gateway: gateway_name.to_string(), + server: server.to_string(), + auth: tls.is_bearer_auth().then_some("bearer"), + status: gateway_service_status_name(info.status).to_string(), + version: info.gateway_version, + compute_drivers: info + .compute_drivers + .into_iter() + .map(|driver| { + let capabilities = driver.capabilities.unwrap_or_default(); + ComputeDriverInfoView { + name: driver.name, + capabilities: ComputeDriverCapabilitiesView { + driver_name: capabilities.driver_name, + driver_version: capabilities.driver_version, + }, + } + }) + .collect(), + }; + + print_gateway_info(&view, output) +} + +pub fn gateway_info_not_configured() -> Result<()> { + Err(miette!( + "No gateway configured.\nRegister a gateway with: openshell gateway add " + )) +} + +fn print_gateway_info(view: &GatewayInfoView, output: &str) -> Result<()> { + if crate::output::print_output_single(output, view, gateway_info_to_json)? { + return Ok(()); + } + + println!("{}", "Gateway Info".cyan().bold()); + println!(); + println!(" {} {}", "Gateway:".dimmed(), view.gateway); + println!(" {} {}", "Server:".dimmed(), view.server); + if view.auth.is_some() { + println!(" {} Edge (bearer token)", "Auth:".dimmed()); + } + println!(" {} {}", "Status:".dimmed(), view.status); + println!(" {} {}", "Version:".dimmed(), view.version); + print_compute_driver_info(&view.compute_drivers); + + Ok(()) +} + +fn print_compute_driver_info(drivers: &[ComputeDriverInfoView]) { + if drivers.is_empty() { + return; + } + + println!(" {}", "Compute drivers:".dimmed()); + for driver in drivers { + println!(" {}", driver.name); + if driver.capabilities.driver_name != driver.name { + println!( + " {} {}", + "Driver name:".dimmed(), + driver.capabilities.driver_name + ); + } + println!( + " {} {}", + "Driver version:".dimmed(), + driver.capabilities.driver_version + ); + } +} + +fn gateway_info_to_json(view: &GatewayInfoView) -> serde_json::Value { + serde_json::json!({ + "gateway": &view.gateway, + "server": &view.server, + "auth": view.auth, + "status": &view.status, + "version": &view.version, + "compute_drivers": view + .compute_drivers + .iter() + .map(|driver| serde_json::json!({ + "name": &driver.name, + "capabilities": { + "driver_name": &driver.capabilities.driver_name, + "driver_version": &driver.capabilities.driver_version, + }, + })) + .collect::>(), + }) +} + /// Set the active gateway. pub fn gateway_use(name: &str) -> Result<()> { // Verify the gateway exists @@ -7838,10 +7984,11 @@ fn format_timestamp_ms(ms: i64) -> String { #[cfg(test)] mod tests { use super::{ - ProvisioningStep, TlsOptions, build_sandbox_resource_limits, - dockerfile_sources_supported_for_gateway, format_endpoint, format_gateway_select_header, - format_gateway_select_items, format_provider_attachment_table, gateway_add, - gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_to_json, + ComputeDriverCapabilitiesView, ComputeDriverInfoView, GatewayInfoView, ProvisioningStep, + TlsOptions, build_sandbox_resource_limits, dockerfile_sources_supported_for_gateway, + format_endpoint, format_gateway_select_header, format_gateway_select_items, + format_provider_attachment_table, gateway_add, gateway_auth_label, + gateway_env_override_warning, gateway_info_to_json, gateway_select_with, gateway_to_json, gateway_type_label, git_sync_files, http_health_check, import_local_package_mtls_bundle, inferred_provider_type, mtls_certs_exist_for_gateway, package_managed_tls_dirs, parse_cli_setting_value, parse_credential_expiry_cli_value, parse_credential_expiry_pairs, @@ -8889,6 +9036,59 @@ mod tests { assert_eq!(json["active"], true); } + #[test] + fn gateway_info_json_includes_compute_drivers_when_available() { + let view = GatewayInfoView { + gateway: "openshell".to_string(), + server: "https://127.0.0.1:17670".to_string(), + auth: Some("bearer"), + status: "healthy".to_string(), + version: "0.0.75".to_string(), + compute_drivers: vec![ComputeDriverInfoView { + name: "podman".to_string(), + capabilities: ComputeDriverCapabilitiesView { + driver_name: "podman".to_string(), + driver_version: "0.0.75".to_string(), + }, + }], + }; + + let json = gateway_info_to_json(&view); + + assert_eq!(json["gateway"], "openshell"); + assert_eq!(json["status"], "healthy"); + assert_eq!(json["version"], "0.0.75"); + assert_eq!(json["compute_drivers"][0]["name"], "podman"); + assert_eq!( + json["compute_drivers"][0]["capabilities"]["driver_name"], + "podman" + ); + assert_eq!( + json["compute_drivers"][0]["capabilities"]["driver_version"], + "0.0.75" + ); + } + + #[test] + fn gateway_info_json_includes_empty_compute_driver_list() { + let view = GatewayInfoView { + gateway: "openshell".to_string(), + server: "https://127.0.0.1:17670".to_string(), + auth: None, + status: "healthy".to_string(), + version: "0.0.74".to_string(), + compute_drivers: Vec::new(), + }; + + let json = gateway_info_to_json(&view); + + assert!( + json["compute_drivers"] + .as_array() + .is_some_and(Vec::is_empty) + ); + } + #[test] fn gateway_auth_label_defaults_https_gateways_to_mtls() { let gateway = GatewayMetadata { diff --git a/crates/openshell-cli/tests/ensure_providers_integration.rs b/crates/openshell-cli/tests/ensure_providers_integration.rs index 7bf8612b4a..8d3ce2ffc0 100644 --- a/crates/openshell-cli/tests/ensure_providers_integration.rs +++ b/crates/openshell-cli/tests/ensure_providers_integration.rs @@ -85,6 +85,13 @@ impl OpenShell for TestOpenShell { })) } + async fn get_gateway_info( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_sandbox( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/mtls_integration.rs b/crates/openshell-cli/tests/mtls_integration.rs index 3f51dd6044..58adf86082 100644 --- a/crates/openshell-cli/tests/mtls_integration.rs +++ b/crates/openshell-cli/tests/mtls_integration.rs @@ -43,6 +43,13 @@ impl OpenShell for TestOpenShell { })) } + async fn get_gateway_info( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_sandbox( &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 5a6e53eb15..e953b0c1f6 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 { })) } + async fn get_gateway_info( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_sandbox( &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 ec8bd53743..0cb6bcc6fb 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -67,6 +67,13 @@ impl OpenShell for TestOpenShell { })) } + async fn get_gateway_info( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_sandbox( &self, request: tonic::Request, diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index 8e799f821e..53aa4ab6a2 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -58,6 +58,13 @@ impl OpenShell for TestOpenShell { })) } + async fn get_gateway_info( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_sandbox( &self, _request: tonic::Request, diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 3a92cd209a..385f2b85b4 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -60,6 +60,16 @@ type SharedComputeDriver = const DELETE_PHASE_CAS_RETRY_LIMIT: usize = 3; +#[derive(Debug, Clone)] +pub struct ComputeDriverInfoSnapshot { + /// Gateway-selected driver name used for routing and `driver_config` keys. + pub name: String, + /// Driver-reported human-readable name from the startup capability snapshot. + pub driver_name: String, + /// Driver-reported implementation version from the startup capability snapshot. + pub driver_version: String, +} + #[tonic::async_trait] trait ShutdownCleanup: Send + Sync { async fn cleanup_on_shutdown(&self) -> Result<(), String>; @@ -259,7 +269,7 @@ impl ComputeDriver for RemoteComputeDriver { #[derive(Clone)] pub struct ComputeRuntime { driver: SharedComputeDriver, - driver_name: String, + driver_info: ComputeDriverInfoSnapshot, shutdown_cleanup: Option>, startup_resume: Option>, _driver_process: Option>, @@ -307,10 +317,15 @@ impl ComputeRuntime { in_tree = driver_kind.is_some(), "Compute driver connected" ); + let driver_info = ComputeDriverInfoSnapshot { + name: driver_name, + driver_name: capabilities.driver_name, + driver_version: capabilities.driver_version, + }; let default_image = capabilities.default_image; Ok(Self { driver, - driver_name, + driver_info, shutdown_cleanup, startup_resume, _driver_process: driver_process, @@ -456,9 +471,14 @@ impl ComputeRuntime { &self.default_image } + #[must_use] + pub fn driver_info_snapshots(&self) -> &[ComputeDriverInfoSnapshot] { + std::slice::from_ref(&self.driver_info) + } + #[must_use] pub fn driver_kind(&self) -> Option { - self.driver_name.parse().ok() + self.driver_info.name.parse().ok() } #[must_use] @@ -467,8 +487,8 @@ impl ComputeRuntime { } pub async fn validate_sandbox_create(&self, sandbox: &Sandbox) -> Result<(), Status> { - let driver_sandbox = - driver_sandbox_from_public(sandbox, &self.driver_name).map_err(|status| *status)?; + let driver_sandbox = driver_sandbox_from_public(sandbox, &self.driver_info.name) + .map_err(|status| *status)?; self.driver .validate_sandbox_create(Request::new(ValidateSandboxCreateRequest { sandbox: Some(driver_sandbox), @@ -483,8 +503,8 @@ impl ComputeRuntime { sandbox_token: Option, ) -> Result { let sandbox_id = sandbox.object_id().to_string(); - let mut driver_sandbox = - driver_sandbox_from_public(&sandbox, &self.driver_name).map_err(|status| *status)?; + let mut driver_sandbox = driver_sandbox_from_public(&sandbox, &self.driver_info.name) + .map_err(|status| *status)?; // Create with MustCreate condition to prevent duplicate creation race self.sandbox_index.update_from_sandbox(&sandbox); @@ -2096,7 +2116,11 @@ impl ComputeDriver for NoopTestDriver { pub async fn new_test_runtime(store: Arc) -> ComputeRuntime { ComputeRuntime { driver: Arc::new(NoopTestDriver), - driver_name: "test".to_string(), + driver_info: ComputeDriverInfoSnapshot { + name: "test".to_string(), + driver_name: "test".to_string(), + driver_version: "test".to_string(), + }, shutdown_cleanup: None, startup_resume: None, _driver_process: None, @@ -2351,7 +2375,11 @@ mod tests { let store = Arc::new(Store::connect("sqlite::memory:").await.unwrap()); ComputeRuntime { driver, - driver_name: "test-driver".to_string(), + driver_info: ComputeDriverInfoSnapshot { + name: "test-driver".to_string(), + driver_name: "test-driver".to_string(), + driver_version: "test".to_string(), + }, shutdown_cleanup: None, startup_resume, _driver_process: None, diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index fe2eb331c9..bf9fd66749 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -13,18 +13,19 @@ mod validation; use openshell_core::proto::{ ApproveAllDraftChunksRequest, ApproveAllDraftChunksResponse, ApproveDraftChunkRequest, ApproveDraftChunkResponse, AttachSandboxProviderRequest, AttachSandboxProviderResponse, - ClearDraftChunksRequest, ClearDraftChunksResponse, ConfigureProviderRefreshRequest, - ConfigureProviderRefreshResponse, CreateProviderRequest, CreateSandboxRequest, - CreateSshSessionRequest, CreateSshSessionResponse, DeleteProviderProfileRequest, - DeleteProviderProfileResponse, DeleteProviderRefreshRequest, DeleteProviderRefreshResponse, - DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - DeleteServiceRequest, DeleteServiceResponse, DetachSandboxProviderRequest, - DetachSandboxProviderResponse, EditDraftChunkRequest, EditDraftChunkResponse, ExecSandboxEvent, - ExecSandboxInput, ExecSandboxRequest, ExposeServiceRequest, GatewayMessage, - GetDraftHistoryRequest, GetDraftHistoryResponse, GetDraftPolicyRequest, GetDraftPolicyResponse, - GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderProfileRequest, - GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse, GetProviderRequest, - GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest, + ClearDraftChunksRequest, ClearDraftChunksResponse, ComputeDriverCapabilities, + ComputeDriverInfo, ConfigureProviderRefreshRequest, ConfigureProviderRefreshResponse, + CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, + DeleteProviderProfileRequest, DeleteProviderProfileResponse, DeleteProviderRefreshRequest, + DeleteProviderRefreshResponse, DeleteProviderRequest, DeleteProviderResponse, + DeleteSandboxRequest, DeleteSandboxResponse, DeleteServiceRequest, DeleteServiceResponse, + DetachSandboxProviderRequest, DetachSandboxProviderResponse, EditDraftChunkRequest, + EditDraftChunkResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, + ExposeServiceRequest, GatewayMessage, GetDraftHistoryRequest, GetDraftHistoryResponse, + GetDraftPolicyRequest, GetDraftPolicyResponse, GetGatewayConfigRequest, + GetGatewayConfigResponse, GetGatewayInfoRequest, GetGatewayInfoResponse, + GetProviderProfileRequest, GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse, + GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxLogsRequest, GetSandboxLogsResponse, GetSandboxPolicyStatusRequest, GetSandboxPolicyStatusResponse, GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, GetSandboxRequest, GetServiceRequest, HealthRequest, HealthResponse, ImportProviderProfilesRequest, @@ -204,6 +205,32 @@ impl OpenShell for OpenShellService { })) } + #[rpc_auth(auth = "bearer", scope = "config:read", role = "admin")] + async fn get_gateway_info( + &self, + _request: Request, + ) -> Result, Status> { + let compute_drivers = self + .state + .compute + .driver_info_snapshots() + .iter() + .map(|driver| ComputeDriverInfo { + name: driver.name.clone(), + capabilities: Some(ComputeDriverCapabilities { + driver_name: driver.driver_name.clone(), + driver_version: driver.driver_version.clone(), + }), + }) + .collect(); + + Ok(Response::new(GetGatewayInfoResponse { + status: ServiceStatus::Healthy.into(), + gateway_version: openshell_core::VERSION.to_string(), + compute_drivers, + })) + } + // --- Sandbox lifecycle --- #[rpc_auth(auth = "bearer", scope = "sandbox:write", role = "user")] diff --git a/crates/openshell-server/tests/common/mod.rs b/crates/openshell-server/tests/common/mod.rs index 3934c8af42..a5bfa1057e 100644 --- a/crates/openshell-server/tests/common/mod.rs +++ b/crates/openshell-server/tests/common/mod.rs @@ -62,6 +62,13 @@ impl OpenShell for TestOpenShell { })) } + async fn get_gateway_info( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_sandbox( &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 bd94d151e5..98c5d99f55 100644 --- a/crates/openshell-server/tests/supervisor_relay_integration.rs +++ b/crates/openshell-server/tests/supervisor_relay_integration.rs @@ -111,6 +111,12 @@ impl OpenShell for RelayGateway { ) -> Result, Status> { Err(Status::unimplemented("unused")) } + async fn get_gateway_info( + &self, + _: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } async fn create_sandbox( &self, _: tonic::Request, diff --git a/docs/sandboxes/manage-gateways.mdx b/docs/sandboxes/manage-gateways.mdx index ce4496e8d2..48b419dada 100644 --- a/docs/sandboxes/manage-gateways.mdx +++ b/docs/sandboxes/manage-gateways.mdx @@ -104,13 +104,28 @@ openshell status -g staging ## Inspect Gateway Status -Use `openshell status` for a quick health check: +Use `openshell status` for a live gateway check. It verifies that the CLI can +reach the running gateway, then reports health and the gateway version: ```shell openshell status ``` -Use `openshell gateway info` when you need the registered endpoint, gateway metadata, or compute driver details: +Use `openshell info` when you need elevated runtime details such as initialized +compute drivers and driver-reported capability versions: + +```shell +openshell info +``` + +Use structured output when scripting against live runtime info: + +```shell +openshell info -o json +``` + +Use `openshell gateway info` when you need local registration details such as +the registered endpoint, gateway metadata source, or authentication mode: ```shell openshell gateway info diff --git a/proto/openshell.proto b/proto/openshell.proto index d2d884f2e8..574f4eb7ac 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -21,6 +21,9 @@ service OpenShell { // Check the health of the service. rpc Health(HealthRequest) returns (HealthResponse); + // Fetch elevated live gateway runtime metadata. + rpc GetGatewayInfo(GetGatewayInfoRequest) returns (GetGatewayInfoResponse); + // Create a new sandbox. rpc CreateSandbox(CreateSandboxRequest) returns (SandboxResponse); @@ -289,6 +292,40 @@ message HealthResponse { string version = 2; } +// Gateway info request. +message GetGatewayInfoRequest {} + +// Gateway info response. +message GetGatewayInfoResponse { + // Service status. + ServiceStatus status = 1; + + // OpenShell gateway binary version. + string gateway_version = 2; + + // Compute driver runtimes initialized by this gateway. Current gateways + // return exactly one entry. + repeated ComputeDriverInfo compute_drivers = 3; +} + +// Info for one initialized compute driver runtime. +message ComputeDriverInfo { + // Gateway-selected driver name used for routing and driver_config keys. + string name = 1; + + // Capabilities reported by the driver during gateway runtime initialization. + ComputeDriverCapabilities capabilities = 2; +} + +// Public compute driver capability snapshot. +message ComputeDriverCapabilities { + // Driver-reported human-readable name from the startup capability snapshot. + string driver_name = 1; + + // Driver-reported implementation version from the startup capability snapshot. + string driver_version = 2; +} + // Public sandbox resource exposed by the OpenShell API. // // This is the canonical gateway-owned view of a sandbox. It merges user intent