feat(gateway): add elevated gateway info (#2202)

* feat(cli)!: fold gateway metadata into list

BREAKING CHANGE: openshell gateway info no longer shows local gateway registration metadata. Use openshell gateway list or openshell gateway list -o json for local registration details.

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* feat(gateway): add elevated gateway info

Signed-off-by: Evan Lezar <elezar@nvidia.com>

---------

Signed-off-by: Evan Lezar <elezar@nvidia.com>
This commit is contained in:
Evan Lezar
2026-07-15 21:24:43 +00:00
committed by GitHub
parent 134e04de6f
commit 985e457f61
15 changed files with 555 additions and 105 deletions
+4
View File
@@ -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 |
+38 -14
View File
@@ -240,7 +240,7 @@ const HELP_TEMPLATE: &str = "\
provider: Manage provider configuration
\x1b[1mGATEWAY COMMANDS\x1b[0m
gateway: Manage gateway registrations
gateway: Manage gateways
status: Show gateway status and information
inference: Manage inference configuration
doctor: Diagnose gateway issues
@@ -524,7 +524,7 @@ enum Commands {
// ===================================================================
// GATEWAY COMMANDS
// ===================================================================
/// Manage gateway registrations.
/// Manage gateways.
#[command(alias = "gw", after_help = GATEWAY_EXAMPLES, help_template = SUBCOMMAND_HELP_TEMPLATE)]
Gateway {
#[command(subcommand)]
@@ -1105,18 +1105,19 @@ enum GatewayCommands {
name: Option<String>,
},
/// Show gateway registration details.
/// Show elevated live gateway runtime information.
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
Info {
/// Gateway name (defaults to active gateway).
#[arg(long, env = "OPENSHELL_GATEWAY", add = ArgValueCompleter::new(completers::complete_gateway_names))]
name: Option<String>,
/// Output format.
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)]
output: OutputFormat,
},
/// List registered gateways.
///
/// Prints a table of all registered gateways with their endpoint, type,
/// and authentication mode. The active gateway is marked with `*`.
/// Prints all registered gateways with their endpoint, type,
/// authentication mode, source, and remote registration details.
/// The active gateway is marked with `*`.
#[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")]
List {
/// Output format.
@@ -2056,11 +2057,14 @@ async fn main() -> Result<()> {
GatewayCommands::Select { name } => {
run::gateway_select(name.as_deref(), &cli.gateway)?;
}
GatewayCommands::Info { name } => {
let name = name
.or_else(|| resolve_gateway_name(&cli.gateway))
.unwrap_or_else(|| "openshell".to_string());
run::gateway_admin_info(&name)?;
GatewayCommands::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()?;
}
}
GatewayCommands::List { output } => {
run::gateway_list(&cli.gateway, output.as_str())?;
@@ -3445,7 +3449,7 @@ mod tests {
for (raw_args, index) in [
(vec!["openshell", "--gateway", "a"], 2),
(vec!["openshell", "gateway", "select", "a"], 3),
(vec!["openshell", "gateway", "info", "--name", "a"], 4),
(vec!["openshell", "gateway", "remove", "a"], 3),
] {
let mut cmd = Cli::command();
let args: Vec<OsString> = raw_args.iter().copied().map(Into::into).collect();
@@ -3473,6 +3477,26 @@ mod tests {
assert!(matches!(cli.command, Some(Commands::Status)));
}
#[test]
fn gateway_info_accepts_output_json() {
let cli = Cli::try_parse_from(["openshell", "gateway", "info", "-o", "json"])
.expect("gateway info -o json should parse");
assert!(matches!(
cli.command,
Some(Commands::Gateway {
command: Some(GatewayCommands::Info {
output: OutputFormat::Json
})
})
));
}
#[test]
fn top_level_info_is_not_a_command() {
assert!(Cli::try_parse_from(["openshell", "info"]).is_err());
}
#[test]
fn hidden_aliases_still_parse() {
let cli = Cli::try_parse_from(["openshell", "lg", "sandbox-1"])
+320 -65
View File
@@ -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<ComputeDriverInfoView>,
}
#[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 <endpoint>"
))
}
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::<Vec<_>>(),
})
}
/// Set the active gateway.
pub fn gateway_use(name: &str) -> Result<()> {
// Verify the gateway exists
@@ -1356,31 +1502,73 @@ pub fn gateway_list(gateway_flag: &Option<String>, output: &str) -> Result<()> {
.max()
.unwrap_or(6)
.max(6);
let auth_width = gateways
.iter()
.map(|g| gateway_auth_label(&g.metadata).len())
.max()
.unwrap_or(4)
.max(4);
let remote_labels: Vec<Option<String>> = gateways
.iter()
.map(|g| gateway_remote_label(&g.metadata))
.collect();
let show_remote = remote_labels.iter().any(Option::is_some);
let remote_width = remote_labels
.iter()
.filter_map(|label| label.as_ref().map(String::len))
.max()
.unwrap_or(6)
.max(6);
// Print header
println!(
" {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {}",
"NAME".bold(),
"ENDPOINT".bold(),
"TYPE".bold(),
"SOURCE".bold(),
"AUTH".bold(),
);
if show_remote {
println!(
" {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {:<auth_width$} {:<remote_width$}",
"NAME".bold(),
"ENDPOINT".bold(),
"TYPE".bold(),
"SOURCE".bold(),
"AUTH".bold(),
"REMOTE".bold(),
);
} else {
println!(
" {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {}",
"NAME".bold(),
"ENDPOINT".bold(),
"TYPE".bold(),
"SOURCE".bold(),
"AUTH".bold(),
);
}
// Print rows
for gateway in gateways {
for (gateway, remote_label) in gateways.iter().zip(remote_labels.iter()) {
let metadata = &gateway.metadata;
let is_active = active.as_deref() == Some(&metadata.name);
let marker = if is_active { "*" } else { " " };
let gw_type = gateway_type_label(metadata);
let gw_auth = gateway_auth_label(metadata);
let line = format!(
"{marker} {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {gw_auth}",
metadata.name,
metadata.gateway_endpoint,
gw_type,
gateway.source.label(),
);
let line = if show_remote {
let remote_label = remote_label.as_deref().unwrap_or("-");
format!(
"{marker} {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {:<auth_width$} {:<remote_width$}",
metadata.name,
metadata.gateway_endpoint,
gw_type,
gateway.source.label(),
gw_auth,
remote_label,
)
} else {
format!(
"{marker} {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {gw_auth}",
metadata.name,
metadata.gateway_endpoint,
gw_type,
gateway.source.label(),
)
};
if is_active {
println!("{}", line.green());
} else {
@@ -1400,9 +1588,23 @@ fn gateway_to_json(gateway: &ListedGateway, active: &Option<String>) -> serde_js
"source": gateway.source.label(),
"auth": gateway_auth_label(metadata),
"active": active.as_deref() == Some(&metadata.name),
"is_remote": metadata.is_remote,
"remote_host": &metadata.remote_host,
"resolved_host": &metadata.resolved_host,
})
}
fn gateway_remote_label(gateway: &GatewayMetadata) -> Option<String> {
match (&gateway.remote_host, &gateway.resolved_host) {
(Some(remote), Some(resolved)) if remote != resolved => {
Some(format!("{remote} -> {resolved}"))
}
(Some(remote), _) => Some(remote.clone()),
(None, Some(resolved)) => Some(resolved.clone()),
(None, None) => None,
}
}
async fn http_health_check(server: &str, tls: &TlsOptions) -> Result<Option<StatusCode>> {
let base = server.trim_end_matches('/');
let uri: hyper::Uri = format!("{base}/healthz").parse().into_diagnostic()?;
@@ -1522,38 +1724,6 @@ pub fn gateway_remove(name: &str) -> Result<()> {
Ok(())
}
/// Show gateway registration details.
pub fn gateway_admin_info(name: &str) -> Result<()> {
let metadata = get_gateway_metadata(name).ok_or_else(|| {
miette::miette!(
"No gateway metadata found for '{name}'.\n\
Register it first: openshell gateway add <endpoint> --name {name}"
)
})?;
println!("{}", "Gateway Info".cyan().bold());
println!();
println!(" {} {}", "Gateway:".dimmed(), metadata.name);
println!(
" {} {}",
"Gateway endpoint:".dimmed(),
metadata.gateway_endpoint
);
if metadata.is_remote {
if let Some(ref host) = metadata.remote_host {
println!(" {} {host}", "Remote host:".dimmed());
} else {
println!(" {} External registration", "Type:".dimmed());
}
if let Some(ref resolved) = metadata.resolved_host {
println!(" {} {resolved}", "Resolved host:".dimmed());
}
}
Ok(())
}
/// Validate system prerequisites for running a gateway.
///
/// Checks Docker connectivity and reports the result. Returns exit code 0
@@ -7887,11 +8057,13 @@ fn format_timestamp_ms(ms: i64) -> String {
#[cfg(test)]
mod tests {
use super::{
PolicyGetView, ProvisioningStep, TlsOptions, build_sandbox_resource_limits,
ComputeDriverCapabilitiesView, ComputeDriverInfoView, GatewayInfoView, PolicyGetView,
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,
gateway_type_label, git_sync_files, http_health_check, import_local_package_mtls_bundle,
gateway_auth_label, gateway_env_override_warning, gateway_info_to_json,
gateway_remote_label, 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,
parse_credential_pairs, parse_driver_config_json, parse_secret_material_env_pairs,
@@ -9035,6 +9207,89 @@ mod tests {
assert_eq!(json["active"], true);
}
#[test]
fn gateway_to_json_includes_remote_registration_details() {
let gateway = ListedGateway {
metadata: GatewayMetadata {
name: "remote-vm".to_string(),
gateway_endpoint: "https://127.0.0.1:17670".to_string(),
is_remote: true,
remote_host: Some("user@gateway-alias".to_string()),
resolved_host: Some("10.0.0.5".to_string()),
auth_mode: Some("mtls".to_string()),
..Default::default()
},
source: GatewayMetadataSource::User,
};
let json = gateway_to_json(&gateway, &Some("local-vm".to_string()));
assert_eq!(json["source"], "user");
assert_eq!(json["type"], "remote");
assert_eq!(json["auth"], "mtls");
assert_eq!(json["active"], false);
assert_eq!(json["is_remote"], true);
assert_eq!(json["remote_host"], "user@gateway-alias");
assert_eq!(json["resolved_host"], "10.0.0.5");
assert_eq!(
gateway_remote_label(&gateway.metadata).as_deref(),
Some("user@gateway-alias -> 10.0.0.5")
);
}
#[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 {
@@ -86,6 +86,13 @@ impl OpenShell for TestOpenShell {
}))
}
async fn get_gateway_info(
&self,
_request: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
Err(Status::unimplemented("unused"))
}
async fn create_sandbox(
&self,
_request: tonic::Request<CreateSandboxRequest>,
@@ -43,6 +43,13 @@ impl OpenShell for TestOpenShell {
}))
}
async fn get_gateway_info(
&self,
_request: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
Err(Status::unimplemented("unused"))
}
async fn create_sandbox(
&self,
_request: tonic::Request<openshell_core::proto::CreateSandboxRequest>,
@@ -108,6 +108,13 @@ impl OpenShell for TestOpenShell {
}))
}
async fn get_gateway_info(
&self,
_request: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
Err(Status::unimplemented("unused"))
}
async fn create_sandbox(
&self,
_request: tonic::Request<CreateSandboxRequest>,
@@ -67,6 +67,13 @@ impl OpenShell for TestOpenShell {
}))
}
async fn get_gateway_info(
&self,
_request: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
Err(Status::unimplemented("unused"))
}
async fn create_sandbox(
&self,
request: tonic::Request<CreateSandboxRequest>,
@@ -58,6 +58,13 @@ impl OpenShell for TestOpenShell {
}))
}
async fn get_gateway_info(
&self,
_request: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
Err(Status::unimplemented("unused"))
}
async fn create_sandbox(
&self,
_request: tonic::Request<CreateSandboxRequest>,
+11
View File
@@ -87,6 +87,17 @@ impl OpenShell for TestOpenShell {
}))
}
async fn get_gateway_info(
&self,
_: tonic::Request<proto::GetGatewayInfoRequest>,
) -> Result<Response<proto::GetGatewayInfoResponse>, Status> {
Ok(Response::new(proto::GetGatewayInfoResponse {
status: proto::ServiceStatus::Healthy.into(),
gateway_version: "test-1.2.3".to_string(),
compute_drivers: Vec::new(),
}))
}
async fn update_provider_profiles(
&self,
_: tonic::Request<proto::UpdateProviderProfilesRequest>,
+37 -9
View File
@@ -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<Arc<dyn ShutdownCleanup>>,
startup_resume: Option<Arc<dyn StartupResume>>,
_driver_process: Option<Arc<ManagedDriverProcess>>,
@@ -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<ComputeDriverKind> {
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<String>,
) -> Result<Sandbox, Status> {
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);
@@ -2097,7 +2117,11 @@ impl ComputeDriver for NoopTestDriver {
pub async fn new_test_runtime(store: Arc<Store>) -> 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,
@@ -2352,7 +2376,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,
+39 -12
View File
@@ -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,
@@ -206,6 +207,32 @@ impl OpenShell for OpenShellService {
}))
}
#[rpc_auth(auth = "bearer", scope = "config:read", role = "admin")]
async fn get_gateway_info(
&self,
_request: Request<GetGatewayInfoRequest>,
) -> Result<Response<GetGatewayInfoResponse>, 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")]
@@ -62,6 +62,13 @@ impl OpenShell for TestOpenShell {
}))
}
async fn get_gateway_info(
&self,
_request: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
Err(Status::unimplemented("unused"))
}
async fn create_sandbox(
&self,
_request: tonic::Request<CreateSandboxRequest>,
@@ -111,6 +111,12 @@ impl OpenShell for RelayGateway {
) -> Result<Response<openshell_core::proto::HealthResponse>, Status> {
Err(Status::unimplemented("unused"))
}
async fn get_gateway_info(
&self,
_: tonic::Request<openshell_core::proto::GetGatewayInfoRequest>,
) -> Result<Response<openshell_core::proto::GetGatewayInfoResponse>, Status> {
Err(Status::unimplemented("unused"))
}
async fn create_sandbox(
&self,
_: tonic::Request<openshell_core::proto::CreateSandboxRequest>,
+21 -5
View File
@@ -84,12 +84,21 @@ The active gateway is the persisted default. The `-g` flag and the `OPENSHELL_GA
Installers can seed read-only gateway entries for package-managed local services. By default the CLI reads these from `/etc/openshell`, using the same `active_gateway` plus `gateways/<name>/metadata.json` layout as per-user config. Packages can override that system config root with a non-empty absolute `OPENSHELL_SYSTEM_GATEWAY_DIR` when needed; empty or relative values fall back to `/etc/openshell` and log a warning. These entries appear in `openshell gateway list` and can be selected like user registrations. `openshell gateway list` and `openshell term` label each gateway as `user` or `system` so you can see which config layer owns it. `openshell gateway remove` removes only per-user registrations. Register a per-user gateway with the same name when you need to shadow an installer-provided default.
List all registered gateways:
List all registered gateways. The table shows each gateway's endpoint, type,
config source, authentication mode, and remote target when a remote registration
has one:
```shell
openshell gateway list
```
Use structured output when you need the complete local registration metadata,
including `remote_host` and `resolved_host` for remote registrations:
```shell
openshell gateway list -o json
```
Switch the active gateway:
```shell
@@ -104,17 +113,24 @@ 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 gateway info` when you need elevated runtime details such as
initialized compute drivers and driver-reported capability versions:
```shell
openshell gateway info
openshell gateway info --name production
```
Use structured output when scripting against live runtime info:
```shell
openshell gateway info -o json
```
Remove a local CLI registration without stopping the gateway service:
@@ -136,7 +152,7 @@ For Docker-backed local gateways, inspect Docker and the gateway process or cont
```shell
openshell doctor check
openshell gateway info
openshell gateway list
```
For Kubernetes gateways, inspect the gateway workload and cluster events:
+37
View File
@@ -22,6 +22,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);
@@ -290,6 +293,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