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
10 changes: 6 additions & 4 deletions .agents/skills/debug-openshell-cluster/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Use gateway metadata, deployment values, or the user's setup notes to identify t

Before debugging the compute platform, inspect gateway logs for failures in dependencies initialized before the listener becomes ready.

For out-of-tree compute drivers, confirm the custom driver name and socket agree across CLI flags or `gateway.toml`, and that the operator-owned driver is running before the gateway starts:
For out-of-tree compute drivers, confirm the selected driver name and socket agree across CLI flags or `gateway.toml`, and that the operator-owned driver is running before the gateway starts:

```bash
rg -n 'compute_drivers|socket_path' /etc/openshell/gateway.toml
Expand All @@ -80,7 +80,7 @@ journalctl -u <driver-service> --no-pager --lines=200
journalctl -u openshell-gateway --no-pager --lines=200
```

The custom driver name must not be a reserved built-in name (`docker`, `podman`, `kubernetes`, or `vm`). The socket must be accessible only to the intended gateway identity. Check gateway logs for connection errors, `GetCapabilities` failures, or an unexpected advertised driver name. The gateway does not create or supervise out-of-tree driver processes or sockets.
Custom names use `[openshell.drivers.<name>].socket_path`. A launch-time `--compute-driver-socket` override may also use `docker`, `podman`, `kubernetes`, or `vm`; the endpoint then takes precedence over built-in construction. The socket must be accessible only to the intended gateway identity. Check gateway logs for connection errors, `GetCapabilities` failures, or an unexpected advertised driver name. The advertised name is diagnostic metadata; negotiated features control optional behavior. The gateway does not create or supervise operator-supplied driver processes or sockets.

For configured gateway interceptors, inspect `[[openshell.gateway.interceptors]]`, their Unix or network endpoints, and gateway startup logs:

Expand Down Expand Up @@ -199,7 +199,9 @@ running intent should stop before the gateway exits and restart after it
returns. Check for `Stopped sandbox during gateway shutdown` and `Started
sandbox during gateway startup` in gateway logs. A sandbox explicitly stopped
through the CLI remains stopped. Kubernetes sandboxes are cluster-owned and do
not follow this local gateway lifecycle.
not follow this local gateway lifecycle. Internal and external drivers follow
the same rule: `GetCapabilities.gateway_managed_lifecycle` must be true for the
gateway to run shutdown and startup sweeps.

### Step 5: Check Podman-Backed Gateways

Expand Down Expand Up @@ -593,7 +595,7 @@ openshell logs <sandbox-name>
| Binary WebSocket message passes without a middleware RPC | Binary is unsupported by the V1 text-message binding under both `on_error` modes | `WEBSOCKET_MIDDLEWARE_COVERAGE state=unsupported_message_type`; the next text RPC may have a valid sequence gap |
| WebSocket messages stop reaching middleware after one failure | A fail-open stage stream was disabled for the rest of the connection | `openshell.middleware.websocket_stage_disabled`; middleware timeout/stream/protocol logs. A per-message capacity bypass alone leaves the stage active. Reconnect to create a fresh stream after a genuine stream failure |
| Supervisor repeatedly fails to install middleware after enabling gateway JWT signing | Extension credential minting, distribution, or authenticated service connection failed; last-known-good registry remains active | Gateway `RefreshSandboxToken` logs, sandbox configuration events, service token-verification logs, registration TLS/audience settings |
| Custom compute driver is unavailable | Driver process/socket missing, inaccessible, or configured with a reserved/mismatched name | Socket ownership/mode, driver service logs, gateway `GetCapabilities` logs |
| Custom compute driver is unavailable | Driver process/socket missing, inaccessible, or selected name does not match its endpoint/config key | Socket ownership/mode, driver service logs, gateway `GetCapabilities` logs |
| Sandbox remains `Stopping` or `Starting` | Driver stop/start failed, retained resource is missing, or a fresh supervisor has not connected | Gateway and driver logs; `docker inspect`, `podman inspect`, Agent Sandbox status/PVC, or VM state marker and launcher process |
| Image pull failure | Gateway or sandbox image cannot be pulled | Runtime events and image pull credentials |
| `K8s namespace not ready` with `envoy-gateway-openshell.yaml: the server could not find the requested resource` | Optional Gateway API manifest was applied without Envoy Gateway CRDs, or k3s Helm controller startup exceeded the namespace wait | Apply `deploy/kube/manifests/envoy-gateway-openshell.yaml` manually only after Envoy Gateway is installed and `grpcRoute` is enabled |
Expand Down
14 changes: 13 additions & 1 deletion architecture/compute-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ references to gateway-internal types. The gateway owns the public
`SandboxPhase::Ready` decision. This applies equally to extension drivers
implementing `ComputeDriver` out of tree.

