Skip to content

feat: read-only web dashboard (/ui) for developers and DevOps engineers #2192

Description

@iamroseyoung

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:gatewayGateway server and control-plane workspiketopic:observabilityLogging, metrics, and observability work

    Fields

    No fields configured for Enhancement.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions