refactor(supervisor): treat sandbox workspace as immutable startup context
@pimlock is already working on this.
Since Aug 20, 2026.
- Dominant language
- Rust
- Stars
- 8.7k
- Forks
- 1.3k
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 253
Description
Description
Treat a sandbox's workspace as immutable startup context instead of mutable state distributed through a tokio::sync::watch channel.
A sandbox cannot move between workspaces during its lifetime. In gateway mode, the supervisor already receives the canonical workspace in the initial GetSandboxConfig / SettingsPollResult response while loading policy. The current implementation discards that initial value, creates an empty workspace channel, and republishes the same workspace from the background policy poll.
Context
The workspace channel was introduced in #2243 and currently begins empty in crates/openshell-sandbox/src/lib.rs:
let (workspace_tx, workspace_rx) =
tokio::sync::watch::channel(String::new());
The initial policy load already has snapshot.workspace, but load_policy() does not return it to run_sandbox(). The policy poll loop later calls workspace_tx.send(client.workspace()) after another successful settings poll.
Existing consumers compensate for the initially-empty state:
policy.localproposal submission and lookup return503 workspace_unavailable.- Denial and activity aggregators defer flushing until the workspace becomes non-empty.
- PR #2771 adds the workspace to supervisor middleware request context and reads it through the same channel. This can make early middleware requests omit a workspace that the initial configuration load already knew.
PR #2771 should not be blocked on this refactor. Its workspace remains display/logging metadata and is explicitly best-effort. This issue is the fast-follow that removes the unnecessary late-discovery model.
Current data flow
load_policy()fetches aSettingsPollResultcontainingsnapshot.workspace.- The workspace is used for policy synchronization but omitted from the return value.
run_sandbox()createsworkspace_tx/workspace_rxwith an empty string.- Networking,
policy.local, and the aggregators receive clones ofworkspace_rx. - The background policy poll performs another config request and publishes
client.workspace(). - Consumers become workspace-aware only after that later poll succeeds.
Pre-existing OCSF coupling
Supervisor middleware already sourced sandbox identity from the process-global OCSF context before #2771:
- HTTP middleware request construction reads
openshell_ocsf::ctx::ctx().sandbox_idincrates/openshell-supervisor-network/src/l7/middleware.rs. This dates to the original HTTP middleware implementation (d55674877). - WebSocket preflight construction reads the same OCSF field in
crates/openshell-supervisor-network/src/l7/relay.rs. This predates #2771 (44bf0df48).
PR #2771 does not introduce the sandbox_id dependency on OCSF. It expands the existing pattern by also sourcing sandbox_name from openshell_ocsf::SandboxContext, while sourcing workspace separately from the workspace channel.
OCSF is an observability subsystem, not the authoritative middleware identity boundary. Its SandboxContext is a process-global singleton initialized early for logging and has an empty fallback for tests. Middleware request construction should instead receive validated sandbox identity and immutable workspace through a neutral supervisor/runtime context (or explicit arguments). OCSF may consume the same authoritative startup values, but middleware identity should not be recovered from OCSF metadata.
Sandbox ID invariant by mode
Operator-run middleware is available only in gateway mode, while built-in middleware can also run in local policy-file mode. Resolve a non-empty runtime sandbox ID once, before OCSF, networking, or middleware starts:
- Gateway mode: require the configured gateway sandbox ID to be present and non-empty. Missing or whitespace-only values are startup/configuration errors; fail before accepting workload traffic.
- File mode with an explicit ID: validate and use the supplied ID.
- File mode without an ID: generate a unique invocation-scoped ID, preferably a UUID with a clear local prefix such as
local-<uuid>.
The generated file-mode ID is stable only for that supervisor invocation. It identifies one local sandbox runtime for correlation and built-in middleware context; it is not a durable gateway identity and must not be reused across launches.
Represent the resolved value with a non-empty type or validated constructor so downstream code cannot construct middleware or OCSF context with an empty ID. This removes the need for normal per-request missing-ID handling. A defensive middleware-boundary validation may remain, but reaching it indicates an internal invariant violation.
Implementation Notes
- Carry the initial workspace out of gateway policy loading before networking starts. Consider replacing the growing
load_policy()tuple with a named result type while making this change. - Represent workspace availability explicitly, preferably with
Option<String>or an equivalent immutable type rather than using an empty string as a sentinel. - Resolve and validate the runtime sandbox ID at the mode boundary before initializing OCSF or networking. Do not silently generate an ID when gateway mode was requested but its ID is missing.
- Consider a
SandboxIdnewtype or equivalent validated constructor that rejects empty and whitespace-only values. - Generate a fresh invocation-scoped ID for file mode only when no explicit ID is supplied.
- Pass the immutable workspace to networking,
PolicyLocalContext, and the denial/activity aggregation setup. - After #2771 merges, use the same immutable value for HTTP and WebSocket middleware request contexts.
- Pass validated
sandbox_id, display-onlysandbox_name, and immutable workspace to middleware through a neutral runtime context or explicit fields; do not useopenshell_ocsf::ctx::ctx()as the source of middleware identity. - Keep the OCSF context focused on event construction. Populate it from the same resolved runtime identity, but do not make it own or supply the middleware contract.
- Remove
workspace_txfromPolicyPollLoopContext, its sends in the poll loop, and the corresponding watch receivers where they are no longer needed. - Preserve safe behavior when no workspace exists, including local policy-file mode and any compatible sidecar topology. These modes should represent workspace as unavailable without pretending it may arrive later.
- Keep middleware identity rules unchanged:
sandbox_idis required and authoritative for operator-run middleware;sandbox_nameand workspace are display/logging metadata only.
Definition of Done
- A gateway-connected supervisor makes the initial workspace available to all consumers before networking accepts workload traffic.
- The first HTTP or WebSocket middleware request after startup carries the workspace returned by the initial sandbox configuration.
- Gateway mode rejects a missing, empty, or whitespace-only sandbox ID before networking accepts workload traffic.
- File mode validates an explicitly supplied sandbox ID and generates a unique invocation-scoped ID when none is supplied.
- Every running supervisor has a non-empty runtime sandbox ID before OCSF, networking, or middleware initialization.
- HTTP and WebSocket middleware request contexts no longer source
sandbox_idorsandbox_namefrom the process-global OCSF context. - Operator-run middleware request construction receives the validated gateway sandbox ID from the authoritative supervisor startup context.
- OCSF and built-in middleware receive the generated invocation-scoped ID in file mode.
-
policy.localdoes not returnworkspace_unavailablemerely because a redundant second settings poll has not completed. - Denial and activity aggregation do not defer flushing merely because a redundant second settings poll has not completed.
- Workspace is modeled as immutable for the lifetime of the supervisor.
- The workspace watch channel and policy-poll workspace updates are removed.
- Local-file and applicable sidecar modes continue to handle an unavailable workspace safely.
- Tests cover gateway-mode ID rejection, file-mode ID generation, initial gateway workspace propagation, middleware identity/workspace propagation after #2771, and unavailable-workspace mode.
Related
- #2243 introduced the current workspace watch-channel plumbing.
- #2771 consumes workspace and sandbox display name for supervisor middleware request context.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.