`compute_driver.proto` is the supported gateway/driver extension boundary.
At initialization the gateway snapshots the driver's identity, version,
default image, and gateway-lifecycle preference from `GetCapabilities`.
Process-identity omissions are preserved across this boundary so every driver
can apply its native image or runtime defaults. Driver-requested listeners are
structurally validated and remain restricted to sandbox callback RPCs.

Drivers own runtime-specific platform event interpretation. When an event should
drive client provisioning UI, the driver attaches the shared
`openshell.progress.*` metadata defined in `openshell-core` instead of requiring
Expand Down Expand Up @@ -115,6 +122,11 @@ shared idempotent `StartSandbox` RPC before watch processing begins. Explicitly
cluster-owned and continue running without gateway shutdown or startup
lifecycle calls.

The driver reports this behavior through
`GetCapabilities.gateway_managed_lifecycle`. The same declaration works for
in-process and external drivers. Older drivers omit the field and retain the
conservative operator-managed behavior.

## Deletion Lifecycle

Lifecycle requests use per-sandbox gates to serialize stop, start, and
Expand Down Expand Up @@ -152,7 +164,7 @@ delete, reconciliation removes the row; otherwise it can remain `Deleting`.
| Podman | Rootless or single-machine deployments. | Container plus nested sandbox namespace. | Uses the Podman REST API and CDI GPU devices when available. Delivers the supervisor via OCI image volume by default; falls back to extracting the binary to a host-side cache and bind-mounting it when `userns` is configured (overlay does not support idmapped mounts). |
| Kubernetes | Cluster deployment through Helm. | Pod plus nested sandbox namespace. | Uses Kubernetes API objects, service accounts, secrets, PVC-backed workspace storage, and GPU resources. |
| VM | Experimental microVM isolation. | Per-sandbox libkrun VM. | Managed endpoint-backed driver. The gateway spawns `openshell-driver-vm`, waits for its Unix socket, and then consumes it through the same remote `compute_driver.proto` path used by unmanaged endpoint drivers. The VM driver boots a cached bootstrap `rootfs.ext4`, prepares requested OCI images inside a bootstrap VM with `umoci`, attaches the prepared image disk read-only, and gives each sandbox a writable `overlay.ext4` for merged-root changes and runtime material. The driver persists each accepted launch request beside the overlay and restarts those VMs on driver startup without recreating the overlay. |
| Extension | Out-of-tree drivers operated alongside the gateway. | Whatever boundary the driver implements. | Selected by a non-reserved custom `compute_drivers = ["<name>"]` entry with `[openshell.drivers.<name>].socket_path`, or at launch time by pairing `--drivers <name>` with `--compute-driver-socket=<path>`. Reserved built-in names such as `vm`, `docker`, `podman`, and `kubernetes` cannot be used as unmanaged socket endpoints. The gateway connects to a UDS the operator already provisioned, runs `GetCapabilities`, logs the advertised `driver_name`, and dispatches all sandbox lifecycle calls through `compute_driver.proto`. The driver process and socket lifecycle are operator-owned; the gateway does not spawn, supervise, or remove unmanaged extension drivers. The trust boundary is the socket's filesystem permissions: the operator must ensure only the gateway uid can read/write it. |
| Extension | Out-of-tree drivers operated alongside the gateway. | Whatever boundary the driver implements. | Selected by a custom `compute_drivers = ["<name>"]` entry with `[openshell.drivers.<name>].socket_path`, or at launch time by pairing `--drivers <name>` with `--compute-driver-socket=<path>`. A launch-time endpoint may use a canonical built-in name to preserve its driver-config key while replacing in-process construction. The gateway connects to an operator-provisioned UDS, snapshots `GetCapabilities`, and dispatches all sandbox lifecycle calls through `compute_driver.proto`. The driver process and socket lifecycle are operator-owned; the gateway does not spawn, supervise, or remove unmanaged extension drivers. The trust boundary is the socket's filesystem permissions: the operator must ensure only the gateway uid can read/write it. |

Per-sandbox CPU and memory values currently enter the driver layer through
template resource limits. Docker and Podman apply them as runtime limits.
Expand Down
2 changes: 2 additions & 0 deletions crates/openshell-core/src/driver_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,11 +380,13 @@ pub fn build_capabilities_response(
driver_name: &str,
driver_version: impl Into<String>,
default_image: impl Into<String>,
gateway_managed_lifecycle: bool,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: where is this helper used? Docker, Podman, and Kubernetes use it, while VM constructs GetCapabilitiesResponse directly. Since gateway_managed_lifecycle is now a meaningful per-driver contract decision, consider constructing the response directly in each driver and removing this thin helper. That would keep each driver's advertised capabilities visible together.

) -> GetCapabilitiesResponse {
GetCapabilitiesResponse {
driver_name: driver_name.to_string(),
driver_version: driver_version.into(),
default_image: default_image.into(),
gateway_managed_lifecycle,
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/openshell-driver-docker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ impl DockerComputeDriver {
"docker",
&self.config.daemon_version,
&self.config.default_image,
true,
)
}

Expand Down
1 change: 1 addition & 0 deletions crates/openshell-driver-kubernetes/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ impl KubernetesComputeDriver {
"kubernetes",
openshell_core::VERSION,
&self.config.default_image,
false,
))
}

