Skip to content

Commit f12d10d

Browse files
committed
feat(extensions)!: normalize protocol negotiation
Closes #3057 Introduce a shared extension handshake, enforce protocol and capability compatibility across extension families, and expose immutable negotiated snapshots through gateway info and the Go SDK. Signed-off-by: Seth Jennings <sjenning@redhat.com>
1 parent 39cf482 commit f12d10d

56 files changed

Lines changed: 2980 additions & 1392 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎architecture/compute-runtimes.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,11 @@ The capability RPC reports driver identity, version, and the default sandbox
106106
image used by the gateway. GPU availability stays driver-local and is validated
107107
when a sandbox create request asks for GPU resources.
108108

109-
The gateway records driver identity and version from the startup capability
110-
response. Elevated gateway info reports that initialized driver snapshot instead
111-
of re-querying drivers on each request.
109+
The gateway sends its common extension peer metadata with the startup capability
110+
request and rejects a driver whose protocol major or capability requirements are
111+
incompatible. It records the negotiated protocol, implementation identity and
112+
version, capability sets, and typed resource support once. Elevated gateway info
113+
reports that immutable snapshot instead of re-querying drivers on each request.
112114

113115
## Compiled Driver Selection
114116

‎architecture/gateway.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,14 @@ resolution and again by the sandbox placeholder resolver. This keeps expired
639639
credentials from resolving even when a running sandbox still has retained
640640
placeholder generations from an earlier provider credential snapshot.
641641

642+
All gateway-owned extension registries negotiate the same peer metadata envelope
643+
before accepting work. Compute drivers, credential drivers, gateway interceptors,
644+
and supervisor middleware retain their typed family manifests, while the shared
645+
validator enforces protocol-major compatibility and mutual required-capability
646+
sets. The gateway aggregates immutable, non-secret startup snapshots for the
647+
protected gateway-info API; it does not publish transport, authentication, or
648+
backend configuration.
649+
642650
Static credential delivery is capability-negotiated and endpoint-bound. The
643651
gateway classifies each returned environment entry as either a credential or
644652
non-secret provider configuration and associates every credential key with the

‎crates/openshell-cli/src/commands/gateway.rs‎

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use openshell_bootstrap::{
2020
save_active_gateway, store_gateway_metadata,
2121
};
2222
use openshell_bootstrap::{GatewayMetadataSource, ListedGateway};
23-
use openshell_core::proto::{GetGatewayInfoRequest, HealthRequest, ServiceStatus};
23+
use openshell_core::proto::{ExtensionKind, GetGatewayInfoRequest, HealthRequest, ServiceStatus};
2424
use std::io::IsTerminal;
2525
use std::path::PathBuf;
2626
use tonic::{Code, Status};
@@ -33,6 +33,7 @@ struct GatewayInfoView {
3333
status: String,
3434
version: String,
3535
compute_drivers: Vec<ComputeDriverInfoView>,
36+
extensions: Vec<ExtensionInfoView>,
3637
}
3738

3839
#[derive(Debug, Clone)]
@@ -47,6 +48,17 @@ struct ComputeDriverCapabilitiesView {
4748
driver_version: String,
4849
}
4950

51+
#[derive(Debug, Clone)]
52+
struct ExtensionInfoView {
53+
kind: String,
54+
configured_name: String,
55+
implementation_name: String,
56+
implementation_version: String,
57+
protocol_version: String,
58+
supported_capabilities: Vec<String>,
59+
required_capabilities: Vec<String>,
60+
}
61+
5062
/// Show gateway status.
5163
#[allow(clippy::branches_sharing_code)]
5264
pub async fn gateway_status(
@@ -376,6 +388,19 @@ pub async fn gateway_info(
376388
})?
377389
.into_inner();
378390

391+
let extensions = info
392+
.extensions
393+
.into_iter()
394+
.map(|extension| ExtensionInfoView {
395+
kind: extension_kind_name(extension.kind).to_string(),
396+
configured_name: extension.configured_name,
397+
implementation_name: extension.implementation_name,
398+
implementation_version: extension.implementation_version,
399+
protocol_version: format!("{}.{}", extension.protocol_major, extension.protocol_minor),
400+
supported_capabilities: extension.supported_capabilities,
401+
required_capabilities: extension.required_capabilities,
402+
})
403+
.collect();
379404
let view = GatewayInfoView {
380405
gateway: gateway_name.to_string(),
381406
server: server.to_string(),
@@ -396,6 +421,7 @@ pub async fn gateway_info(
396421
}
397422
})
398423
.collect(),
424+
extensions,
399425
};
400426

401427
print_gateway_info(&view, output)
@@ -422,10 +448,52 @@ fn print_gateway_info(view: &GatewayInfoView, output: &str) -> Result<()> {
422448
println!(" {} {}", "Status:".dimmed(), view.status);
423449
println!(" {} {}", "Version:".dimmed(), view.version);
424450
print_compute_driver_info(&view.compute_drivers);
451+
print_extension_info(&view.extensions);
425452

426453
Ok(())
427454
}
428455

