Problem Statement
OpenShell currently exposes two operator surfaces: the CLI and the ratatui TUI. Both work well for hands-on-keyboard workflows, but several recurring situations are poorly served:
- Policy iteration is log-archaeology. The core loop (watch denials → understand why → allow → retry) requires juggling
openshell logs --tail, policy get, YAML edits, and policy set across terminals. The "why" of a denial (binary mismatch, resolved symlink, policy rule) is buried in log text.
- Failure causes are hidden. The OCSF shorthand formatter suppresses the
message field when a destination endpoint is present, so an operator sees NET:FAIL [LOW] pypi.org:443 with no reason. In a real debugging session this cost us a full day chasing an IPv6 hypothesis when the actual cause was an upstream TLS verification failure.
- Sandbox health is opaque. A sandbox stuck in a supervisor crash-loop (e.g. expired static token after a container restart) shows only "Provisioning" — restart counts and supervisor state are not surfaced anywhere.
- No glanceable fleet view for DevOps engineers operating a gateway: phase, policy version, revision history, and live activity require per-sandbox CLI invocations.
Proposed Design
A read-only web dashboard embedded in the gateway binary, as the first slice of a larger web UI direction (full plan drafted separately; this issue covers only the read-only observability slice).
Backend (openshell-server):
- New
web_ui module serving a single-file SPA at /ui plus a small JSON API under /ui/api/*:
GET /ui/api/overview — gateway version, sandbox counts
GET /ui/api/sandboxes — fleet list (phase, image, created, active policy version, conditions)
GET /ui/api/sandboxes/{name} — detail incl. policy revision history and current policy YAML
GET /ui/api/sandboxes/{name}/logs — initial log tail
GET /ui/api/sandboxes/{name}/stream — SSE live stream bridged from the in-memory TracingLogBus
- Reuses existing gRPC handler logic in-process (no duplicated data access), keeping the JSON shapes as a thin, stable adapter layer.
- Gated off by default: the entire surface only mounts when
OPENSHELL_WEB_UI=1 is set. These routes intentionally bypass gRPC bearer auth, so the gate keeps them dev/local-only until proper authn/z (OIDC session or token) is added in a follow-up.
Frontend (embedded assets/ui.html, no build step, no external dependencies):
- Left rail: gateway status + sandbox list with phase indicators, keyboard navigation (j/k, mirroring the TUI).
- Live tab: a "decision board" that renders network policy decisions parsed from OCSF events as flight-strip rows (ALLOW in green, DENY in amber with the full denial reason available), plus a filterable, auto-following log tail.
- Policy tab: current policy YAML and the revision table (status, hash, load errors, active version).
Non-goals for this slice: any mutating action (policy edit, sandbox lifecycle), multi-gateway views, web terminal, persistent event storage. Those are follow-ups per the broader plan.
Alternatives Considered
- tonic-web/gRPC-web + generated TS client + React build pipeline — the right long-term target (streaming RPCs already exist), but it introduces a frontend toolchain and codegen pipeline that is disproportionate for a first read-only slice. The JSON adapter is deliberately small and can be replaced by gRPC-web without UI redesign.
- Standalone BFF process talking gRPC to the gateway — avoids touching the server crate, but adds a second deployable and a second auth boundary; embedding keeps single-binary distribution.
- Extending the TUI instead — the TUI already covers hands-on workflows; the gap is shareable, glanceable observability (a browser tab on a second monitor / TV dashboard), which a terminal UI cannot serve.
Agent Investigation
Feasibility was validated by implementing a working prototype against a live gateway (Docker driver) running a real agent workload (Discord bot in a policy-constrained sandbox):
- All five endpoints serve live data by delegating to
grpc::sandbox::{handle_get_sandbox, handle_list_sandboxes} and grpc::policy::{handle_get_sandbox_policy_status, handle_list_sandbox_policies, handle_get_sandbox_logs} (visibility widened to pub; the grpc module itself remains crate-private).
- SSE bridging from
TracingLogBus::subscribe works end-to-end: a policy denial triggered inside the sandbox appeared on the board in real time, including the full denial reason (binary-not-allowed with ancestor chain and symlink hint) that the shorthand log format suppresses.
cargo clippy --workspace --all-targets -- -D warnings and cargo fmt clean; no new dependencies added to the workspace.
Problem Statement
OpenShell currently exposes two operator surfaces: the CLI and the ratatui TUI. Both work well for hands-on-keyboard workflows, but several recurring situations are poorly served:
openshell logs --tail,policy get, YAML edits, andpolicy setacross terminals. The "why" of a denial (binary mismatch, resolved symlink, policy rule) is buried in log text.messagefield when a destination endpoint is present, so an operator seesNET:FAIL [LOW] pypi.org:443with no reason. In a real debugging session this cost us a full day chasing an IPv6 hypothesis when the actual cause was an upstream TLS verification failure.Proposed Design
A read-only web dashboard embedded in the gateway binary, as the first slice of a larger web UI direction (full plan drafted separately; this issue covers only the read-only observability slice).
Backend (
openshell-server):web_uimodule serving a single-file SPA at/uiplus a small JSON API under/ui/api/*:GET /ui/api/overview— gateway version, sandbox countsGET /ui/api/sandboxes— fleet list (phase, image, created, active policy version, conditions)GET /ui/api/sandboxes/{name}— detail incl. policy revision history and current policy YAMLGET /ui/api/sandboxes/{name}/logs— initial log tailGET /ui/api/sandboxes/{name}/stream— SSE live stream bridged from the in-memoryTracingLogBusOPENSHELL_WEB_UI=1is set. These routes intentionally bypass gRPC bearer auth, so the gate keeps them dev/local-only until proper authn/z (OIDC session or token) is added in a follow-up.Frontend (embedded
assets/ui.html, no build step, no external dependencies):Non-goals for this slice: any mutating action (policy edit, sandbox lifecycle), multi-gateway views, web terminal, persistent event storage. Those are follow-ups per the broader plan.
Alternatives Considered
Agent Investigation
Feasibility was validated by implementing a working prototype against a live gateway (Docker driver) running a real agent workload (Discord bot in a policy-constrained sandbox):
grpc::sandbox::{handle_get_sandbox, handle_list_sandboxes}andgrpc::policy::{handle_get_sandbox_policy_status, handle_list_sandbox_policies, handle_get_sandbox_logs}(visibility widened topub; thegrpcmodule itself remains crate-private).TracingLogBus::subscribeworks end-to-end: a policy denial triggered inside the sandbox appeared on the board in real time, including the full denial reason (binary-not-allowed with ancestor chain and symlink hint) that the shorthand log format suppresses.cargo clippy --workspace --all-targets -- -D warningsandcargo fmtclean; no new dependencies added to the workspace.