From 0bf47e0f89e757c31837d4a1ced86fb4c10f90eb Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Sun, 11 Oct 2026 09:09:36 +0100 Subject: [PATCH] docs(extensibility): name extension base capabilities Signed-off-by: Eric Curtin --- .../openshell-core/src/extension_protocol.rs | 61 ++++++++++++++++--- docs/extensibility/overview.mdx | 23 +++++++ proto/compute_driver.proto | 2 + proto/credential_driver.proto | 2 + proto/gateway_interceptor.proto | 2 + proto/supervisor_middleware.proto | 2 + 6 files changed, 84 insertions(+), 8 deletions(-) diff --git a/crates/openshell-core/src/extension_protocol.rs b/crates/openshell-core/src/extension_protocol.rs index 9c25c6cb87..aef96b46d2 100644 --- a/crates/openshell-core/src/extension_protocol.rs +++ b/crates/openshell-core/src/extension_protocol.rs @@ -64,15 +64,23 @@ pub struct NegotiatedExtension { #[derive(Debug, Error, PartialEq, Eq)] pub enum NegotiationError { #[error( - "{family} extension '{name}' did not provide protocol metadata; upgrade the extension to a version that supports OpenShell extension negotiation" + "{family} extension '{name}' did not provide protocol metadata; upgrade the extension to a version that supports OpenShell extension negotiation (expected {expected})" )] - MissingMetadata { family: &'static str, name: String }, + MissingMetadata { + family: &'static str, + name: String, + expected: String, + }, #[error( "gateway did not provide protocol metadata to {family} extension '{name}'; upgrade the gateway and extension together" )] MissingGatewayMetadata { family: &'static str, name: String }, - #[error("{family} extension '{name}' did not provide a protocol version")] - MissingProtocolVersion { family: &'static str, name: String }, + #[error("{family} extension '{name}' did not provide a protocol version (expected {expected})")] + MissingProtocolVersion { + family: &'static str, + name: String, + expected: String, + }, #[error( "{family} extension '{name}' uses unsupported protocol {remote_major}.{remote_minor}; gateway supports {local_major}.{local_minor}" )] @@ -148,6 +156,14 @@ pub fn extension_metadata( } } +/// Protocol version and base capability an extension of `family` must declare. +fn expected_contract(family: ExtensionFamily) -> String { + format!( + "protocol {PROTOCOL_MAJOR}.{PROTOCOL_MINOR} and capability {}", + family.contract_capability() + ) +} + pub fn negotiate( family: ExtensionFamily, configured_name: impl Into, @@ -156,20 +172,24 @@ pub fn negotiate( ) -> Result { let configured_name = configured_name.into(); let family_name = family.as_str(); + let expected = expected_contract(family); let extension = extension.ok_or_else(|| NegotiationError::MissingMetadata { family: family_name, name: configured_name.clone(), + expected: expected.clone(), })?; let version = extension.protocol_version.as_ref().ok_or_else(|| { NegotiationError::MissingProtocolVersion { family: family_name, name: configured_name.clone(), + expected: expected.clone(), } })?; let gateway_version = gateway.protocol_version.as_ref().ok_or_else(|| { NegotiationError::MissingProtocolVersion { family: family_name, name: "gateway".to_string(), + expected, } })?; if version.major != gateway_version.major { @@ -401,10 +421,13 @@ mod tests { #[test] fn missing_metadata_and_major_skew_are_actionable() { let (gateway, mut extension) = compatible(); - assert!(matches!( - negotiate(ExtensionFamily::Compute, "example", &gateway, None), - Err(NegotiationError::MissingMetadata { .. }) - )); + let missing = negotiate(ExtensionFamily::Compute, "example", &gateway, None).unwrap_err(); + assert!(matches!(missing, NegotiationError::MissingMetadata { .. })); + assert!( + missing + .to_string() + .contains("protocol 1.0 and capability openshell.compute.contract") + ); extension.protocol_version.as_mut().unwrap().major = 2; assert!(matches!( negotiate( @@ -417,6 +440,28 @@ mod tests { )); } + #[test] + fn missing_protocol_version_names_the_expected_contract() { + let (gateway, mut extension) = compatible(); + extension.protocol_version = None; + let error = negotiate( + ExtensionFamily::Compute, + "example", + &gateway, + Some(extension), + ) + .unwrap_err(); + assert!(matches!( + error, + NegotiationError::MissingProtocolVersion { .. } + )); + assert!( + error + .to_string() + .contains("protocol 1.0 and capability openshell.compute.contract") + ); + } + #[test] fn both_peers_require_capabilities_from_the_other() { let (mut gateway, mut extension) = compatible(); diff --git a/docs/extensibility/overview.mdx b/docs/extensibility/overview.mdx index 149df13d96..8e551aed0e 100644 --- a/docs/extensibility/overview.mdx +++ b/docs/extensibility/overview.mdx @@ -102,6 +102,29 @@ are satisfied. It rejects different major versions, missing metadata, or an unmet required capability during startup. Built-in and external service extensions follow the same compatibility checks. +Every extension returns protocol `1.0` and lists its family's base capability in +`supported_capabilities`: + +| Family | Base capability | Returned in | +|---|---|---| +| Compute driver | `openshell.compute.contract` | `GetCapabilitiesResponse.extension` | +| Credential driver | `openshell.credentials.contract` | `GetCredentialDriverCapabilitiesResponse.extension` | +| Gateway interceptor | `openshell.gateway-interceptor.contract` | `InterceptorManifest.extension` | +| Supervisor middleware | `openshell.supervisor-middleware.contract` | `MiddlewareManifest.extension` | + +For example, a supervisor middleware `Describe` response includes: + +```json +{ + "extension": { + "protocol_version": { "major": 1, "minor": 0 }, + "implementation_name": "example/content-guard", + "implementation_version": "0.1.0", + "supported_capabilities": ["openshell.supervisor-middleware.contract"] + } +} +``` + Use `openshell gateway info` to inspect the negotiated extension families, implementation versions, protocol versions, and capabilities active on a gateway. diff --git a/proto/compute_driver.proto b/proto/compute_driver.proto index f278174b5c..60bda56914 100644 --- a/proto/compute_driver.proto +++ b/proto/compute_driver.proto @@ -101,6 +101,8 @@ message GetCapabilitiesResponse { // the driver does not support rootfs tar sources. uint64 rootfs_tar_max_bytes = 11; // Compute extension protocol metadata. Required for protocol negotiation. + // Declare protocol 1.0 and list "openshell.compute.contract" in + // supported_capabilities. openshell.extension.v1.PeerMetadata extension = 12; // Versioned effective operator admission policy (v1: followed by JSON). // Gateways require an exact policy match before activating the driver. diff --git a/proto/credential_driver.proto b/proto/credential_driver.proto index 765099d71b..da0743a1f5 100644 --- a/proto/credential_driver.proto +++ b/proto/credential_driver.proto @@ -51,6 +51,8 @@ message GetCredentialDriverCapabilitiesResponse { // True when ResolveCredentials may return expiration_time values. bool supports_expires_at = 5; // Credential-driver protocol metadata. Required for protocol negotiation. + // Declare protocol 1.0 and list "openshell.credentials.contract" in + // supported_capabilities. openshell.extension.v1.PeerMetadata extension = 6; } diff --git a/proto/gateway_interceptor.proto b/proto/gateway_interceptor.proto index 1bd12d9d64..cf556198c5 100644 --- a/proto/gateway_interceptor.proto +++ b/proto/gateway_interceptor.proto @@ -111,6 +111,8 @@ message InterceptorManifest { // post-authentication consistency check. string expected_audience = 5; // Gateway-interceptor protocol metadata. Required for negotiation. + // Declare protocol 1.0 and list "openshell.gateway-interceptor.contract" in + // supported_capabilities. openshell.extension.v1.PeerMetadata extension = 6; } diff --git a/proto/supervisor_middleware.proto b/proto/supervisor_middleware.proto index 52793d905f..6c0cc947da 100644 --- a/proto/supervisor_middleware.proto +++ b/proto/supervisor_middleware.proto @@ -67,6 +67,8 @@ message MiddlewareManifest { // this post-authentication consistency check. string expected_audience = 4; // Supervisor-middleware protocol metadata. Required for negotiation. + // Declare protocol 1.0 and list "openshell.supervisor-middleware.contract" + // in supported_capabilities. openshell.extension.v1.PeerMetadata extension = 5; }