Skip to content
Draft
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
2 changes: 1 addition & 1 deletion architecture/compute-runtimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ process-supervision leaf and launches the user workload after the sidecar
serves bootstrap state over a local control socket. The network sidecar owns
gateway credentials and sends policy plus workload-facing provider environment
state to the process leaf over that socket. It also streams provider
environment updates after settings polls so future process sessions see
environment updates received from the gateway so future process sessions see
updated provider env without giving the process leaf gateway access. The
pre-workload process supervisor is the only accepted control client: the
network sidecar verifies its UID, GID, and PID with peer credentials, removes
Expand Down
8 changes: 4 additions & 4 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,8 +467,8 @@ still referenced by a sandbox.

Policy and runtime settings are delivered together through the effective sandbox
config path. A gateway-global policy can override sandbox-scoped policy. The
sandbox supervisor polls for config revisions and hot-reloads dynamic policy
when the policy engine accepts the update.
gateway pushes changed snapshots over `ConnectSupervisor`, and the sandbox
supervisor hot-reloads dynamic policy when the policy engine accepts the update.

External supervisor middleware registration is operator-owned configuration
under `[[openshell.supervisor.middleware]]`. At startup the gateway connects to
Expand Down Expand Up @@ -505,8 +505,8 @@ include profile endpoint and binding changes.

Cluster inference routes store only `provider_name`, `model_id`, and optional
timeout. The gateway resolves endpoint URLs, protocols, credentials, auth
style, and route-shaping metadata from the provider record when supervisors call
`GetInferenceBundle`. Supported provider types for cluster inference are
style, and route-shaping metadata from the provider record when the gateway
builds supervisor bootstrap and update snapshots. Supported provider types for cluster inference are
`openai`, `anthropic`, `nvidia`, `deepinfra`, and `google-vertex-ai`.

The bundle carries enough information for sandbox-local routers to construct
Expand Down
12 changes: 6 additions & 6 deletions architecture/google-vertex-ai-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ Gateway (openshell-server)
│ ├── infer_vertex_publisher() model → publisher
│ └── vertex_location_and_host() region → Vertex API host
└── GetInferenceBundleRequest (from sandbox on connect)
└── resolve_route_by_name() re-resolves live route+credentials
└── ConfigBootstrap / ConfigUpdate snapshot builder
└── resolve_route_by_name() resolves live route+credentials

Router (openshell-router)
Expand Down Expand Up @@ -412,10 +412,10 @@ Provider type normalization for the `ProviderRegistry` (non-inference providers

## 9. Inference Routing in the Sandbox

When a sandbox agent connects to `https://inference.local`, the sandbox fetches the
inference bundle from the gateway (`GetInferenceBundleRequest`). The bundle contains one
or more `ResolvedRoute` proto messages built by `resolve_route_by_name`. For a Vertex AI
route the bundle contains:
Before a sandbox agent connects to `https://inference.local`, the supervisor applies the
inference snapshot delivered through `ConnectSupervisor`. The snapshot contains one or
more `ResolvedRoute` proto messages built by `resolve_route_by_name`. For a Vertex AI
route the snapshot contains:

```
ResolvedRoute {
Expand Down
84 changes: 63 additions & 21 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Each sandbox workload has two trust levels:

| Process | Role |
|---|---|
| Supervisor | Starts as root inside the workload, prepares isolation, runs the proxy, fetches config, injects credentials, serves the relay socket, and launches child processes. |
| Supervisor | Starts as root inside the workload, applies desired state, prepares isolation, runs the proxy, injects credentials, serves the relay socket, and launches child processes. |
| Agent child | Runs as an unprivileged user with filesystem, process, and network restrictions applied. |

The supervisor keeps enough privilege to manage the sandbox, but the agent child
Expand All @@ -25,15 +25,55 @@ only when the set is already empty; any other outcome fails the spawn.

1. The compute runtime starts the workload with sandbox identity, callback
endpoint, TLS or secret material, image metadata, and initial command.
2. The supervisor loads policy and runtime settings from local files or the
gateway, depending on mode.
3. It prepares filesystem access, process restrictions, network namespace
2. In gateway-backed mode, the supervisor opens `ConnectSupervisor` before it
initializes gateway-owned runtime state. The gateway sends a complete
configuration, provider environment, and inference bootstrap.
3. The supervisor applies the bootstrap, or loads explicit local files in
standalone mode. A required configuration or policy failure aborts startup.
4. It prepares filesystem access, process restrictions, network namespace
routing, trust stores, provider credential resolution, and inference routes.
4. It launches the persisted canonical main-process argv and retains its PTY
5. It launches the persisted canonical main-process argv and retains its PTY
or pipes in the main-session multiplexer.
5. It starts the policy proxy and local SSH server.
6. It opens a supervisor session back to the gateway for connect, exec, file
sync, config polling, and log push.
6. It starts the policy proxy and local SSH server, then signals runtime-ready.
7. The gateway enables connect, exec, file sync, and other relay operations only
after both bootstrap initialization and runtime-ready are complete.

## Desired-State Delivery

`ConnectSupervisor` carries gateway-owned desired state. A fresh session always
starts with a complete `ConfigBootstrap`. Bootstrap success marks the session
initialized; `SupervisorRuntimeReady` separately proves that runtime-dependent
services are available. Bootstrap failure or a 120-second timeout moves the
sandbox to `Error`. Disconnect before initialization returns it to
`Provisioning`.

After bootstrap, the gateway sends level-triggered `ConfigUpdate` messages for
one component at a time. Request IDs correlate results, component-local sequence
numbers reject stale delivery, and snapshot revisions remain equality-only
content fingerprints. The gateway keeps at most one update in flight for each
component and coalesces newer state. Failed live updates keep the sandbox
`Ready` and add a component-specific degraded condition. Policy and inference
retain their last-known-good runtime state. Invalid provider bindings fail
closed by revoking static credential material while retaining fetched dynamic
token grants.

Committed sandbox changes notify the session owner immediately. Provider and
inference changes use a workspace-wide invalidation signal. A 30-second
jittered reconciliation pass rebuilds snapshots for active locally owned
sessions, repairing missed notifications and cross-replica writes. Reconnects
discard queued state and begin again with one complete bootstrap.

Explicit local policy and inference files remain authoritative. Their component
results report `RETAINED_LOCAL_OVERRIDE`, while non-conflicting gateway settings
and provider state continue to update. A supervisor without a gateway does not
open or wait for a session.

`GetSandboxConfig` remains a public read API. Policy status, policy analysis,
log upload, credential refresh, and relay RPCs remain independent of desired
state delivery. The old supervisor-only provider-environment and inference-bundle
fetch RPCs no longer exist. Snapshot builders and component apply routines are
shared implementation boundaries for a future direct transport; the runtime
does not add a transport abstraction before that transport exists.

## Isolation Layers

Expand Down Expand Up @@ -195,10 +235,9 @@ the registry. Public custom-CA PEM travels with the stable registration.

The slots live in a supervisor-owned `ExtensionCredentialStore` shared by every
gateway connection the supervisor opens, so the registry's clients and the
polling loop that rotates them observe the same credentials. Configuration
polling runs far more frequently than credentials expire, so the loop rotates
only when a credential is missing or has passed four fifths of its lifetime,
and bounds its sleep by the soonest rotation deadline.
independent credential-refresh task observe the same credentials. The task
rotates only when a credential is missing or has passed four fifths of its
lifetime.

Middleware cannot observe injected credentials or mutate supervisor-owned
credential, routing, or framing headers. Body transformations are re-evaluated
Expand Down Expand Up @@ -442,9 +481,9 @@ policy structure.
This holds even when the initial policy is enriched with baseline paths during
startup: the enriched revision the supervisor synced back to the gateway is the
revision it acknowledges, so a successfully constructed initial policy never
remains `Pending`. If the first poll returns a different revision, the supervisor
processes it through the normal reload path instead of treating it as already
loaded.
remains `Pending`. If a subsequent pushed snapshot carries a different revision,
the supervisor processes it through the normal reload path instead of treating
it as already loaded.

A newer sandbox-scoped revision can carry the same non-empty effective policy
hash as the currently loaded revision, for example when provenance changes
Expand All @@ -459,19 +498,22 @@ Policy status delivery uses a FIFO background worker. Retryable delivery
failures retain the ordered update and retry with capped exponential backoff;
terminal errors are logged and discarded. The outbox is nonblocking and does
not discard updates because of a fixed queue capacity, so status endpoint
outages cannot block policy polling, enforcement, settings, or provider
refreshes and cannot permanently lose the initial acknowledgement.
outages cannot block desired-state application, enforcement, settings, or
provider refreshes and cannot permanently lose the initial acknowledgement.

Only sandbox-scoped revisions (`PolicySource::Sandbox`, version greater than
zero) are acknowledged. Global policies and local-file development policies do
not use the sandbox revision API and produce no acknowledgement. When explicit
local Rego and data files are configured, the supervisor continues polling the
gateway for settings and provider refreshes but never replaces the local OPA
engine with a gateway policy revision.
local Rego and data files are configured, pushed gateway updates still apply
settings and provider changes but never replace the local OPA engine with a
gateway policy revision.

## Failure Behavior

- If gateway config polling fails, the sandbox keeps its last-known-good policy.
- If desired-state delivery or application fails after startup, the sandbox
remains ready and reports a degraded condition. Policy and inference retain
their last-known-good state; invalid provider bindings follow the fail-closed
revocation behavior above.
- If a live policy or middleware-registry update is invalid, the supervisor
rejects the combined update and keeps the current runtime pair.
- If an operator-run middleware call fails, the selected config's `on_error`
Expand Down
7 changes: 4 additions & 3 deletions architecture/security-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,10 @@ detection finding at startup naming the inactive controls.
The gateway stores sandbox-authored policy revisions separately from derived
effective sandbox configuration. Effective configuration can include
gateway-global policy overrides and provider-profile policy layers. The
supervisor polls for config revisions and attempts to load new dynamic policy
into the in-process OPA engine; CLI reads of the latest sandbox policy use the
same effective configuration path.
gateway pushes effective configuration revisions over the supervisor session.
The supervisor validates and loads changed dynamic policy into the in-process
OPA engine; CLI reads of the latest sandbox policy use the same effective
configuration path.

The supervisor validates complete effective policy generations before
activation. Overlapping endpoint selectors may contribute request allow and
Expand Down
38 changes: 19 additions & 19 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ use openshell_core::proto::{
ExposeServiceRequest, GetCurrentUserRequest, GetDraftHistoryRequest, GetDraftPolicyRequest,
GetGatewayConfigRequest, GetInferenceRouteRequest, GetProviderProfileRequest,
GetProviderRefreshStatusRequest, GetProviderRequest, GetSandboxConfigRequest,
GetSandboxConfigResponse, GetSandboxLogsRequest, GetSandboxPolicyStatusRequest,
GetSandboxRequest, GetServiceRequest, GpuResourceRequirements, ImportProviderProfilesRequest,
LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest,
ListSandboxPoliciesRequest, ListSandboxProvidersRequest, ListSandboxesRequest,
ListServicesRequest, PolicySource, PolicyStatus, Provider,
ProviderCredentialRefreshRecoveryAction, ProviderCredentialRefreshStatus,
ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantType, ProviderProfile,
ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest,
ResourceRequirements, RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox,
GetSandboxLogsRequest, GetSandboxPolicyStatusRequest, GetSandboxRequest, GetServiceRequest,
GpuResourceRequirements, ImportProviderProfilesRequest, LintProviderProfilesRequest,
ListProviderProfilesRequest, ListProvidersRequest, ListSandboxPoliciesRequest,
ListSandboxProvidersRequest, ListSandboxesRequest, ListServicesRequest, PolicySource,
PolicyStatus, Provider, ProviderCredentialRefreshRecoveryAction,
ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy,
ProviderCredentialTokenGrantType, ProviderProfile, ProviderProfileDiagnostic,
ProviderProfileImportItem, RejectDraftChunkRequest, ResourceRequirements,
RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, SandboxConfigSnapshot,
SandboxPhase, SandboxPolicy, SandboxSpec, SandboxTemplate, ServiceEndpointResponse,
SetInferenceRouteRequest, SettingScope, StartSandboxRequest, StopSandboxRequest,
TcpForwardFrame, TcpForwardInit, TcpRelayTarget, UpdateConfigRequest,
Expand Down Expand Up @@ -2109,7 +2109,7 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value {

fn sandbox_detail_to_json(
sandbox: &Sandbox,
config: &GetSandboxConfigResponse,
config: &SandboxConfigSnapshot,
) -> Result<serde_json::Value> {
let mut value = sandbox_to_json(sandbox);
let obj = value
Expand Down Expand Up @@ -5935,7 +5935,7 @@ pub async fn gateway_settings_get(server: &str, json: bool, tls: &TlsOptions) ->
fn settings_to_json_sandbox(
name: &str,
workspace: &str,
response: &GetSandboxConfigResponse,
response: &SandboxConfigSnapshot,
) -> serde_json::Value {
let policy_source = if response.policy_source == PolicySource::Global as i32 {
"global"
Expand Down Expand Up @@ -7429,12 +7429,12 @@ mod tests {
PROGRESS_STEP_STARTING_SANDBOX,
};
use openshell_core::proto::{
GetSandboxConfigResponse, GpuResourceRequirements, PolicySource, PolicyStatus, Provider,
ProviderCredentialRefresh, ProviderCredentialRefreshRecoveryAction,
ProviderCredentialRefreshStatus, ProviderCredentialRefreshStrategy,
ProviderCredentialTokenGrant, ProviderProfile, ProviderProfileCredential,
ResourceRequirements, Sandbox, SandboxCondition, SandboxPhase, SandboxPolicyRevision,
SandboxStatus, datamodel::v1::ObjectMeta,
GpuResourceRequirements, PolicySource, PolicyStatus, Provider, ProviderCredentialRefresh,
ProviderCredentialRefreshRecoveryAction, ProviderCredentialRefreshStatus,
ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrant, ProviderProfile,
ProviderProfileCredential, ResourceRequirements, Sandbox, SandboxCondition,
SandboxConfigSnapshot, SandboxPhase, SandboxPolicyRevision, SandboxStatus,
datamodel::v1::ObjectMeta,
};

#[test]
Expand Down Expand Up @@ -8789,7 +8789,7 @@ mod tests {
sandbox.set_phase(SandboxPhase::Ready as i32);
sandbox.set_current_policy_version(2);

let config = GetSandboxConfigResponse {
let config = SandboxConfigSnapshot {
policy_source: PolicySource::Global as i32,
global_policy_version: 3,
..Default::default()
Expand All @@ -8815,7 +8815,7 @@ mod tests {
}),
..Default::default()
};
let config = GetSandboxConfigResponse {
let config = SandboxConfigSnapshot {
policy_source: PolicySource::Sandbox as i32,
version: 0,
..Default::default()
Expand Down
27 changes: 8 additions & 19 deletions crates/openshell-cli/tests/ensure_providers_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ use openshell_core::proto::{
DetachSandboxProviderRequest, DetachSandboxProviderResponse,
ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent,
ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest,
GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest,
GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest,
GetSandboxProviderEnvironmentResponse, GetSandboxRequest, HealthRequest, HealthResponse,
ListProvidersRequest, ListProvidersResponse, ListSandboxProvidersRequest,
ListSandboxProvidersResponse, ListSandboxesRequest, ListSandboxesResponse, Provider,
ProviderResponse, RevokeSshSessionRequest, RevokeSshSessionResponse, SandboxResponse,
SandboxStreamEvent, ServiceStatus, SupervisorMessage, UpdateProviderRequest,
WatchSandboxRequest,
GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, GetSandboxRequest,
HealthRequest, HealthResponse, ListProvidersRequest, ListProvidersResponse,
ListSandboxProvidersRequest, ListSandboxProvidersResponse, ListSandboxesRequest,
ListSandboxesResponse, Provider, ProviderResponse, RevokeSshSessionRequest,
RevokeSshSessionResponse, SandboxConfigSnapshot, SandboxResponse, SandboxStreamEvent,
ServiceStatus, SupervisorMessage, UpdateProviderRequest, WatchSandboxRequest,
};
use openshell_core::{ObjectId, ObjectName};
use std::collections::HashMap;
Expand Down Expand Up @@ -178,8 +176,8 @@ impl OpenShell for TestOpenShell {
async fn get_sandbox_config(
&self,
_request: tonic::Request<GetSandboxConfigRequest>,
) -> Result<Response<GetSandboxConfigResponse>, Status> {
Ok(Response::new(GetSandboxConfigResponse::default()))
) -> Result<Response<SandboxConfigSnapshot>, Status> {
Ok(Response::new(SandboxConfigSnapshot::default()))
}

async fn get_gateway_config(
Expand All @@ -189,15 +187,6 @@ impl OpenShell for TestOpenShell {
Ok(Response::new(GetGatewayConfigResponse::default()))
}

async fn get_sandbox_provider_environment(
&self,
_request: tonic::Request<GetSandboxProviderEnvironmentRequest>,
) -> Result<Response<GetSandboxProviderEnvironmentResponse>, Status> {
Ok(Response::new(
GetSandboxProviderEnvironmentResponse::default(),
))
}

async fn create_ssh_session(
&self,
_request: tonic::Request<CreateSshSessionRequest>,
Expand Down
14 changes: 2 additions & 12 deletions crates/openshell-cli/tests/mtls_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ impl OpenShell for TestOpenShell {
async fn get_sandbox_config(
&self,
_request: tonic::Request<openshell_core::proto::GetSandboxConfigRequest>,
) -> Result<Response<openshell_core::proto::GetSandboxConfigResponse>, Status> {
) -> Result<Response<openshell_core::proto::SandboxConfigSnapshot>, Status> {
Ok(Response::new(
openshell_core::proto::GetSandboxConfigResponse::default(),
openshell_core::proto::SandboxConfigSnapshot::default(),
))
}

Expand All @@ -160,16 +160,6 @@ impl OpenShell for TestOpenShell {
))
}

async fn get_sandbox_provider_environment(
&self,
_request: tonic::Request<openshell_core::proto::GetSandboxProviderEnvironmentRequest>,
) -> Result<Response<openshell_core::proto::GetSandboxProviderEnvironmentResponse>, Status>
{
Ok(Response::new(
openshell_core::proto::GetSandboxProviderEnvironmentResponse::default(),
))
}

async fn create_ssh_session(
&self,
_request: tonic::Request<CreateSshSessionRequest>,
Expand Down
Loading
Loading