Expand Down
1 change: 1 addition & 0 deletions crates/openshell-driver-podman/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ impl PodmanComputeDriver {
"podman",
openshell_core::VERSION,
&self.config.default_image,
true,
))
}

Expand Down
1 change: 1 addition & 0 deletions crates/openshell-driver-vm/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ impl VmDriver {
driver_name: DRIVER_NAME.to_string(),
driver_version: openshell_core::VERSION.to_string(),
default_image: self.config.default_image.clone(),
gateway_managed_lifecycle: true,
}
}

Expand Down
41 changes: 10 additions & 31 deletions crates/openshell-server/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ struct RunArgs {
/// implementing `compute_driver.proto`.
///
/// When set, the socket is associated with the single driver name supplied
/// by `--drivers` or `OPENSHELL_DRIVERS`. Reserved built-in driver names
/// such as Docker, Podman, Kubernetes, and VM do not accept socket
/// endpoints.
/// by `--drivers` or `OPENSHELL_DRIVERS`. The endpoint overrides built-in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not the case that this endpoint overrides normal driver construction for every selected driver name? Canonical built-in names are the newly supported case, but a custom selected name also resolves through the supplied endpoint. Please phrase this as a general override rule rather than limiting it to Docker, Podman, Kubernetes, and VM.

/// construction when the selected name is Docker, Podman, Kubernetes, or
/// VM.
#[arg(long, env = "OPENSHELL_COMPUTE_DRIVER_SOCKET")]
compute_driver_socket: Option<PathBuf>,

Expand Down Expand Up @@ -755,27 +755,14 @@ fn normalize_compute_driver_socket_args(args: &mut RunArgs, matches: &ArgMatches
}
if arg_defaulted(matches, "drivers") {
return Err(miette::miette!(
"--compute-driver-socket requires --drivers <name> or OPENSHELL_DRIVERS=<name> to select a non-reserved compute driver name"
"--compute-driver-socket requires --drivers <name> or OPENSHELL_DRIVERS=<name> to select a compute driver name"
));
}

match args.drivers.as_slice() {
[driver] => {
let driver = openshell_core::config::normalize_compute_driver_name(driver)
.map_err(|err| miette::miette!("{err}"))?;
if matches!(
driver.parse::<ComputeDriverKind>().ok(),
Some(
ComputeDriverKind::Docker
| ComputeDriverKind::Podman
| ComputeDriverKind::Kubernetes
| ComputeDriverKind::Vm
)
) {
return Err(miette::miette!(
"--compute-driver-socket cannot be combined with reserved built-in compute driver '{driver}'"
));
}
args.drivers[0] = driver;
Ok(())
}
Expand Down Expand Up @@ -1663,7 +1650,7 @@ ssh_session_ttl_secs = 1234
}

#[test]
fn compute_driver_socket_rejects_reserved_builtin_drivers() {
fn compute_driver_socket_accepts_canonical_builtin_driver_name() {
let _lock = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Expand All @@ -1679,16 +1666,12 @@ ssh_session_ttl_secs = 1234
"--compute-driver-socket",
"/run/openshell/extension.sock",
]);
let err = super::normalize_compute_driver_socket_args(&mut args, &matches).unwrap_err();
assert!(
err.to_string()
.contains("cannot be combined with reserved built-in compute driver 'docker'"),
"unexpected error: {err}"
);
super::normalize_compute_driver_socket_args(&mut args, &matches).unwrap();
assert_eq!(args.drivers, ["docker"]);
}

#[test]
fn compute_driver_socket_rejects_vm_endpoint() {
fn compute_driver_socket_accepts_vm_endpoint() {
let _lock = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Expand All @@ -1704,12 +1687,8 @@ ssh_session_ttl_secs = 1234
"--compute-driver-socket",
"/run/openshell/vm.sock",
]);
let err = super::normalize_compute_driver_socket_args(&mut args, &matches).unwrap_err();
assert!(
err.to_string()
.contains("cannot be combined with reserved built-in compute driver 'vm'"),
"unexpected error: {err}"
);
super::normalize_compute_driver_socket_args(&mut args, &matches).unwrap();
assert_eq!(args.drivers, ["vm"]);
}

#[test]
Expand Down
Loading
Loading