456+
fn extension_kind_name(kind: i32) -> &'static str {
457+
match ExtensionKind::try_from(kind).unwrap_or(ExtensionKind::Unspecified) {
458+
ExtensionKind::ComputeDriver => "compute-driver",
459+
ExtensionKind::CredentialDriver => "credential-driver",
460+
ExtensionKind::GatewayInterceptor => "gateway-interceptor",
461+
ExtensionKind::SupervisorMiddleware => "supervisor-middleware",
462+
ExtensionKind::Unspecified => "unspecified",
463+
}
464+
}
465+
466+
fn print_extension_info(extensions: &[ExtensionInfoView]) {
467+
if extensions.is_empty() {
468+
return;
469+
}
470+
println!(" {}", "Extensions:".dimmed());
471+
for extension in extensions {
472+
println!(" {} ({})", extension.configured_name, extension.kind);
473+
println!(
474+
" {} {} {} (protocol {})",
475+
"Implementation:".dimmed(),
476+
extension.implementation_name,
477+
extension.implementation_version,
478+
extension.protocol_version
479+
);
480+
if !extension.supported_capabilities.is_empty() {
481+
println!(
482+
" {} {}",
483+
"Capabilities:".dimmed(),
484+
extension.supported_capabilities.join(", ")
485+
);
486+
}
487+
if !extension.required_capabilities.is_empty() {
488+
println!(
489+
" {} {}",
490+
"Requires gateway:".dimmed(),
491+
extension.required_capabilities.join(", ")
492+
);
493+
}
494+
}
495+
}
496+
429497
fn print_compute_driver_info(drivers: &[ComputeDriverInfoView]) {
430498
if drivers.is_empty() {
431499
return;
@@ -467,6 +535,15 @@ fn gateway_info_to_json(view: &GatewayInfoView) -> serde_json::Value {
467535
},
468536
}))
469537
.collect::<Vec<_>>(),
538+
"extensions": view.extensions.iter().map(|extension| serde_json::json!({
539+
"kind": &extension.kind,
540+
"configured_name": &extension.configured_name,
541+
"implementation_name": &extension.implementation_name,
542+
"implementation_version": &extension.implementation_version,
543+
"protocol_version": &extension.protocol_version,
544+
"supported_capabilities": &extension.supported_capabilities,
545+
"required_capabilities": &extension.required_capabilities,
546+
})).collect::<Vec<_>>(),
470547
})
471548
}
472549

@@ -1507,9 +1584,9 @@ pub fn gateway_remove(name: &str) -> Result<()> {
15071584
#[cfg(test)]
15081585
mod tests {
15091586
use super::{
1510-
ComputeDriverCapabilitiesView, ComputeDriverInfoView, GatewayAuthenticationState,
1511-
GatewayInfoView, TlsOptions, format_gateway_select_header, format_gateway_select_items,
1512-
gateway_add, gateway_auth_label, gateway_authentication_state,
1587+
ComputeDriverCapabilitiesView, ComputeDriverInfoView, ExtensionInfoView,
1588+
GatewayAuthenticationState, GatewayInfoView, TlsOptions, format_gateway_select_header,
1589+
format_gateway_select_items, gateway_add, gateway_auth_label, gateway_authentication_state,
15131590
gateway_env_override_warning, gateway_info_to_json, gateway_remote_label,
15141591
gateway_select_with, gateway_to_json, gateway_type_label, http_health_check,
15151592
import_local_package_mtls_bundle, mtls_certs_exist_for_gateway, package_managed_tls_dirs,
@@ -1824,6 +1901,15 @@ mod tests {
18241901
driver_version: "0.0.75".to_string(),
18251902
},
18261903
}],
1904+
extensions: vec![ExtensionInfoView {
1905+
kind: "compute-driver".to_string(),
1906+
configured_name: "podman".to_string(),
1907+
implementation_name: "openshell/podman".to_string(),
1908+
implementation_version: "0.0.75".to_string(),
1909+
protocol_version: "1.0".to_string(),
1910+
supported_capabilities: vec!["openshell.compute.contract".to_string()],
1911+
required_capabilities: vec!["openshell.compute.contract".to_string()],
1912+
}],
18271913
};
18281914

18291915
let json = gateway_info_to_json(&view);
@@ -1836,6 +1922,7 @@ mod tests {
18361922
json["compute_drivers"][0]["capabilities"]["driver_name"],
18371923
"podman"
18381924
);
1925+
assert_eq!(json["extensions"][0]["protocol_version"], "1.0");
18391926
assert_eq!(
18401927
json["compute_drivers"][0]["capabilities"]["driver_version"],
18411928
"0.0.75"
@@ -1851,6 +1938,7 @@ mod tests {
18511938
status: "healthy".to_string(),
18521939
version: "0.0.74".to_string(),
18531940
compute_drivers: Vec::new(),
1941+
extensions: Vec::new(),
18541942
};
18551943

18561944
let json = gateway_info_to_json(&view);

0 commit comments

Comments
 (0)