From e3a0fb87e90bd9db56475084af7f033ee3ea68dc Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Tue, 6 Oct 2026 10:13:17 -0700 Subject: [PATCH] refactor(proto): negotiate supervisor config delivery with feature flags Replace the exact-match protocol_revision handshake with a SupervisorHello.supports_config_snapshots capability, matching the existing supports_provider_readiness pattern. Peers no longer reject unknown revisions, so gateway rollback and mixed-version HA replicas keep supervisor sessions alive. SessionAccepted no longer carries a revision. Document SandboxConfigSnapshot and ProviderEnvironmentSnapshot as the canonical configuration shapes, with the GetSandboxConfig and GetSandboxProviderEnvironment responses as polling projections. State ProviderEnvironmentValue invariants and number its expiration_time field 3 instead of reserving a field that never shipped. Signed-off-by: Piotr Mlocek --- crates/openshell-core/src/proto/mod.rs | 15 ---- .../src/config_delivery/mod.rs | 26 ++----- crates/openshell-server/src/grpc/mod.rs | 8 +-- .../src/grpc/provider_readiness_tests.rs | 3 +- crates/openshell-server/src/storage_proto.rs | 2 +- .../src/supervisor_session.rs | 70 ++----------------- .../src/supervisor_session.rs | 35 +--------- proto/openshell.proto | 43 +++++++----- proto/sandbox.proto | 9 ++- sdk/go/proto/openshellv1/openshell.pb.go | 60 ++++++++-------- sdk/go/proto/openshellv1/openshell_grpc.pb.go | 22 ++++-- sdk/go/proto/sandboxv1/sandbox.pb.go | 9 ++- 12 files changed, 108 insertions(+), 194 deletions(-) diff --git a/crates/openshell-core/src/proto/mod.rs b/crates/openshell-core/src/proto/mod.rs index 3fcfc18d8..c6076620a 100644 --- a/crates/openshell-core/src/proto/mod.rs +++ b/crates/openshell-core/src/proto/mod.rs @@ -99,21 +99,6 @@ pub fn all_workspaces_selector() -> WorkspaceSelector { } } -/// Exact protocol revision required between a gateway and its supervisor. -/// -/// The supervisor stream is an internal, version-locked deployment contract. -/// Bump this when either peer can no longer honor the previous stream -/// semantics. -pub const SUPERVISOR_PROTOCOL_REVISION: u32 = 1; - -/// Revision implied by peers built before the handshake existed. Proto3 leaves -/// the field unset, so such peers report zero. -/// -/// Sandboxes keep their supervisor binary until they are recreated, so a -/// gateway upgrade must keep serving them for one release. Remove this -/// allowance once every supported release sends an explicit revision. -pub const LEGACY_SUPERVISOR_PROTOCOL_REVISION: u32 = 0; - #[cfg(test)] mod tests { use std::collections::HashMap; diff --git a/crates/openshell-server/src/config_delivery/mod.rs b/crates/openshell-server/src/config_delivery/mod.rs index b147926c0..33e7d2cf6 100644 --- a/crates/openshell-server/src/config_delivery/mod.rs +++ b/crates/openshell-server/src/config_delivery/mod.rs @@ -1055,13 +1055,9 @@ mod tests { state: &Arc, sandbox_id: &str, ) -> (SupervisorStreamHarness, String) { - let mut harness = connect_supervisor_stream( - state, - sandbox_id, - openshell_core::proto::SUPERVISOR_PROTOCOL_REVISION, - ) - .await - .unwrap(); + let mut harness = connect_supervisor_stream(state, sandbox_id, true) + .await + .unwrap(); let first = tokio::time::timeout(Duration::from_secs(5), harness.inbound.message()) .await .unwrap() @@ -1322,13 +1318,9 @@ mod tests { async fn legacy_supervisor_keeps_polling_when_gateway_shadow_push_is_enabled() { let state = push_state().await; put_sandbox(&state, "legacy-sandbox", &[]).await; - let mut harness = connect_supervisor_stream( - &state, - "legacy-sandbox", - openshell_core::proto::LEGACY_SUPERVISOR_PROTOCOL_REVISION, - ) - .await - .unwrap(); + let mut harness = connect_supervisor_stream(&state, "legacy-sandbox", false) + .await + .unwrap(); let first = harness.inbound.message().await.unwrap().unwrap(); let Some(gateway_message::Payload::SessionAccepted(accepted)) = first.payload else { panic!("expected SessionAccepted"); @@ -1394,11 +1386,7 @@ mod tests { put_sandbox(&state, "sandbox", &["provider"]).await; let (resolve_hit, _release_resolve) = state.credentials.gate_next_resolve(); tokio::time::timeout(Duration::from_secs(10), async { - let connect = connect_supervisor_stream( - &state, - "sandbox", - openshell_core::proto::SUPERVISOR_PROTOCOL_REVISION, - ); + let connect = connect_supervisor_stream(&state, "sandbox", true); let (response, hit) = tokio::join!(connect, resolve_hit); hit.expect("bootstrap must reach the stalled credential driver"); let mut harness = response.unwrap(); diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index a9c7bd860..109078e71 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -983,12 +983,12 @@ pub mod test_support { } /// Serve `state` on loopback and open a supervisor stream whose hello - /// carries the given protocol revision. Returns the gRPC status when the - /// gateway rejects the stream before accepting it. + /// advertises the given configuration snapshot support. Returns the gRPC + /// status when the gateway rejects the stream before accepting it. pub async fn connect_supervisor_stream( state: &Arc, sandbox_id: &str, - protocol_revision: u32, + supports_config_snapshots: bool, ) -> Result { let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = listener.local_addr().unwrap(); @@ -1008,7 +1008,7 @@ pub mod test_support { payload: Some(supervisor_message::Payload::Hello(SupervisorHello { sandbox_id: sandbox_id.into(), instance_id: "instance".into(), - protocol_revision, + supports_config_snapshots, connection_epoch: 0, supports_provider_readiness: false, })), diff --git a/crates/openshell-server/src/grpc/provider_readiness_tests.rs b/crates/openshell-server/src/grpc/provider_readiness_tests.rs index 44fcb6319..45e7fe0b0 100644 --- a/crates/openshell-server/src/grpc/provider_readiness_tests.rs +++ b/crates/openshell-server/src/grpc/provider_readiness_tests.rs @@ -7,7 +7,6 @@ use crate::config_update_operation::CONFIG_UPDATE_OPERATION_OBJECT_TYPE; use crate::grpc::test_support::{authed_request, test_server_state}; use crate::persistence::WriteCondition; use crate::storage_proto::StoredConfigUpdateOperation; -use openshell_core::proto::SUPERVISOR_PROTOCOL_REVISION; use openshell_core::proto::SandboxSpec; use openshell_core::proto::datamodel::v1::ObjectMeta; use prost::Message; @@ -71,7 +70,7 @@ fn hello() -> SupervisorHello { SupervisorHello { sandbox_id: Uuid::new_v4().to_string(), instance_id: Uuid::new_v4().to_string(), - protocol_revision: SUPERVISOR_PROTOCOL_REVISION, + supports_config_snapshots: true, connection_epoch: 0, supports_provider_readiness: true, } diff --git a/crates/openshell-server/src/storage_proto.rs b/crates/openshell-server/src/storage_proto.rs index 3a683287e..8bd8a5d94 100644 --- a/crates/openshell-server/src/storage_proto.rs +++ b/crates/openshell-server/src/storage_proto.rs @@ -136,7 +136,7 @@ mod tests { // SandboxProvisioning in both closures. Old rows decode false and empty; // decoding or deadline updates cannot claim an existing attempt. const PUBLIC_RPC_SCHEMA_SHA256: &str = - "fe0408efcdc1d6e485586223fe8f14ce6a5747959aa2d646c94be57e1fc8c665"; + "26279ae924037ff79c531625b3051ed750eb502d4b9c63e0653f6f0deafc35a1"; const DURABLE_SCHEMA_SHA256: &str = "76487ab369fc3a4b03a179bb5e7ea6be8d20e380ad5563075dff8ee50e539406"; const PUBLIC_DURABLE_OVERLAP_SHA256: &str = diff --git a/crates/openshell-server/src/supervisor_session.rs b/crates/openshell-server/src/supervisor_session.rs index c282fda0a..4b760d7c4 100644 --- a/crates/openshell-server/src/supervisor_session.rs +++ b/crates/openshell-server/src/supervisor_session.rs @@ -27,7 +27,6 @@ use openshell_core::proto::{ config_update, gateway_message, open_shell_client, peer_relay_frame, relay_open, supervisor_message, }; -use openshell_core::proto::{LEGACY_SUPERVISOR_PROTOCOL_REVISION, SUPERVISOR_PROTOCOL_REVISION}; use openshell_core::transport_errors::is_expected_transport_close_status; use crate::ServerState; @@ -2094,7 +2093,6 @@ pub async fn handle_connect_supervisor( if sandbox_id.is_empty() { return Err(Status::invalid_argument("sandbox_id is required")); } - validate_protocol_revision(&sandbox_id, hello.protocol_revision)?; if let Some(principal) = principal.as_ref() { crate::auth::guard::ensure_sandbox_principal_scope(principal, &sandbox_id)?; } @@ -2102,7 +2100,7 @@ pub async fn handle_connect_supervisor( // record, so registration detects publications the bootstrap may miss. let captured_seq = (state.config.config_delivery_mode == openshell_core::config::ConfigDeliveryMode::Push - && hello.protocol_revision == SUPERVISOR_PROTOCOL_REVISION) + && hello.supports_config_snapshots) .then(|| state.config_delivery.current_seq()); let sandbox = require_persisted_sandbox(&state.store, &sandbox_id).await?; @@ -2218,7 +2216,6 @@ async fn establish_supervisor_session( payload: Some(gateway_message::Payload::SessionAccepted(SessionAccepted { session_id: session_id.clone(), bootstrap, - protocol_revision: SUPERVISOR_PROTOCOL_REVISION, heartbeat_interval: openshell_core::time::duration_from_std(Duration::from_secs( u64::from(HEARTBEAT_INTERVAL_SECS), )) @@ -2429,23 +2426,6 @@ async fn establish_supervisor_session( Ok(Response::new(stream)) } -fn validate_protocol_revision(sandbox_id: &str, supervisor_revision: u32) -> Result<(), Status> { - match supervisor_revision { - SUPERVISOR_PROTOCOL_REVISION => Ok(()), - LEGACY_SUPERVISOR_PROTOCOL_REVISION => { - counter!("openshell_supervisor_protocol_legacy_sessions_total").increment(1); - warn!( - sandbox_id = %sandbox_id, - "supervisor session: supervisor predates the protocol handshake; recreate the sandbox before the next gateway upgrade" - ); - Ok(()) - } - other => Err(Status::failed_precondition(format!( - "supervisor protocol revision mismatch: gateway requires {SUPERVISOR_PROTOCOL_REVISION}, supervisor offered {other}" - ))), - } -} - pub async fn handle_report_main_process_exit( state: &Arc, request: Request, @@ -2735,7 +2715,6 @@ mod tests { sandbox_config: Some(SandboxConfigSnapshot::default()), provider_environment: Some(ProviderEnvironmentSnapshot::default()), }), - protocol_revision: SUPERVISOR_PROTOCOL_REVISION, })), }; let updates = [ @@ -2796,12 +2775,6 @@ mod tests { assert_eq!(decoded, result); } - #[test] - fn supervisor_protocol_revision_accepts_current_and_legacy_peers() { - assert!(validate_protocol_revision("sb-1", SUPERVISOR_PROTOCOL_REVISION).is_ok()); - assert!(validate_protocol_revision("sb-1", LEGACY_SUPERVISOR_PROTOCOL_REVISION).is_ok()); - } - async fn state_with_sandbox(sandbox_id: &str) -> Arc { let state = crate::grpc::test_support::test_server_state().await; state @@ -2823,22 +2796,19 @@ mod tests { } #[tokio::test] - async fn legacy_supervisor_without_protocol_revision_is_accepted() { + async fn supervisor_without_config_snapshots_is_accepted() { let state = state_with_sandbox("sb-legacy").await; - let mut harness = crate::grpc::test_support::connect_supervisor_stream( - &state, - "sb-legacy", - LEGACY_SUPERVISOR_PROTOCOL_REVISION, - ) - .await - .expect("legacy supervisor must connect"); + let mut harness = + crate::grpc::test_support::connect_supervisor_stream(&state, "sb-legacy", false) + .await + .expect("supervisor without configuration snapshots must connect"); let Some(gateway_message::Payload::SessionAccepted(accepted)) = first_gateway_message(&mut harness).await.payload else { panic!("expected SessionAccepted"); }; - assert_eq!(accepted.protocol_revision, SUPERVISOR_PROTOCOL_REVISION); + assert!(accepted.bootstrap.is_none()); assert!( state .supervisor_sessions @@ -2846,32 +2816,6 @@ mod tests { ); } - #[tokio::test] - async fn unknown_supervisor_protocol_revision_is_rejected() { - let state = state_with_sandbox("sb-future").await; - let Err(status) = crate::grpc::test_support::connect_supervisor_stream( - &state, - "sb-future", - SUPERVISOR_PROTOCOL_REVISION + 1, - ) - .await - else { - panic!("mismatched revision must be rejected"); - }; - - assert_eq!(status.code(), tonic::Code::FailedPrecondition); - assert!(status.message().contains("revision mismatch")); - assert!(!state.supervisor_sessions.has_session("sb-future")); - } - - #[test] - fn supervisor_protocol_revision_rejects_unknown_peers() { - let error = - validate_protocol_revision("sb-1", SUPERVISOR_PROTOCOL_REVISION + 1).unwrap_err(); - assert_eq!(error.code(), tonic::Code::FailedPrecondition); - assert!(error.message().contains("revision mismatch")); - } - fn register_push_session( registry: &SupervisorSessionRegistry, sandbox_id: &str, diff --git a/crates/openshell-supervisor-process/src/supervisor_session.rs b/crates/openshell-supervisor-process/src/supervisor_session.rs index 3c54477a4..94309a83f 100644 --- a/crates/openshell-supervisor-process/src/supervisor_session.rs +++ b/crates/openshell-supervisor-process/src/supervisor_session.rs @@ -21,7 +21,6 @@ use openshell_core::proto::{ RelayOpenResult, ReportMainProcessExitRequest, SupervisorHeartbeat, SupervisorHello, SupervisorMessage, TcpRelayTarget, gateway_message, relay_open, supervisor_message, }; -use openshell_core::proto::{LEGACY_SUPERVISOR_PROTOCOL_REVISION, SUPERVISOR_PROTOCOL_REVISION}; use openshell_isolation_interface::contract::{BoundaryLoopbackConnector, LoopbackTarget}; use openshell_ocsf::{ ActivityId, BaseEventBuilder, ConnectionInfo, Endpoint, EventContext, NetworkActivityBuilder, @@ -414,7 +413,7 @@ async fn run_single_session( payload: Some(supervisor_message::Payload::Hello(SupervisorHello { sandbox_id: config.sandbox_id.clone(), instance_id: config.instance_id.clone(), - protocol_revision: SUPERVISOR_PROTOCOL_REVISION, + supports_config_snapshots: true, connection_epoch, supports_provider_readiness: true, })), @@ -448,7 +447,6 @@ async fn run_single_session( .as_ref() .and_then(|value| openshell_core::time::duration_to_std(value).ok()) .map_or(5, |value| value.as_secs().max(5)); - validate_gateway_protocol_revision(accepted.protocol_revision)?; if let Some(updates) = &config.session_id_updates { updates.send_replace(Some(accepted.session_id.clone())); } @@ -511,24 +509,6 @@ async fn run_single_session( } } -fn validate_gateway_protocol_revision( - gateway_revision: u32, -) -> Result<(), Box> { - match gateway_revision { - SUPERVISOR_PROTOCOL_REVISION => Ok(()), - LEGACY_SUPERVISOR_PROTOCOL_REVISION => { - warn!( - "supervisor session: gateway predates the protocol handshake; upgrade the gateway before pinning newer supervisor images" - ); - Ok(()) - } - other => Err(format!( - "supervisor protocol revision mismatch: supervisor requires {SUPERVISOR_PROTOCOL_REVISION}, gateway offered {other}" - ) - .into()), - } -} - /// Report the canonical process result and wait for durable handling. pub async fn report_main_process_exit( endpoint: &str, @@ -962,19 +942,6 @@ fn normalize_tcp_target_host(target: &TcpRelayTarget) -> Result mod target_tests { use super::*; - #[test] - fn gateway_protocol_revision_accepts_current_and_legacy_peers() { - assert!(validate_gateway_protocol_revision(SUPERVISOR_PROTOCOL_REVISION).is_ok()); - assert!(validate_gateway_protocol_revision(LEGACY_SUPERVISOR_PROTOCOL_REVISION).is_ok()); - } - - #[test] - fn gateway_protocol_revision_rejects_unknown_peers() { - let error = validate_gateway_protocol_revision(SUPERVISOR_PROTOCOL_REVISION + 1) - .expect_err("version skew must be rejected"); - assert!(error.to_string().contains("revision mismatch")); - } - fn tcp(host: &str, port: u32) -> TcpRelayTarget { TcpRelayTarget { host: host.to_string(), diff --git a/proto/openshell.proto b/proto/openshell.proto index e74a4ae17..032c9452b 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -424,6 +424,8 @@ service OpenShell { } // Get sandbox settings by id (called by sandbox entrypoint and poll loop). + // Polling projection of the sandbox configuration delivered over + // ConnectSupervisor; removed with the polling RPCs. rpc GetSandboxConfig(openshell.sandbox.v1.GetSandboxConfigRequest) returns (openshell.sandbox.v1.GetSandboxConfigResponse) { option (openshell.options.v1.authorization) = { @@ -509,7 +511,9 @@ service OpenShell { option (openshell.options.v1.authorization) = { auth_mode: "sandbox" }; } - // Get provider environment for a sandbox (called by sandbox supervisor at startup). + // Get provider environment for a sandbox (called by the sandbox supervisor at + // startup and by its poll loop). Polling projection of the provider + // environment delivered over ConnectSupervisor; removed with the polling RPCs. rpc GetSandboxProviderEnvironment(GetSandboxProviderEnvironmentRequest) returns (GetSandboxProviderEnvironmentResponse) { option (openshell.options.v1.authorization) = { @@ -547,8 +551,9 @@ service OpenShell { // The supervisor opens this stream at startup and keeps it alive for the // sandbox lifetime. The gateway uses it to coordinate relay channels for // SSH connect, ExecSandbox, targetable sandbox services, and configuration - // delivery. Peers must report the same exact protocol_revision during the - // handshake. Raw service bytes flow over RelayStream calls (separate HTTP/2 + // delivery. Supervisors advertise optional stream features in + // SupervisorHello, and the gateway sends only payloads the supervisor + // advertises. Raw service bytes flow over RelayStream calls (separate HTTP/2 // streams on the same connection), not over this stream. rpc ConnectSupervisor(stream SupervisorMessage) returns (stream GatewayMessage) { option (openshell.options.v1.authorization) = { @@ -2632,7 +2637,9 @@ message StaticCredentialBinding { string workload_credential_handle = 3; } -// Get sandbox provider environment response. +// Polling projection of ProviderEnvironmentSnapshot. The parallel maps below +// are keyed by the same environment variable names. Removed with the polling +// RPCs; new provider environment fields belong on the snapshot first. message GetSandboxProviderEnvironmentResponse { reserved 3; reserved "credential_expires_at_ms"; @@ -2664,25 +2671,29 @@ message GetSandboxProviderEnvironmentResponse { map files = 10; } +// Receivers treat UNSPECIFIED and unknown values as secret. enum ProviderEnvironmentValueClassification { PROVIDER_ENVIRONMENT_VALUE_CLASSIFICATION_UNSPECIFIED = 0; PROVIDER_ENVIRONMENT_VALUE_CLASSIFICATION_NON_SECRET = 1; PROVIDER_ENVIRONMENT_VALUE_CLASSIFICATION_STATIC_CREDENTIAL = 2; } -// One environment value and all metadata that shares its key. +// One environment value and all metadata that shares its key. Names are unique +// within a snapshot. static_credential_binding is present exactly when +// classification is STATIC_CREDENTIAL. Snapshots that violate either rule are +// invalid. message ProviderEnvironmentValue { - reserved 3; - reserved "expires_at_ms"; string name = 1; string value = 2 [(openshell.options.v1.secret) = true]; ProviderEnvironmentValueClassification classification = 4; StaticCredentialBinding static_credential_binding = 5; // Absolute expiration time. Absence means no expiry. - google.protobuf.Timestamp expiration_time = 103; + google.protobuf.Timestamp expiration_time = 3; } -// Complete provider environment state delivered to a supervisor. Dynamic +// Complete provider environment state delivered to a supervisor. This is the +// canonical provider environment shape; GetSandboxProviderEnvironmentResponse +// is its polling projection and is removed with the polling RPCs. Dynamic // credentials are endpoint selectors rather than environment values and stay // in their own collection. message ProviderEnvironmentSnapshot { @@ -3049,9 +3060,9 @@ message SupervisorHello { uint64 connection_epoch = 3; // The supervisor can report credential, policy, and launch-environment installation. bool supports_provider_readiness = 4; - // Exact internal stream protocol revision implemented by this supervisor. - // Zero identifies a supervisor built before the handshake existed. - uint32 protocol_revision = 5; + // The supervisor accepts ConfigBootstrap and ConfigUpdate payloads on this + // stream. Gateways send configuration payloads only when this is set. + bool supports_config_snapshots = 5; } // Gateway accepts the supervisor session. @@ -3060,11 +3071,11 @@ message SessionAccepted { reserved "heartbeat_interval_secs"; // Gateway-assigned session ID for this connection. string session_id = 1; - // Complete gateway-owned configuration. During the staged rollout this may - // be omitted only when the gateway cannot build the projection. + // Complete gateway-owned configuration. Sent only to supervisors that set + // SupervisorHello.supports_config_snapshots. During the staged rollout this + // may also be omitted when push delivery is disabled or the gateway cannot + // build the projection. ConfigBootstrap bootstrap = 3; - // Exact internal stream protocol revision implemented by this gateway. - uint32 protocol_revision = 4; // Recommended heartbeat interval. google.protobuf.Duration heartbeat_interval = 102; } diff --git a/proto/sandbox.proto b/proto/sandbox.proto index 18d257b2f..cfed316ba 100644 --- a/proto/sandbox.proto +++ b/proto/sandbox.proto @@ -400,7 +400,11 @@ enum PolicySource { POLICY_SOURCE_GLOBAL = 2; } -// Complete effective sandbox configuration delivered to a supervisor. +// Complete effective sandbox configuration delivered to a supervisor. Fields +// have the same meaning as the identically numbered GetSandboxConfigResponse +// fields, including fail-closed handling of an unknown +// policy_validation_failure_mode and treating absent configuration_admitted as +// not admitted. Move those field comments here when the polling RPC is removed. message SandboxConfigSnapshot { SandboxPolicy policy = 1; uint32 version = 2; @@ -420,7 +424,8 @@ message SandboxConfigSnapshot { string configuration_error = 16; } -// Response containing effective sandbox settings and policy. +// Response containing effective sandbox settings and policy. Polling +// projection of SandboxConfigSnapshot; removed with the polling RPCs. message GetSandboxConfigResponse { // The sandbox policy configuration. SandboxPolicy policy = 1; diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index dad41e711..36bfd3448 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -729,6 +729,7 @@ func (ProviderProfileCategory) EnumDescriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{10} } +// Receivers treat UNSPECIFIED and unknown values as secret. type ProviderEnvironmentValueClassification int32 const ( @@ -11213,7 +11214,9 @@ func (x *StaticCredentialBinding) GetWorkloadCredentialHandle() string { return "" } -// Get sandbox provider environment response. +// Polling projection of ProviderEnvironmentSnapshot. The parallel maps below +// are keyed by the same environment variable names. Removed with the polling +// RPCs; new provider environment fields belong on the snapshot first. type GetSandboxProviderEnvironmentResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Provider credential environment variables. @@ -11346,7 +11349,10 @@ func (x *GetSandboxProviderEnvironmentResponse) GetFiles() map[string]string { return nil } -// One environment value and all metadata that shares its key. +// One environment value and all metadata that shares its key. Names are unique +// within a snapshot. static_credential_binding is present exactly when +// classification is STATIC_CREDENTIAL. Snapshots that violate either rule are +// invalid. type ProviderEnvironmentValue struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -11354,7 +11360,7 @@ type ProviderEnvironmentValue struct { Classification ProviderEnvironmentValueClassification `protobuf:"varint,4,opt,name=classification,proto3,enum=openshell.v1.ProviderEnvironmentValueClassification" json:"classification,omitempty"` StaticCredentialBinding *StaticCredentialBinding `protobuf:"bytes,5,opt,name=static_credential_binding,json=staticCredentialBinding,proto3" json:"static_credential_binding,omitempty"` // Absolute expiration time. Absence means no expiry. - ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,103,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + ExpirationTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -11424,7 +11430,9 @@ func (x *ProviderEnvironmentValue) GetExpirationTime() *timestamppb.Timestamp { return nil } -// Complete provider environment state delivered to a supervisor. Dynamic +// Complete provider environment state delivered to a supervisor. This is the +// canonical provider environment shape; GetSandboxProviderEnvironmentResponse +// is its polling projection and is removed with the polling RPCs. Dynamic // credentials are endpoint selectors rather than environment values and stay // in their own collection. type ProviderEnvironmentSnapshot struct { @@ -13659,11 +13667,11 @@ type SupervisorHello struct { ConnectionEpoch uint64 `protobuf:"varint,3,opt,name=connection_epoch,json=connectionEpoch,proto3" json:"connection_epoch,omitempty"` // The supervisor can report credential, policy, and launch-environment installation. SupportsProviderReadiness bool `protobuf:"varint,4,opt,name=supports_provider_readiness,json=supportsProviderReadiness,proto3" json:"supports_provider_readiness,omitempty"` - // Exact internal stream protocol revision implemented by this supervisor. - // Zero identifies a supervisor built before the handshake existed. - ProtocolRevision uint32 `protobuf:"varint,5,opt,name=protocol_revision,json=protocolRevision,proto3" json:"protocol_revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The supervisor accepts ConfigBootstrap and ConfigUpdate payloads on this + // stream. Gateways send configuration payloads only when this is set. + SupportsConfigSnapshots bool `protobuf:"varint,5,opt,name=supports_config_snapshots,json=supportsConfigSnapshots,proto3" json:"supports_config_snapshots,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SupervisorHello) Reset() { @@ -13724,11 +13732,11 @@ func (x *SupervisorHello) GetSupportsProviderReadiness() bool { return false } -func (x *SupervisorHello) GetProtocolRevision() uint32 { +func (x *SupervisorHello) GetSupportsConfigSnapshots() bool { if x != nil { - return x.ProtocolRevision + return x.SupportsConfigSnapshots } - return 0 + return false } // Gateway accepts the supervisor session. @@ -13736,11 +13744,11 @@ type SessionAccepted struct { state protoimpl.MessageState `protogen:"open.v1"` // Gateway-assigned session ID for this connection. SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` - // Complete gateway-owned configuration. During the staged rollout this may - // be omitted only when the gateway cannot build the projection. + // Complete gateway-owned configuration. Sent only to supervisors that set + // SupervisorHello.supports_config_snapshots. During the staged rollout this + // may also be omitted when push delivery is disabled or the gateway cannot + // build the projection. Bootstrap *ConfigBootstrap `protobuf:"bytes,3,opt,name=bootstrap,proto3" json:"bootstrap,omitempty"` - // Exact internal stream protocol revision implemented by this gateway. - ProtocolRevision uint32 `protobuf:"varint,4,opt,name=protocol_revision,json=protocolRevision,proto3" json:"protocol_revision,omitempty"` // Recommended heartbeat interval. HeartbeatInterval *durationpb.Duration `protobuf:"bytes,102,opt,name=heartbeat_interval,json=heartbeatInterval,proto3" json:"heartbeat_interval,omitempty"` unknownFields protoimpl.UnknownFields @@ -13791,13 +13799,6 @@ func (x *SessionAccepted) GetBootstrap() *ConfigBootstrap { return nil } -func (x *SessionAccepted) GetProtocolRevision() uint32 { - if x != nil { - return x.ProtocolRevision - } - return 0 -} - func (x *SessionAccepted) GetHeartbeatInterval() *durationpb.Duration { if x != nil { return x.HeartbeatInterval @@ -19683,13 +19684,13 @@ const file_openshell_proto_rawDesc = "" + "\n" + "FilesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x03\x10\x04R\x18credential_expires_at_ms\"\xe5\x02\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x03\x10\x04R\x18credential_expires_at_ms\"\xd0\x02\n" + "\x18ProviderEnvironmentValue\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1a\n" + "\x05value\x18\x02 \x01(\tB\x04\x88\xb5\x18\x01R\x05value\x12\\\n" + "\x0eclassification\x18\x04 \x01(\x0e24.openshell.v1.ProviderEnvironmentValueClassificationR\x0eclassification\x12a\n" + "\x19static_credential_binding\x18\x05 \x01(\v2%.openshell.v1.StaticCredentialBindingR\x17staticCredentialBinding\x12C\n" + - "\x0fexpiration_time\x18g \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTimeJ\x04\b\x03\x10\x04R\rexpires_at_ms\"\xaa\x05\n" + + "\x0fexpiration_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x0eexpirationTime\"\xaa\x05\n" + "\x1bProviderEnvironmentSnapshot\x122\n" + "\x15provider_env_revision\x18\x01 \x01(\x04R\x13providerEnvRevision\x12>\n" + "\x06values\x18\x02 \x03(\v2&.openshell.v1.ProviderEnvironmentValueR\x06values\x12r\n" + @@ -19877,20 +19878,19 @@ const file_openshell_proto_rawDesc = "" + "\vrelay_close\x18\x05 \x01(\v2\x18.openshell.v1.RelayCloseH\x00R\n" + "relayClose\x12A\n" + "\rconfig_update\x18\x06 \x01(\v2\x1a.openshell.v1.ConfigUpdateH\x00R\fconfigUpdateB\t\n" + - "\apayload\"\xe9\x01\n" + + "\apayload\"\xf8\x01\n" + "\x0fSupervisorHello\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1f\n" + "\vinstance_id\x18\x02 \x01(\tR\n" + "instanceId\x12)\n" + "\x10connection_epoch\x18\x03 \x01(\x04R\x0fconnectionEpoch\x12>\n" + - "\x1bsupports_provider_readiness\x18\x04 \x01(\bR\x19supportsProviderReadiness\x12+\n" + - "\x11protocol_revision\x18\x05 \x01(\rR\x10protocolRevision\"\x83\x02\n" + + "\x1bsupports_provider_readiness\x18\x04 \x01(\bR\x19supportsProviderReadiness\x12:\n" + + "\x19supports_config_snapshots\x18\x05 \x01(\bR\x17supportsConfigSnapshots\"\xd6\x01\n" + "\x0fSessionAccepted\x12\x1d\n" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12;\n" + - "\tbootstrap\x18\x03 \x01(\v2\x1d.openshell.v1.ConfigBootstrapR\tbootstrap\x12+\n" + - "\x11protocol_revision\x18\x04 \x01(\rR\x10protocolRevision\x12H\n" + + "\tbootstrap\x18\x03 \x01(\v2\x1d.openshell.v1.ConfigBootstrapR\tbootstrap\x12H\n" + "\x12heartbeat_interval\x18f \x01(\v2\x19.google.protobuf.DurationR\x11heartbeatIntervalJ\x04\b\x02\x10\x03R\x17heartbeat_interval_secs\"\xc3\x01\n" + "\x0fConfigBootstrap\x12R\n" + "\x0esandbox_config\x18\x01 \x01(\v2+.openshell.sandbox.v1.SandboxConfigSnapshotR\rsandboxConfig\x12\\\n" + diff --git a/sdk/go/proto/openshellv1/openshell_grpc.pb.go b/sdk/go/proto/openshellv1/openshell_grpc.pb.go index f8bccdd30..4a8e31927 100644 --- a/sdk/go/proto/openshellv1/openshell_grpc.pb.go +++ b/sdk/go/proto/openshellv1/openshell_grpc.pb.go @@ -215,6 +215,8 @@ type OpenShellClient interface { // Delete a custom provider type profile by id. DeleteProviderProfile(ctx context.Context, in *DeleteProviderProfileRequest, opts ...grpc.CallOption) (*DeleteProviderProfileResponse, error) // Get sandbox settings by id (called by sandbox entrypoint and poll loop). + // Polling projection of the sandbox configuration delivered over + // ConnectSupervisor; removed with the polling RPCs. GetSandboxConfig(ctx context.Context, in *sandboxv1.GetSandboxConfigRequest, opts ...grpc.CallOption) (*sandboxv1.GetSandboxConfigResponse, error) // Get gateway-global settings (read-only runtime configuration; any // authenticated user may read these without requiring Platform Admin). @@ -239,7 +241,9 @@ type OpenShellClient interface { ReportProviderReadiness(ctx context.Context, in *ReportProviderReadinessRequest, opts ...grpc.CallOption) (*ReportProviderReadinessResponse, error) // Register startup and acknowledge an exact validated runtime configuration. ReportSandboxConfiguration(ctx context.Context, in *ReportSandboxConfigurationRequest, opts ...grpc.CallOption) (*ReportSandboxConfigurationResponse, error) - // Get provider environment for a sandbox (called by sandbox supervisor at startup). + // Get provider environment for a sandbox (called by the sandbox supervisor at + // startup and by its poll loop). Polling projection of the provider + // environment delivered over ConnectSupervisor; removed with the polling RPCs. GetSandboxProviderEnvironment(ctx context.Context, in *GetSandboxProviderEnvironmentRequest, opts ...grpc.CallOption) (*GetSandboxProviderEnvironmentResponse, error) // Exchange a stored provider subject token for an intermediate token scoped // to the calling supervisor's SPIFFE identity. @@ -253,8 +257,9 @@ type OpenShellClient interface { // The supervisor opens this stream at startup and keeps it alive for the // sandbox lifetime. The gateway uses it to coordinate relay channels for // SSH connect, ExecSandbox, targetable sandbox services, and configuration - // delivery. Peers must report the same exact protocol_revision during the - // handshake. Raw service bytes flow over RelayStream calls (separate HTTP/2 + // delivery. Supervisors advertise optional stream features in + // SupervisorHello, and the gateway sends only payloads the supervisor + // advertises. Raw service bytes flow over RelayStream calls (separate HTTP/2 // streams on the same connection), not over this stream. ConnectSupervisor(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SupervisorMessage, GatewayMessage], error) // Persist the canonical main process result before the supervisor exits. @@ -1323,6 +1328,8 @@ type OpenShellServer interface { // Delete a custom provider type profile by id. DeleteProviderProfile(context.Context, *DeleteProviderProfileRequest) (*DeleteProviderProfileResponse, error) // Get sandbox settings by id (called by sandbox entrypoint and poll loop). + // Polling projection of the sandbox configuration delivered over + // ConnectSupervisor; removed with the polling RPCs. GetSandboxConfig(context.Context, *sandboxv1.GetSandboxConfigRequest) (*sandboxv1.GetSandboxConfigResponse, error) // Get gateway-global settings (read-only runtime configuration; any // authenticated user may read these without requiring Platform Admin). @@ -1347,7 +1354,9 @@ type OpenShellServer interface { ReportProviderReadiness(context.Context, *ReportProviderReadinessRequest) (*ReportProviderReadinessResponse, error) // Register startup and acknowledge an exact validated runtime configuration. ReportSandboxConfiguration(context.Context, *ReportSandboxConfigurationRequest) (*ReportSandboxConfigurationResponse, error) - // Get provider environment for a sandbox (called by sandbox supervisor at startup). + // Get provider environment for a sandbox (called by the sandbox supervisor at + // startup and by its poll loop). Polling projection of the provider + // environment delivered over ConnectSupervisor; removed with the polling RPCs. GetSandboxProviderEnvironment(context.Context, *GetSandboxProviderEnvironmentRequest) (*GetSandboxProviderEnvironmentResponse, error) // Exchange a stored provider subject token for an intermediate token scoped // to the calling supervisor's SPIFFE identity. @@ -1361,8 +1370,9 @@ type OpenShellServer interface { // The supervisor opens this stream at startup and keeps it alive for the // sandbox lifetime. The gateway uses it to coordinate relay channels for // SSH connect, ExecSandbox, targetable sandbox services, and configuration - // delivery. Peers must report the same exact protocol_revision during the - // handshake. Raw service bytes flow over RelayStream calls (separate HTTP/2 + // delivery. Supervisors advertise optional stream features in + // SupervisorHello, and the gateway sends only payloads the supervisor + // advertises. Raw service bytes flow over RelayStream calls (separate HTTP/2 // streams on the same connection), not over this stream. ConnectSupervisor(grpc.BidiStreamingServer[SupervisorMessage, GatewayMessage]) error // Persist the canonical main process result before the supervisor exits. diff --git a/sdk/go/proto/sandboxv1/sandbox.pb.go b/sdk/go/proto/sandboxv1/sandbox.pb.go index a4b47b687..861039bbe 100644 --- a/sdk/go/proto/sandboxv1/sandbox.pb.go +++ b/sdk/go/proto/sandboxv1/sandbox.pb.go @@ -1970,7 +1970,11 @@ func (x *EffectiveSetting) GetScope() SettingScope { return SettingScope_SETTING_SCOPE_UNSPECIFIED } -// Complete effective sandbox configuration delivered to a supervisor. +// Complete effective sandbox configuration delivered to a supervisor. Fields +// have the same meaning as the identically numbered GetSandboxConfigResponse +// fields, including fail-closed handling of an unknown +// policy_validation_failure_mode and treating absent configuration_admitted as +// not admitted. Move those field comments here when the polling RPC is removed. type SandboxConfigSnapshot struct { state protoimpl.MessageState `protogen:"open.v1"` Policy *SandboxPolicy `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"` @@ -2135,7 +2139,8 @@ func (x *SandboxConfigSnapshot) GetConfigurationError() string { return "" } -// Response containing effective sandbox settings and policy. +// Response containing effective sandbox settings and policy. Polling +// projection of SandboxConfigSnapshot; removed with the polling RPCs. type GetSandboxConfigResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The sandbox policy configuration.