Skip to content

refactor(supervisor): treat sandbox workspace as immutable startup context #2853

Description

@pimlock

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.local proposal submission and lookup return 503 workspace_unavailable.
  • Denial and activity aggregators defer flushing until the workspace becomes non-empty.
  • PR feat(supervisor): expose sandbox name to middleware request context #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

  1. load_policy() fetches a SettingsPollResult containing snapshot.workspace.
  2. The workspace is used for policy synchronization but omitted from the return value.
  3. run_sandbox() creates workspace_tx/workspace_rx with an empty string.
  4. Networking, policy.local, and the aggregators receive clones of workspace_rx.
  5. The background policy poll performs another config request and publishes client.workspace().
  6. 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_id in crates/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 feat(supervisor): expose sandbox name to middleware request context #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 SandboxId newtype 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 feat(supervisor): expose sandbox name to middleware request context #2771 merges, use the same immutable value for HTTP and WebSocket middleware request contexts.
  • Pass validated sandbox_id, display-only sandbox_name, and immutable workspace to middleware through a neutral runtime context or explicit fields; do not use openshell_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_tx from PolicyPollLoopContext, 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_id is required and authoritative for operator-run middleware; sandbox_name and 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_id or sandbox_name from 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.local does not return workspace_unavailable merely 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 feat(supervisor): expose sandbox name to middleware request context #2771, and unavailable-workspace mode.

Related

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions