Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .agents/skills/helm-dev-environment/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,28 @@ above) instead of relying only on `OPENSHELL_GATEWAY_ENDPOINT`; some tests call
### Kubernetes E2E Notes

Use `mise run e2e:kubernetes` for the standard Helm-backed Kubernetes suite.
Set `OPENSHELL_E2E_KUBE_TEST=kubernetes_image_update` to scope the Rust suite to
image replacement with retained sandbox, SSH, and PVC identities. This target is
included in both existing Agent Sandbox API-version lanes; unlike the Python
Docker suite, it exercises the driver that supports image updates.

Image updates default to disabled outside test fixtures. The e2e wrapper opts
in only for a fresh gateway fixture that it owns; set
`OPENSHELL_E2E_KUBE_IMAGE_UPDATES=0` to prevent that opt-in. Do not propagate
activation to an existing shared gateway or enable it during a mixed-version
rollout. For manual testing, follow the retirement and activation checklist in
`docs/kubernetes/high-availability.mdx` before setting
`gatewayConfig: {"openshell.gateway": {enable_sandbox_image_updates: true}}`.
The gateway process equivalents are `--enable-sandbox-image-updates true` and
`OPENSHELL_ENABLE_SANDBOX_IMAGE_UPDATES=true`. Retire every old process and Job,
settle outstanding driver work, prevent old binaries returning, and do not
treat PostgreSQL connection markers or membership TTL expiry as retirement
proof. SQLite testing supports one gateway process only. Image-update clients
must call the separate `SandboxUpdates` service on the existing gateway
listener, not add a method to the baseline `OpenShell` service or inspect new
public `SandboxStatus` image fields. Pending-operation recovery uses the same
requested image with a fresh version and the documented request-ID semantics.

The kube e2e wrapper creates only one port-forward, to `svc/openshell`; it no
longer forwards the unauthenticated health listener or runs a `/readyz` e2e
target. `/readyz` remains covered by server unit/integration tests.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions crates/openshell-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ configuration validation live in `openshell_core::middleware`. Policy admission
and the supervisor runtime consume the same contract without introducing a
dependency from the policy crate to the supervisor implementation.

## Optional Compute Image Updates

The generated `proto::compute::v1::compute_driver_server::ComputeDriver` trait
retains its baseline methods and streaming associated type. External drivers
that support image updates additionally serve `ComputeDriverImageUpdates` on
the same endpoint and advertise `openshell.compute.update-sandbox-image`.
Drivers without that capability need no new generated-trait method.

Existing linked in-process drivers and factories need no new trait or hook.
Only drivers opting into image updates implement
`openshell_core::compute::ComputeDriver` and register that implementation through
`ComputeDriverFactory::build_with_image_updates`. The default factory hook calls
the existing `build` method without an updater. The gateway requires both this
separate registration and the negotiated capability before invoking updates.

## Protobuf Struct Conversion

Use `openshell_core::proto_struct` when crossing between `serde_json` values and
Expand Down
26 changes: 26 additions & 0 deletions crates/openshell-core/src/compute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Opt-in in-process image updates, separate from the baseline driver contract.

use crate::proto::compute::v1::{
UpdateSandboxImageRequest, UpdateSandboxImageResponse, compute_driver_server,
};

/// Optional in-process image updater, registered separately by a driver factory.
///
/// Existing drivers need only the generated baseline trait, not this extension.
/// Supporting factories opt in through `ComputeDriverFactory::build_with_image_updates`
/// and advertise the image-update capability. External servers use the separate
/// generated `ComputeDriverImageUpdates` service on the same endpoint instead.
#[tonic::async_trait]
pub trait ComputeDriver: compute_driver_server::ComputeDriver {
async fn update_sandbox_image(
&self,
_request: tonic::Request<UpdateSandboxImageRequest>,
) -> Result<tonic::Response<UpdateSandboxImageResponse>, tonic::Status> {
Err(tonic::Status::unimplemented(
"storage-preserving image updates are not supported by this driver",
))
}
}
6 changes: 6 additions & 0 deletions crates/openshell-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ pub struct Config {
/// Disabled for local gateways by default.
pub enable_websocket_tunnel: bool,

/// Operator activation after all gateways and outstanding legacy driver
/// operations have been drained onto the image-update-compatible protocol.
/// Disabled by default; not a license to run mixed-version writers.
pub enable_sandbox_image_updates: bool,

/// Disabled-by-default gateway interceptor service configs.
pub gateway_interceptors: Vec<GatewayInterceptorConfig>,

Expand Down Expand Up @@ -905,6 +910,7 @@ impl Config {
oidc: None,
auth: GatewayAuthConfig::default(),
enable_websocket_tunnel: false,
enable_sandbox_image_updates: false,
gateway_interceptors: Vec::new(),
provider_profile_sources: vec![GatewayProviderProfileSourceConfig::User],
mtls_auth: MtlsAuthConfig::default(),
Expand Down
21 changes: 21 additions & 0 deletions crates/openshell-core/src/extension_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub const COMPUTE_LAUNCH_AUTHENTICATION: &str = "openshell.compute.launch-authen
/// Driver stages operator-selected external isolation routes for the supervisor.
pub const COMPUTE_ISOLATION_BACKEND: &str = "openshell.compute.isolation-backend";

/// Optional stopped-state image updates retaining the resource and its storage.
pub const COMPUTE_UPDATE_SANDBOX_IMAGE: &str = "openshell.compute.update-sandbox-image";

const MAX_IMPLEMENTATION_NAME_BYTES: usize = 128;
const MAX_IMPLEMENTATION_VERSION_BYTES: usize = 128;
const MAX_CAPABILITY_BYTES: usize = 128;
Expand Down Expand Up @@ -125,6 +128,7 @@ pub fn gateway_metadata(family: ExtensionFamily) -> PeerMetadata {
if family == ExtensionFamily::Compute {
supported_capabilities.push(COMPUTE_LAUNCH_AUTHENTICATION.to_string());
supported_capabilities.push(COMPUTE_ISOLATION_BACKEND.to_string());
supported_capabilities.push(COMPUTE_UPDATE_SANDBOX_IMAGE.to_string());
}
PeerMetadata {
protocol_version: Some(ProtocolVersion {
Expand Down Expand Up @@ -396,6 +400,23 @@ fn valid_capability(value: &str) -> bool {
mod tests {
use super::*;

#[test]
fn compute_gateway_advertises_both_optional_extensions() {
let gateway = gateway_metadata(ExtensionFamily::Compute);
assert!(
gateway
.supported_capabilities
.iter()
.any(|c| c == COMPUTE_ISOLATION_BACKEND)
);
assert!(
gateway
.supported_capabilities
.iter()
.any(|c| c == COMPUTE_UPDATE_SANDBOX_IMAGE)
);
}

fn compatible() -> (PeerMetadata, PeerMetadata) {
let gateway = gateway_metadata(ExtensionFamily::Compute);
let extension = extension_metadata(
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

pub mod activity;
pub mod auth;
pub mod compute;
pub mod config;
pub mod container_paths;
pub mod denial;
Expand Down
Loading
Loading