Repository navigation
refactor: clean up gateway.toml config schema inconsistencies #2792
Description
Problem Statement
The gateway.toml configuration schema has accumulated 21 distinct inconsistencies across the gateway core and its four compute-driver tables (kubernetes, docker, podman, vm): misplaced fields (Kubernetes-specific settings living at the gateway level), inconsistent field naming for the same concept across drivers, overloaded 0 sentinel values instead of Option<T>, duplicated TLS fields with no practical use for the duplication, and feature parity gaps (proxy config, AppArmor, SPIFFE support present on some drivers but arbitrarily missing from others). This makes the config surface harder to document, harder to reason about, and easy to misconfigure. This issue proposes a structured cleanup.
Technical Context
[openshell.gateway] holds both true gateway concerns (listeners, TLS, auth) and driver-inheritable defaults (image names, TLS paths, K8s-only settings) that flow into driver tables via an allowlist-based inheritance mechanism. Because only one compute driver is active per gateway process, several fields designed for "shared defaults across drivers" provide no real value and only add indirection. Driver tables also evolved independently, so equivalent concepts (SSH socket path, gRPC endpoint, image pull policy, PID limits) have different names, types, and default-value conventions per driver.
Affected Components
| Component | Key Files | Role |
|---|---|---|
| Gateway config core | crates/openshell-core/src/config.rs (lines ~413-1576) |
Core config types: GatewayJwtConfig, GatewayInterceptorConfig, compute driver kind parsing |
| Gateway config file loader | crates/openshell-server/src/config_file.rs (lines ~45-1073) |
TOML parsing, schema versioning, driver-table inheritance (driver_table(), inheritable_keys(), gateway_inherited_value()) |
| Kubernetes driver config | crates/openshell-driver-kubernetes/src/config.rs (lines ~93-1473) |
Most complete driver config: AppArmor, SPIFFE, proxy, topology |
| Docker driver config | crates/openshell-driver-docker/src/lib.rs (lines ~88-500) |
Missing proxy config, AppArmor; raw-string pull policy |
| Podman driver config | crates/openshell-driver-podman/src/config.rs (lines ~72-633) |
Typed pull-policy enum; has proxy config; inconsistent SSH socket field name |
| VM driver config | crates/openshell-driver-vm/src/driver.rs (lines ~216-300) |
Inconsistent endpoint field name; divergent UID default; no proxy config |
| Published docs | docs/reference/gateway-config.mdx (~1051 lines) |
Full schema reference; needs updates for any renamed/moved/retyped field |
Technical Investigation
Architecture Overview
Config loading happens in crates/openshell-server/src/config_file.rs:
- TOML is parsed into
ConfigFile(schema version validated againstSCHEMA_VERSION = 1; secrets likedatabase_urlare rejected if present in the file). driver_table()(config_file.rs:331-360) merges[openshell.gateway]defaults into the active driver's table.- Each driver has an allowlist of inheritable keys,
inheritable_keys()(config_file.rs:362-386):- Kubernetes:
namespace, default_image, supervisor_image, client_tls_secret_name, service_account_name, host_gateway_ip, enable_user_namespaces, sa_token_ttl_secs - Docker:
sandbox_namespace, default_image, supervisor_image, host_gateway_ip, guest_tls_ca, guest_tls_cert, guest_tls_key - Podman:
default_image, supervisor_image, host_gateway_ip, guest_tls_ca, guest_tls_cert, guest_tls_key - VM:
default_image, guest_tls_ca, guest_tls_cert, guest_tls_key - Remote/custom drivers: no inheritance
- Kubernetes:
gateway_inherited_value()(config_file.rs:388-402) copies the value, and this is where a single gateway key (sandbox_namespace) maps to different meanings per driver (K8snamespacevs. Dockersandbox_namespace/label) - a source of confusion in its own right.- Precedence overall: CLI flag >
OPENSHELL_*env var > TOML file > built-in default (documented indocs/reference/gateway-config.mdx:16-19).
Because only one compute driver runs per gateway process (despite compute_drivers being typed as Vec<String>, config_file.rs:115 / config.rs:480), the inheritance model exists mainly to avoid repeating a handful of values, but has become a source of misplaced ownership (Kubernetes-only settings sitting at the "shared" gateway level) rather than genuine cross-driver sharing.
Code References
| # | Issue | File:Line | Current Behavior |
|---|---|---|---|
| 1 | gateway.sandbox_namespace is K8s-specific |
config_file.rs:122 |
Gateway-level field inherited as namespace by K8s (:373), as sandbox_namespace by Docker (:377); unused by Podman/VM |
| 2 | gateway.service_account_name is K8s-specific |
config_file.rs:125 |
Inherited only by Kubernetes (:374) |
| 3 | gateway.enable_user_namespaces is driver-level |
config_file.rs:127 |
Inherited only by Kubernetes (:376) |
| 4 | gateway.compute_drivers plural but single-active |
config_file.rs:115, config.rs:480 |
Option<Vec<String>>; runtime only uses the first entry |
| 5 | gateway_jwt.ttl_secs overloads 0 |
config.rs:767-768 |
default_sandbox_token_ttl_secs() -> 0; 0 means "never expire" |
| 6 | docker.sandbox_namespace should be sandbox_label |
docker/lib.rs:105 |
Named like a K8s namespace but is actually a Docker container label |
| 7 | docker.sandbox_pids_limit overloads 0 |
docker/lib.rs:174, config.rs:123 |
i64; 0 means "use Docker default" |
| 8 | docker.image_pull_policy raw String |
docker/lib.rs:103 |
Podman has a typed ImagePullPolicy enum (podman/config.rs:25) |
| 9 | Docker missing proxy config | docker/lib.rs:88-180 vs podman/config.rs:170-196 |
No https_proxy/no_proxy/proxy_auth_* fields |
| 10 | podman.sandbox_pids_limit overloads 0 |
podman/config.rs:108 |
Same pattern as #7 |
| 11 | podman.health_check_interval_secs overloads 0 |
podman/config.rs:119-126 |
u64; 0 = disable |
| 12 | podman.sandbox_ssh_socket_path inconsistent name |
podman/config.rs:82 |
K8s/Docker use ssh_socket_path |
| 13 | vm.openshell_endpoint inconsistent name |
vm/driver.rs:217 |
Other drivers use grpc_endpoint |
| 14 | VM sandbox_uid default (10001) vs K8s (1000) |
vm/driver.rs:244 vs kubernetes/config.rs:341 |
DEFAULT_SANDBOX_UID diverges across drivers |
| 15 | VM missing proxy config | vm/driver.rs:216-260 |
No proxy fields, unlike Podman/K8s |
| 16 | grpc_endpoint/openshell_endpoint duplicated per driver |
all driver configs | Gateway already knows its own bind address; endpoint is redundantly configured per driver |
| 17 | Interceptors vs. supervisor middleware field naming | config.rs:653 (max_response_bytes) vs config_file.rs:228 (max_body_bytes) |
Structurally identical concept (body size limit), different field name |
| 18 | provider_spiffe_workload_api_socket_path is K8s-only |
kubernetes/config.rs:368-371 |
Not exposed for Docker/Podman/VM despite SPIFFE being runtime-agnostic |
| 19 | image_pull_policy type inconsistency |
podman/config.rs:25-57 (enum) vs kubernetes/config.rs:251 (String) vs docker/lib.rs:103 (String) |
No shared type; valid values differ per runtime (Podman adds Newer) |
| 20 | guest_tls_ca/cert/key duplicated everywhere |
config_file.rs:129-131 + docker/lib.rs:159-161, podman/config.rs:101-103, vm/driver.rs:233-235 |
Defined at gateway level and independently in every driver struct; only one driver active at a time |
| 21 | kubernetes.app_armor_profile is K8s-only |
kubernetes/config.rs:141-209, config.rs:302-307 |
AppArmor is a Linux kernel feature; Docker/Podman support --security-opt apparmor=<profile> too |
Current Behavior
0-overload pattern appears in at least 3 independent fields (gateway_jwt.ttl_secs,sandbox_pids_limitin both Docker and Podman,health_check_interval_secsin Podman), each reimplementing the same "0 means special case" convention with no shared idiom, and no way to distinguish "unset" from "explicitly zero."- Inheritance key remapping: the same gateway key can mean different things per driver (
sandbox_namespacemaps to K8snamespacevs. Docker label), which is confusing when reading gateway.toml in isolation. - TLS field triplication:
guest_tls_ca/cert/keyexist at the gateway level (for inheritance) and are redundantly redeclared in Docker, Podman, and VM driver structs, even though only one driver is ever active. - Image pull policy has three different representations across drivers with no shared validation, so typos in Kubernetes/Docker configs are only caught (or not caught) at runtime by the container runtime itself, not at config-parse time.
- Feature parity gaps: proxy config (Podman, Kubernetes only), AppArmor (Kubernetes only), SPIFFE (Kubernetes only) are runtime-agnostic capabilities arbitrarily scoped to specific drivers, apparently because that's where they were first implemented rather than because of a technical constraint.
What Would Need to Change
Config file schema (config_file.rs)
- Remove Kubernetes-only fields (
sandbox_namespace,service_account_name,enable_user_namespaces) fromGatewayFileSection; move to[openshell.drivers.kubernetes]only, updatinginheritable_keys()accordingly. - Resolve
compute_drivers: Vec<String>vs. single-active-driver reality - either narrow the type to a scalar or explicitly validate/document the "first wins" semantics. - Add serde aliases for any renamed keys to support a migration window.
Core config types (config.rs)
- Convert
0-overloaded fields toOption<T>(gateway_jwt.ttl_secs,sandbox_pids_limitin Docker/Podman,health_check_interval_secsin Podman), withNonemeaning the "special" behavior and explicit validation rejecting nonsensicalSome(0)where relevant. - Rename
GatewayInterceptorConfig.max_response_bytesto align with middleware'smax_body_bytes(or vice versa) for naming consistency.
Driver configs
- Docker: rename
sandbox_namespacetosandbox_label; adopt a shared typedImagePullPolicyenum; add proxy config fields matching Podman; addapp_armor_profile. - Podman: rename
sandbox_ssh_socket_pathtossh_socket_path. - VM: rename
openshell_endpointtogrpc_endpoint; reconcilesandbox_uiddefault with Kubernetes'1000; add proxy config fields; addapp_armor_profile. - Kubernetes: no field changes needed, but should be the source of truth for shared types/fields being generalized (typed pull policy, SPIFFE, AppArmor, proxy).
Cross-cutting
- Decide TLS field ownership: consolidate
guest_tls_ca/cert/keyto gateway-level only, removing the redundant per-driver copies (recommended, since only one driver is active and the paths are host-side). - Decide
grpc_endpoint/openshell_endpointownership: move toward gateway-computed/auto-injected endpoint rather than per-driver configuration, with an explicit override escape hatch. - Generalize SPIFFE workload socket support beyond Kubernetes (Docker/Podman via host socket bind-mount, VM via virtio-vsock).
- Update
docs/reference/gateway-config.mdxfor every renamed, moved, or retyped field (currently documents ~250+ TOML examples touching these fields). - Update Helm chart values/templates and packaged default configs (
deploy/rpm/gateway.toml.default, Homebrew default config) that reference any changed field.
Alternative Approaches Considered
- Alias lifetime: keep serde aliases for renamed fields permanently vs. a deprecation window (e.g., 2-3 releases) vs. a hard break tied to a major version. This needs a human/product decision, not an agent one.
0-overload replacement:Option<T>(idiomatic Rust, recommended) vs. sentinel constants (e.g.,u64::MAX) vs. separateenable_*boolean fields.Option<T>is recommended for clarity, but changes TOML ergonomics (must omit the key rather than write= 0).- TLS field ownership: gateway-only (recommended - single active driver, host-side paths, no case where drivers need different values) vs. keeping per-driver copies for hypothetical future multi-driver support.
grpc_endpointauto-detection: full auto-injection by the gateway vs. keeping explicit per-driver config as an opt-in override when auto-detection is insufficient (e.g., custom network topologies). A pure auto-detect-only approach risks removing power-user control drivers currently rely on.compute_driverscardinality: whether OpenShell's roadmap ever intends multi-driver support. If never, collapse to a scalar field; if it's a real future possibility, keep theVectype but tighten validation/docs now.
Patterns to Follow
- Existing driver
validate()methods (e.g., Podman'svalidate_tls_config(),validate_proxy_config()inpodman/config.rs) are the established place to add validation for newOption<T>semantics or shared enum values. - Podman's
ImagePullPolicyenum (podman/config.rs:25-57) is the best existing example of a typed pull-policy and should be promoted toopenshell-coreas the shared base type, with driver-specific extension (e.g., Podman'sNewervariant) layered on top. - Serde
#[serde(alias = "...")]is the established idiom in this codebase for backward-compatible renames.
Proposed Approach
Stage the cleanup rather than attempting all 21 issues in one PR. A natural split: (1) low-risk pure field renames with serde aliases (#6, #12, #13), (2) Option<T> conversions for 0-overloaded fields with validation (#5, #7, #10, #11), (3) feature-parity additions copying existing Kubernetes/Podman patterns to Docker/VM (#9, #15, #18, #21), and (4) architectural moves requiring the most human input (#1-3, #4, #16, #17, #19, #20 - field ownership changes, endpoint auto-detection, shared enum promotion, TLS consolidation). Each stage should update docs/reference/gateway-config.mdx, Helm chart values, and packaged default configs in the same PR, and ship serde aliases for any renamed/relocated key to avoid breaking existing deployments outright.
Scope Assessment
- Complexity: High (overall) - individual issues range from Low (pure renames) to High (field ownership moves, endpoint auto-detection)
- Confidence: Medium - the technical mapping is well understood and file:line-verified, but several design decisions (alias lifetime, TLS/endpoint ownership,
compute_driverscardinality) require human/product input before implementation - Estimated files to change: ~10-15 core files (
config.rs,config_file.rs, 4 driver config modules) plusdocs/reference/gateway-config.mdx, Helm chart values/templates, and packaged default configs (RPM, Homebrew) - Issue type:
refactor
Risks & Open Questions
- Backward compatibility is the dominant risk. OpenShell has real deployments with existing
gateway.tomlfiles (hand-written, RPM/Homebrew defaults, Helm-generated). Field renames and moves are breaking changes unless serde aliases are added; type changes (0toOption<T>) change what's valid TOML syntax for a field. - VM
sandbox_uiddefault change (10001 to 1000) is a semantic behavior change, not just a rename - could affect file ownership expectations in already-running VM sandboxes. Needs explicit call-out in release notes if changed. grpc_endpointauto-detection could remove power-user control for custom network topologies if implemented as auto-detect-only rather than auto-detect-with-override.compute_driversVec vs. scalar depends on whether multi-driver support is ever on the roadmap - a product decision, not a technical one.- Helm chart and packaged defaults (
deploy/rpm/gateway.toml.default, Homebrew config) must be updated in lockstep with any renamed/moved field, or upgrades will silently stop applying settings. - Alias retention window (how long to support old field names before removal) needs a human decision - likely tied to release/versioning policy.
Disposition Readiness
- State:
state:validated - Assessment: The investigation confirms all 21 issues against current source with exact file:line references, maps the inheritance mechanism, and surfaces the key design decisions and backward-compatibility risks needed for a human accept/decline call. No further investigation is needed to make that decision.
- Missing evidence: None for disposition. Implementation-time decisions (alias lifetime, TLS/endpoint ownership model,
compute_driverscardinality) are flagged above and should be resolved duringbuild-from-issueplanning or via explicit human direction before implementation begins.
Test Considerations
crates/openshell-server/src/config_file.rshas 30+ existing tests including driver-table inheritance tests (~lines 729-858) that will need updates for any moved/renamed inheritable keys.crates/openshell-core/src/config.rshas 40+ tests covering compute driver kind parsing, gateway JWT defaults, interceptor failure policies - default-value tests will need updates for0toOption<T>conversions.- Kubernetes driver config has 56 tests (
kubernetes/config.rs), Podman has 31 (podman/config.rs) covering TLS validation, proxy config, runtime limits - new tests needed for any fields ported to Docker/VM. - A packaged-default "contract test" exists (referenced around
config_file.rs:860) validating RPM/Homebrew default configs parse correctly - must be updated alongside any schema change. - Any staged PR should include: unit tests for new
Option<T>validation logic, alias-parsing tests (old name still works, new name works, both present is an error or new-name-wins), and updated inheritance tests for any relocated keys. docs/reference/gateway-config.mdxshould be treated as a documentation contract - every schema example in the ~1051-line file touching a changed field must be updated in the same PR as the code change.
Created by spike investigation. state:validated means the issue is ready for human disposition; state:needs-info means specific evidence is still required. A human applies state:accepted or places the issue on the roadmap if OpenShell should pursue the work. To queue unattended agent planning, a human applies agent:plan-requested; a direct request to an agent does not require that label.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
0.1.0#2565drew commentedon Aug 18, 2026
I gave this a quick skim and this all looks reasonable. Ideally we would schedule this work with the other breaking changes around Sept. 1st as part of https://github.com/NVIDIA/OpenShell/milestone/10.
jhjaggars commentedon Aug 19, 2026
Implementation Plan
Issue type:
refactorfollowed by a coordinated breaking/additive changeOverall complexity: High
Packaging: Two PRs total
Confidence: High for the compatibility PR; Medium for the coordinated cutover
Summary
Implement #2792 with an expand–migrate–contract sequence split across exactly two PRs:
Both PRs reference #2792. Only the second PR closes it.
Corrected current behavior
Code inspection corrected two assumptions from the issue text:
compute_driversdoes not use first-wins behavior. Empty selects auto-detection, a singleton selects that driver, and multiple entries are rejected at startup.max_response_bytesand middlewaremax_body_bytesdo not currently represent interchangeable limits. They must not be aliased merely because their names look similar.PR 1 — compatibility expansion (#2814)
Invariant: every previously valid configuration continues to parse, retains the same defaults, and produces the same effective runtime behavior.
Implemented scope:
namespace,service_account_name, andenable_user_namespacescanonical in[openshell.drivers.kubernetes]. Retain the gateway-level fields as lower-precedence compatibility inputs. Retain gatewaysandbox_namespacecompatibility for Docker inheritance.compute_driver = "<name>"TOML. Continue acceptingcompute_drivers = [...]and preserve empty/single/multiple behavior, the Rust vector representation, CLI/env names, and API fields.gateway_jwt.ttl_secs = 0default from canonical serialization and expose an internalOption<Duration>effective accessor. Continue accepting explicit zero and preserve token behavior.sandbox_namespacetosandbox_label; preserve the serde alias, explicit-legacy-override inheritance behavior, and persistedopenshell.sandbox_namespacelabel key.sandbox_ssh_socket_pathtossh_socket_path; preserve the serde alias and standalone CLI/environment interface.openshell_endpoint/--openshell-endpointcontract togrpc_endpoint/--grpc-endpoint; preserve serde and hidden clap aliases. Unrelated sandbox/supervisor endpoint concepts remain unchanged.Deferred because they are not genuinely behavior-neutral:
Option<T>conversions and rejection of explicit zero for PID/health fields (Security feedback: SSH hardening and policy validation #7, Example: Run common agent harnesses #10, Example: Run common tools from CLI tracker #11).PR 2 — coordinated breaking cutover and driver parity
Land all remaining intentional behavior changes in one PR:
Option<T>/sentinel removal for bug(sandbox): arm64 remote cluster fails on x86_64 host without DOCKER_PLATFORM and missing default policy file #5, Security feedback: SSH hardening and policy validation #7, Example: Run common agent harnesses #10, and Example: Run common tools from CLI tracker #11, including explicit validation and migration notes.sandbox_uidbehavior (Example: Bootstrap Navigator development #14).Test plan
PR 1 contract coverage includes:
PR 2 adds explicit migration/error tests for removed forms, strict optional-value and image-pull validation, endpoint/TLS resolution matrices, VM UID behavior, and per-driver proxy/SPIFFE/AppArmor E2E coverage.
Risks
Documentation impact
Each PR updates
docs/reference/gateway-config.mdx,docs/reference/sandbox-compute-drivers.mdx, relevant architecture/driver docs, deployment producers, examples, and the cluster-debugging skill. PR 1 documents compatibility forms but emits canonical forms only; PR 2 documents the final schema and migration errors.Revision 3 — consolidate the accepted two-PR expand/migrate/contract plan and record corrected runtime semantics
Revision 2 — align the initial stage with the verified rename set
Revision 1 — initial staged implementation plan
github-actions commentedon Sep 4, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.