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
10 changes: 10 additions & 0 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ 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.
When configured, a gateway runtime settings file reconciles selected
gateway-global settings into the same settings row. Keys present in that file
are file-managed; omitted keys stay available through the normal global
settings API.

Provider credential expiry is enforced during gateway-to-sandbox credential
resolution and again by the sandbox placeholder resolver. This keeps expired
Expand Down Expand Up @@ -422,6 +426,12 @@ Driver implementation settings live in the TOML driver tables. See
`docs/reference/gateway-config.mdx` for worked per-driver examples and RFC
0003 for the full schema.

Startup configuration can reference a separate runtime settings file with
`runtime_config_path`. That file is watched after startup and is intentionally
limited to registered runtime settings such as provider/profile and logging
feature flags. It does not configure drivers, listeners, providers,
credentials, or global policy payloads.

`database_url` is env-only and rejected when present in the file
(`OPENSHELL_DB_URL` / `--db-url`).

Expand Down
10 changes: 10 additions & 0 deletions crates/openshell-server/src/config_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ pub struct GatewayFileSection {
// ── Logging ──────────────────────────────────────────────────────────
#[serde(default)]
pub log_level: Option<String>,
/// Path to an optional gateway runtime settings file. When set, the
/// gateway loads registered runtime settings from this file at startup and
/// watches it for changes.
#[serde(default)]
pub runtime_config_path: Option<PathBuf>,

// ── Drivers ──────────────────────────────────────────────────────────
#[serde(default)]
Expand Down Expand Up @@ -351,6 +356,7 @@ version = 1
bind_address = "0.0.0.0:8080"
health_bind_address = "0.0.0.0:8081"
log_level = "info"
runtime_config_path = "/etc/openshell/runtime.toml"
compute_drivers = ["kubernetes"]
sandbox_namespace = "agents"
grpc_rate_limit_requests = 120
Expand All @@ -377,6 +383,10 @@ grpc_endpoint = "https://openshell-gateway.agents.svc:8080"
let file = load(tmp.path()).expect("valid file parses");
let gw = &file.openshell.gateway;
assert_eq!(gw.log_level.as_deref(), Some("info"));
assert_eq!(
gw.runtime_config_path.as_deref(),
Some(Path::new("/etc/openshell/runtime.toml"))
);
assert_eq!(
gw.default_image.as_deref(),
Some("ghcr.io/nvidia/openshell/sandbox:latest")
Expand Down
10 changes: 5 additions & 5 deletions crates/openshell-server/src/grpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,18 @@ const MAX_PROVIDER_CONFIG_ENTRIES: usize = 64;
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct StoredSettings {
revision: u64,
settings: BTreeMap<String, StoredSettingValue>,
pub struct StoredSettings {
pub revision: u64,
pub settings: BTreeMap<String, StoredSettingValue>,
/// Database `resource_version` for CAS. Not persisted in the JSON payload;
/// loaded from `ObjectRecord` and used for optimistic concurrency control.
#[serde(skip)]
resource_version: u64,
pub resource_version: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", content = "value")]
enum StoredSettingValue {
pub enum StoredSettingValue {
String(String),
Bool(bool),
Int(i64),
Expand Down
57 changes: 52 additions & 5 deletions crates/openshell-server/src/grpc/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1605,6 +1605,11 @@ async fn handle_update_config_inner(
if key != POLICY_SETTING_KEY {
validate_registered_setting_key(key)?;
}
if state.runtime_settings.is_managed_key(key) {
return Err(Status::failed_precondition(format!(
"setting '{key}' is managed by the gateway runtime config file; update that file instead"
)));
}

let mut global_settings = load_global_settings(state.store.as_ref()).await?;
let changed = if req.delete_setting {
Expand Down Expand Up @@ -3698,14 +3703,11 @@ fn upsert_setting_value(
}
}

pub(super) async fn load_global_settings(store: &Store) -> Result<StoredSettings, Status> {
pub async fn load_global_settings(store: &Store) -> Result<StoredSettings, Status> {
load_settings_record(store, GLOBAL_SETTINGS_OBJECT_TYPE, GLOBAL_SETTINGS_NAME).await
}

pub(super) async fn save_global_settings(
store: &Store,
settings: &StoredSettings,
) -> Result<(), Status> {
pub async fn save_global_settings(store: &Store, settings: &StoredSettings) -> Result<(), Status> {
save_settings_record(
store,
GLOBAL_SETTINGS_OBJECT_TYPE,
Expand Down Expand Up @@ -8825,6 +8827,51 @@ mod tests {
);
}

#[tokio::test]
async fn update_config_global_rejects_set_for_runtime_managed_key() {
let state = test_server_state().await;
state
.runtime_settings
.set_managed_keys([settings::PROVIDERS_V2_ENABLED_KEY.to_string()]);

let req = with_user(Request::new(UpdateConfigRequest {
global: true,
setting_key: settings::PROVIDERS_V2_ENABLED_KEY.to_string(),
setting_value: Some(SettingValue {
value: Some(setting_value::Value::BoolValue(false)),
}),
..Default::default()
}));
let err = handle_update_config(&state, req)
.await
.expect_err("runtime-managed setting must reject global set");
assert_eq!(err.code(), Code::FailedPrecondition);
assert!(
err.message().contains("runtime config file"),
"expected runtime config file guidance; got: {}",
err.message()
);
}

#[tokio::test]
async fn update_config_global_rejects_delete_for_runtime_managed_key() {
let state = test_server_state().await;
state
.runtime_settings
.set_managed_keys([settings::PROVIDERS_V2_ENABLED_KEY.to_string()]);

let req = with_user(Request::new(UpdateConfigRequest {
global: true,
setting_key: settings::PROVIDERS_V2_ENABLED_KEY.to_string(),
delete_setting: true,
..Default::default()
}));
let err = handle_update_config(&state, req)
.await
.expect_err("runtime-managed setting must reject global delete");
assert_eq!(err.code(), Code::FailedPrecondition);
}

#[cfg(feature = "dev-settings")]
#[test]
fn merge_effective_settings_global_overrides_sandbox_key() {
Expand Down
29 changes: 29 additions & 0 deletions crates/openshell-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ mod persistence;
pub(crate) mod policy_store;
mod provider_refresh;
mod readiness;
mod runtime_config;
mod sandbox_index;
mod sandbox_watch;
mod service_routing;
Expand Down Expand Up @@ -107,6 +108,9 @@ pub struct ServerState {
/// mutations that reads global state.
pub settings_mutex: tokio::sync::Mutex<()>,

/// Runtime settings managed by the optional gateway runtime config file.
pub(crate) runtime_settings: runtime_config::RuntimeSettingsState,

/// Registry of active supervisor sessions and pending relay channels.
///
/// Stored as `Arc` so compute drivers (e.g. the Docker driver)
Expand Down Expand Up @@ -179,6 +183,7 @@ impl ServerState {
ssh_connections_by_token: Mutex::new(HashMap::new()),
ssh_connections_by_sandbox: Mutex::new(HashMap::new()),
settings_mutex: tokio::sync::Mutex::new(()),
runtime_settings: runtime_config::RuntimeSettingsState::default(),
supervisor_sessions,
oidc_cache,
sandbox_jwt_issuer: None,
Expand Down Expand Up @@ -254,6 +259,27 @@ pub async fn run_server(
oidc_cache,
);

let runtime_config_path = config_file
.as_ref()
.and_then(|file| file.openshell.gateway.runtime_config_path.clone());
if let Some(path) = runtime_config_path.as_ref() {
let outcome = runtime_config::apply_file(
path,
store.as_ref(),
&state.settings_mutex,
&state.runtime_settings,
)
.await
.map_err(|e| Error::config(e.to_string()))?;
info!(
path = %path.display(),
changed = outcome.changed,
settings_revision = outcome.revision,
managed_key_count = outcome.managed_key_count,
"runtime config file applied"
);
}

// Load the gateway-minted sandbox JWT signing key when configured.
// Optional so single-driver dev deployments without certgen continue
// to start. The helm-deployed gateway and the RPM init script populate
Expand Down Expand Up @@ -353,6 +379,9 @@ pub async fn run_server(
}

state.compute.spawn_watchers(shutdown_rx.clone());
if let Some(path) = runtime_config_path {
runtime_config::spawn_watcher(state.clone(), path, shutdown_rx.clone());
}
ssh_sessions::spawn_session_reaper(store.clone(), Duration::from_secs(3600));
supervisor_session::spawn_relay_reaper(state.clone(), Duration::from_secs(30));
provider_refresh::spawn_refresh_worker(state.clone(), Duration::from_secs(60));
Expand Down
Loading
Loading