Skip to content

feat(settings): gateway-to-sandbox runtime settings channel#474

Merged
johntmyers merged 28 commits intomainfrom
codex/feat-405-sandbox-settings-channel
Mar 20, 2026
Merged

feat(settings): gateway-to-sandbox runtime settings channel#474
johntmyers merged 28 commits intomainfrom
codex/feat-405-sandbox-settings-channel

Conversation

@johntmyers
Copy link
Collaborator

@johntmyers johntmyers commented Mar 19, 2026

Summary

Adds a general-purpose settings channel between the gateway and sandboxes, enabling runtime configuration changes without sandbox restarts. This separates the settings transport concern from issue #393 (OCSF logging) so it can land independently.

Related Issue

Closes #405

UX Changes

CLI

New commands:

  • openshell settings get [sandbox] [--global] [--json] -- show effective settings with scope indicators (sandbox/global/unset)
  • openshell settings set [sandbox] --key K --value V [--global] [--yes] -- set a setting at sandbox or global scope
  • openshell settings delete [sandbox] --key K [--global] [--yes] -- delete a setting (sandbox-scoped when not globally managed, or global)
  • openshell policy list --global -- list global policy revision history
  • openshell policy get --global [--rev N] [--full] -- show a specific global policy revision

Changed commands:

  • openshell policy set --global now creates versioned revisions (deduped by hash) instead of overwriting in-place
  • openshell policy set --global --wait is rejected with a message ("global policies are effective immediately")
  • All --global mutations require HITL confirmation (bypass with --yes)

Scope resolution:

  • Two-tier precedence: global overrides sandbox. A globally-managed key blocks sandbox set/delete with FailedPrecondition.
  • Deleting a global setting restores sandbox-level control.

TUI

Dashboard:

  • Middle pane now has tabs: Providers | Global Settings (switch with h/l)
  • Global Settings tab: list all registered keys, edit with type-aware input (bool toggle, string/int text), HITL confirmation modals for set and delete
  • Gateway row shows Global Policy Active (v3) in yellow when a global policy is set

Sandbox screen:

  • Bottom pane now has tabs: Policy | Settings (switch with h/l)
  • Settings tab: shows effective settings with scope column (sandbox/global/unset), edit/delete with HITL
  • Globally-managed keys show status bar message when edit/delete attempted
  • Metadata pane shows Policy: managed globally (v3) when sandbox policy is globally overridden
  • All sandbox settings and policy source auto-refresh on tick (no need to exit/re-enter)

Sandbox Logs

  • Poll loop messages changed from Policy poll: to Settings poll:
  • Individual setting changes logged: Setting changed key=log_level new=debug old=<unset>
  • Policy reloaded successfully only appears when the policy hash actually changed (not for settings-only changes)
  • Global policy reloads include global_version=N in the log

Architecture (tl;dr)

Proto changes (sandbox.proto, openshell.proto)

New types: SettingValue (oneof string/bool/int64/bytes), EffectiveSetting (value + scope), SettingScope, PolicySource.

New RPCs:

  • GetSandboxSettings -- returns effective policy + merged settings + config_revision + global_policy_version
  • GetGatewaySettings -- returns global-only settings

Renamed: UpdateSandboxPolicy -> UpdateSettings -- now handles policy and setting mutations at both scopes through field-based dispatch (global, setting_key, setting_value, delete_setting).

Extended: GetSandboxPolicyStatus and ListSandboxPolicies gained a global bool to query global policy revisions.

Settings storage

Settings are stored as JSON blobs in the existing objects table with gateway_settings and sandbox_settings object types. Sandbox settings use a prefixed ID (settings:{sandbox_uuid}) to avoid PK collision with sandbox objects. A tokio::sync::Mutex serializes all settings mutations to prevent read-modify-write races.

Global policy versioning

Global policies are now versioned in the sandbox_policies table using a sentinel sandbox_id of __global__. Revisions are marked loaded immediately (no sandbox confirmation for global policies). The existing get_latest_policy, list_policies, and supersede_older_policies Store methods work unchanged with the sentinel.

Settings registry (openshell-core/src/settings.rs)

Compile-time REGISTERED_SETTINGS array defines allowed keys with typed SettingValueKind. Test keys (dummy_bool, dummy_int) gated behind dev-settings feature flag (on by default). The doc comment on the registry documents the process for adding new settings.

Config revision

config_revision is a 64-bit content hash (first 8 bytes of SHA-256 over policy + settings + source). The sandbox poll loop compares it to detect changes. Not a monotonic counter -- uses wrapping_add for the settings revision counter.

TUI Dashboard with a global policy set:

image

Sandbox view with a global policy and one global setting override:

image

Changes

  • Proto: New settings types, GetSandboxSettings/GetGatewaySettings RPCs, UpdateSettings rename, global flag on policy status/list RPCs, global_policy_version field
  • Server: Settings persistence, merge resolution, per-key mutual exclusion, global policy versioning, settings cleanup on sandbox delete, concurrency mutex
  • Sandbox: Poll loop refactor (settings diff, conditional OPA reload, global version logging)
  • CLI: settings get/set/delete, policy list/get --global, --json output, --wait rejection for global
  • TUI: Global settings tab, sandbox settings tab, dashboard global policy indicator, auto-refresh, HITL modals, scope indicators
  • Core: Settings registry with feature-gated test keys, parse_bool_like, display_setting_value
  • E2E: settings_management.rs covering full lifecycle (sandbox set/delete, global override, lock/unlock)
  • Unit tests: 35 new tests across openshell-core and openshell-server (merge algorithm, conflict guard, delete-unlock, concurrency, round-trips)
  • Docs: Architecture docs for settings subsystem, updated gateway/sandbox/TUI/security-policy docs

Testing

  • mise run pre-commit passes
  • Unit tests added/updated (35 new tests, all passing)
  • E2E tests added/updated (settings_management.rs)
  • Manual CLI smoke testing (all settings/policy flows)
  • Manual TUI testing (both tabs, edit/delete/HITL, auto-refresh, global policy indicator)

Checklist

  • Follows Conventional Commits
  • Architecture docs updated
  • User-facing docs deferred (no real settings to document yet -- dummy keys are dev-only)

@johntmyers johntmyers requested a review from a team as a code owner March 19, 2026 15:37
@johntmyers johntmyers self-assigned this Mar 19, 2026
@github-actions
Copy link

github-actions bot commented Mar 19, 2026

PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-03-20 21:09 UTC

@johntmyers johntmyers added the test:e2e Requires end-to-end coverage label Mar 19, 2026
@johntmyers johntmyers requested review from drew and pimlock March 19, 2026 21:19
@johntmyers johntmyers force-pushed the codex/feat-405-sandbox-settings-channel branch from 4d4e3ce to 85fef54 Compare March 19, 2026 21:24
@pimlock
Copy link
Collaborator

pimlock commented Mar 19, 2026

Few questions after first pass

  • in the context of multiple sandboxes running and global settings/policies - the global policy always takes precedence, so there is no way to have a sandbox that have more/less restrictive policy than the global one.
  • how do the policy update chunks fit in here? If my policy is global and my sandbox cannot connect to something, would the approve flow add that to the global? Or is that flow not supported when global policy is active?
  • most of the code is already set up to support different setting/policy scopes, is the idea to potentially have scopes like user, group, team, etc? I think the --global makes sense (alternative would be --scope global, but without more examples that's probably unnecessary and more can be added later). I would expect the only place for the global:bool would be the CLI and it would be later passes as some kind of enum.

@johntmyers
Copy link
Collaborator Author

  • how do the policy update chunks fit in here? If my policy is global and my sandbox cannot connect to something, would the approve flow add that to the global? Or is that flow not supported when global policy is active?

Right now they would succeed in updating the sandbox policy, but have no effect until the global policy was taken down. This is def a UX bug. Good call out. My thinking is we block chunk approval while a global policy is active. This isn't a one-way door and is the safest thing to do.

Other options:

  1. Allow updating global - IMO too dangerous
  2. allow these chunks to amend a single sandbox in addition the global policy - too complex no signal we need it.

So I think we just disallow approving chunks when a global policy is active.

@johntmyers
Copy link
Collaborator Author

Okay we block rule acceptance when a global policy is set. I think we can go with this for now.

$ echo "=== Try approve-all (should FAIL) ===" && openshell rule approve-all musical-fantail 2>&1
=== Try approve-all (should FAIL) ===
Error:   × status: FailedPrecondition, message: "cannot approve rules while a global
  │ policy is active; delete the global policy to manage per-sandbox rules",
  │ details: [], metadata: MetadataMap { headers: {"content-type":
  │ "application/grpc", "date": "Fri, 20 Mar 2026 15:49:28 GMT"} }

@johntmyers johntmyers closed this Mar 20, 2026
@johntmyers johntmyers reopened this Mar 20, 2026
Closes #405

Refactor the sandbox policy polling channel into an effective settings response with config revision tracking, global policy source metadata, and merged global/sandbox key resolution.

Add gateway-global and sandbox-scoped setting mutations with per-key mutual exclusion, global delete unlock semantics, and global policy override behavior.

Extend the CLI with settings get/set/delete and --global policy flows, then document the new control-plane behavior in architecture and user docs.

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
@johntmyers johntmyers force-pushed the codex/feat-405-sandbox-settings-channel branch from 00c9461 to 1998927 Compare March 20, 2026 19:22
@johntmyers johntmyers merged commit a831a89 into main Mar 20, 2026
10 checks passed
@johntmyers johntmyers deleted the codex/feat-405-sandbox-settings-channel branch March 20, 2026 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:e2e Requires end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(gateway/sandbox): support arbitrary gateway-to-sandbox runtime settings over SandboxConfig

2 participants