Skip to content

feat(cli): optional OpenShell sandbox backend for the bridge daemon - #296

Merged
duyet merged 1 commit into
mainfrom
feat/cli-openshell-sandbox-backend
Aug 2, 2026
Merged

feat(cli): optional OpenShell sandbox backend for the bridge daemon#296
duyet merged 1 commit into
mainfrom
feat/cli-openshell-sandbox-backend

Conversation

@duyet

@duyet duyet commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Track A of the OpenShell CLI/k8s plan. Lets oma bridge daemon execute relayed sandbox ops inside an OpenShell sandbox on the user's own machine instead of raw host subprocess. Charts (Track B) are a separate PR.

Security posture — the actual trade-off

subprocess (default, unchanged) openshell (new, opt-in)
Isolation none — /bin/sh on the host FS container, gateway-enforced egress
What the agent sees the user's real repos, toolchains, gh/cloud auth an empty box

Neither is strictly better. Isolation is exactly what makes the box useless for the workflow most people paired their machine for. So the user picks.

Why explicit opt-in, with no auto-detect

resolveBridgeBackendKind (apps/k8s-bridge) flips to OpenShell as soon as OPENSHELL_GATEWAY_ENDPOINT is present. That's safe for a dedicated in-cluster deployment and unsafe on a laptop: someone who installed OpenShell for unrelated reasons would silently lose every agent's view of their files. So selection here is daemon settings → BRIDGE_SANDBOX_BACKEND → subprocess, and nothing else. bridge setup offers the switch when it probes a live gateway, states the trade-off in the prompt, and persists whichever way the user answers so it never re-asks. The divergence is called out in a code comment and covered by a test.

What changed

  • bridge-sandbox.ts — extracted a RelaySandboxExecutor seam carrying all 8 relay ops (exec / readFile / readFileBytes / writeFile / writeFileBytes / setEnvVars / ping / destroy). Deliberately not the k8s-bridge's 5-op BoxExecutor, which lacks the bytes ops and would silently break binary file relay. Subprocess behavior and BridgeSandboxManager's public surface are unchanged; bridge-sandbox.test.ts passes untouched.
  • openshell-client.ts — vendored lean gRPC client + gateway probe. The CLI publishes a single zero-runtime-dependency esbuild bundle and the internal @duyet/oma-* packages are private, so importing the real adapter is impossible; it also pulls observability at module scope. @grpc/* are devDeps (bundled; 1.0MB → 2.1MB). A test asserts the proto string stays byte-identical to packages/sandbox/src/adapters/openshell.ts — field-number drift is the failure mode that would silently corrupt the wire format.
  • No /workspace path rewriting on the openshell backend (/workspace is a real container path there), and exec output passes through verbatim — the exit=N shape is what boxrun/k8s-bridge consumers already parse.
  • Every exec passes an explicit ms timeout; the adapter's ?? 600 default is 600ms, an upstream typo. Relay timeout, else 120s.
  • destroyAll() now awaits the destroys (OpenShell's is an async DeleteSandbox) bounded at 10s, so a hung gateway can't wedge shutdown past launchd's ExitTimeOut. Boxes survive a WS reconnect.
  • bridge status shows the active backend and, for openshell, endpoint + reachability — "configured" and "reachable" are different claims.
  • Daemon logs the resolved backend + reason once at startup.

Stated limitations

  • No OMA policy mapping. The relay carries no environment config (there is no createBox op), so mapEnvironmentConfigToOpenShellPolicy has nothing to feed it. Egress is the gateway's own default policy.
  • Daemon crash leaks boxes on the gateway. Clean shutdown destroys them.
  • ACP agents still spawn on the host either way — this changes only sandbox-op execution. Said in the setup prompt, README, and code.
  • No memory-store / session-outputs mounts (shared with boxrun and k8s-remote).

Verification

  • pnpm typecheck — clean.
  • pnpm --filter @getoma/cli test — 8 files, 62 tests passed. pnpm test:packages — all 10 packages green. pnpm test:console — 29 files / 177 tests green.
  • Root Workers-pool suite: the only failures are 30s-timeout flakes under full-suite load on this machine (a different set each run, all in apps/agent integration tests, which this PR does not touch); they pass when run in isolation.
  • pnpm --filter @getoma/cli build succeeds and the built CLI starts.
  • Changeset included (patch).

Summary by Sourcery

Introduce an optional OpenShell-backed sandbox substrate for the bridge daemon, with explicit user opt-in and surfaced backend selection, while keeping subprocess as the default.

New Features:

  • Add an OpenShell gRPC client and backend implementation so relayed sandbox ops can run inside an OpenShell sandbox instead of the host subprocess.
  • Expose sandbox backend selection in the bridge daemon, including configuration via daemon settings or BRIDGE_SANDBOX_BACKEND and visibility in oma bridge status.
  • Extend oma bridge setup to detect a local OpenShell gateway and offer a one-time choice of sandbox backend, persisting the decision in a new daemon settings file.

Enhancements:

  • Refactor bridge sandbox management behind a generic relay executor interface that supports all relay operations and allows pluggable backends.
  • Improve daemon shutdown behavior by awaiting sandbox destruction with a bounded timeout so remote boxes are cleaned up without blocking exit.
  • Document where relayed sandbox operations run, the OpenShell option, and related environment variables in the CLI README.

Build:

  • Add gRPC-related devDependencies required for the bundled OpenShell client.

Tests:

  • Add tests covering backend selection precedence and explicit opt-in semantics for the sandbox backend.
  • Add tests ensuring the vendored OpenShell proto stays byte-identical to the source adapter and that the bridge sandbox correctly drives a non-subprocess backend executor, including byte/base64 handling and timeout behavior.

Relayed sandbox ops (`sandbox_provider: "subprocess"`) can now execute
inside an OpenShell sandbox on the paired machine instead of `child_process`
on the host. Same relay protocol, isolated execution, gateway-enforced
egress — but an empty box: none of the user's repos, toolchains, or CLI
auth are visible. So it is explicit opt-in only, via `oma bridge setup`
(offered when a gateway is detected) or `BRIDGE_SANDBOX_BACKEND=openshell`.
Deliberately no "endpoint present → use it" auto-detect, unlike the
in-cluster k8s-bridge: silently flipping a laptop would break every agent
that relies on the host.

- extract a RelaySandboxExecutor seam with all 8 relay ops; subprocess
  stays the default and its behavior is unchanged
- vendor a lean OpenShell gRPC client (the CLI ships a zero-runtime-dep
  esbuild bundle and can't resolve the private @duyet/oma-sandbox), with a
  test asserting the proto stays byte-identical to the adapter's
- destroyAll now awaits box teardown under a bounded timeout
- `oma bridge status` reports the active backend + gateway reachability
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@sourcery-ai

sourcery-ai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduce an optional OpenShell-based sandbox backend for the bridge daemon, refactoring sandbox execution behind a pluggable executor interface, adding a vendored OpenShell gRPC client, explicit backend selection semantics, user prompts and settings persistence, daemon/status wiring, and tests/docs to cover the behavior and security trade-offs.

Sequence diagram for sandbox backend selection and OpenShell opt-in

sequenceDiagram
  actor User
  participant SetupCmd as bridge_setup
  participant Config as config_settings
  participant Daemon as bridge_daemon
  participant BackendSel as resolveSandboxBackend
  participant SandboxMgr as BridgeSandboxManager
  participant OSClient as OpenShellClient

  User->>SetupCmd: runSetupInner
  SetupCmd->>Config: readSettings
  SetupCmd->>SetupCmd: probeOpenShellGateway
  SetupCmd->>User: offerOpenShellBackend prompt
  User-->>SetupCmd: choose subprocess or openshell
  SetupCmd->>Config: writeSettings(sandboxBackend, openshellEndpoint)

  User->>Daemon: runDaemon
  Daemon->>Config: readSettings
  Daemon->>BackendSel: resolveSandboxBackend(env, settings)
  BackendSel-->>Daemon: { kind, endpoint, reason }
  Daemon->>SandboxMgr: new BridgeSandboxManager(backend?)
  alt kind == openshell
    Daemon->>OSClient: createOpenShellBackend(endpoint, token, image, tls)
  else kind == subprocess
    Daemon->>SandboxMgr: createSubprocessBackend
  end

  Daemon->>Daemon: log sandbox backend + reason
  Daemon->>SandboxMgr: handle(exec/readFile/...) using selected backend
Loading

File-Level Changes

Change Details Files
Refactor bridge sandbox execution behind a pluggable RelaySandboxBackend/RelaySandboxExecutor with a default subprocess implementation and improved teardown semantics.
  • Introduce RelaySandboxExecutor and RelaySandboxBackend interfaces representing all 8 relay ops and per-session executor creation.
  • Implement SubprocessSandbox as the default backend, encapsulating workdir, env vars, path resolution, and exec/file operations.
  • Change BridgeSandboxManager to depend on a backend instead of inline subprocess logic, expose backendKind, and keep boxes across WS reconnects.
  • Convert destroyAll to async, awaiting executor destroy calls under a bounded timeout to avoid hung shutdowns.
  • Ensure exec always receives an explicit timeout and keep binary file ops wired via base64 at the relay boundary.
packages/cli/src/bridge/lib/bridge-sandbox.ts
packages/cli/src/bridge/lib/bridge-sandbox-openshell.test.ts
Add an OpenShell sandbox backend via a vendored gRPC client and backend resolver with explicit opt-in semantics and TLS/env handling.
  • Implement OpenShellClient as a vendored copy of the sandbox adapter, including minimal proto, lazy sandbox creation, exec-based file I/O, env var management, ping/destroy, and TLS channel setup.
  • Provide createOpenShellBackend factory that creates one OpenShellClient per session and preserves verbatim paths and exec output shape.
  • Add probeOpenShellGateway and resolveOpenShellTlsFromEnv helpers for reachability checks and TLS config derived from environment.
  • Ensure the vendored proto string stays byte-identical to the upstream adapter via an agreement test.
packages/cli/src/bridge/lib/openshell-client.ts
packages/cli/src/bridge/lib/openshell-client.test.ts
Introduce daemon settings and sandbox backend resolution logic, and wire them into setup, daemon startup, and status reporting with clear user-facing behavior.
  • Define DaemonSettings and readSettings/writeSettings helpers storing non-secret daemon behavior in settings.json next to credentials.
  • Implement resolveSandboxBackend to select subprocess vs openshell based on persisted settings and BRIDGE_SANDBOX_BACKEND, explicitly avoiding endpoint-based auto-detect.
  • Use resolveSandboxBackend in runDaemon to instantiate BridgeSandboxManager with either subprocess or OpenShell backend, and log the chosen backend plus reason.
  • Extend bridge status to show the active sandbox backend, and for openshell also display endpoint reachability.
  • Add sandbox-backend tests to validate selection precedence and lack of auto-detect.
packages/cli/src/bridge/lib/config.ts
packages/cli/src/bridge/lib/sandbox-backend.ts
packages/cli/src/bridge/lib/sandbox-backend.test.ts
packages/cli/src/bridge/commands/daemon.ts
packages/cli/src/bridge/commands/status.ts
Add interactive setup-time offer for the OpenShell backend and persist the user’s choice, integrating with service install/refresh flows.
  • Introduce offerOpenShellBackend to detect a reachable OpenShell gateway on TTY setups, prompt once for backend choice, and persist sandboxBackend/openshellEndpoint in settings.json.
  • Call offerOpenShellBackend from installServiceOrFallback and refreshServiceOrFallback so the choice is made before daemon/service setup or refresh.
  • Use @inquirer/select for the interactive choice, with descriptions that spell out the isolation vs usability trade-off, and ensure non-TTY flows keep the default.
  • Log the chosen backend and settings file path plus a hint that ACP agents still spawn on the host.
packages/cli/src/bridge/commands/setup.ts
Update user-facing documentation, dependencies, and changeset to reflect the new OpenShell backend feature and configuration options.
  • Extend README with a section explaining where relayed sandbox ops run, the OpenShell option, its limitations, and how to configure it via setup or env vars.
  • Document new environment variables for backend selection and OpenShell gateway/TLS configuration.
  • Add @grpc/grpc-js and @grpc/proto-loader as devDependencies for the vendored client.
  • Add a changeset describing the optional OpenShell sandbox backend and the new bridge status reporting.
  • Ensure pnpm-lock.yaml is updated to reflect dependency additions.
packages/cli/README.md
packages/cli/package.json
pnpm-lock.yaml
.changeset/cli-openshell-sandbox-backend.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@duyet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1e13112f-1ec9-4de7-99d5-025599c7e2da

📥 Commits

Reviewing files that changed from the base of the PR and between 234926d and eda2a1c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • .changeset/cli-openshell-sandbox-backend.md
  • packages/cli/README.md
  • packages/cli/package.json
  • packages/cli/src/bridge/commands/daemon.ts
  • packages/cli/src/bridge/commands/setup.ts
  • packages/cli/src/bridge/commands/status.ts
  • packages/cli/src/bridge/lib/bridge-sandbox-openshell.test.ts
  • packages/cli/src/bridge/lib/bridge-sandbox.ts
  • packages/cli/src/bridge/lib/config.ts
  • packages/cli/src/bridge/lib/openshell-client.test.ts
  • packages/cli/src/bridge/lib/openshell-client.ts
  • packages/cli/src/bridge/lib/sandbox-backend.test.ts
  • packages/cli/src/bridge/lib/sandbox-backend.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-openshell-sandbox-backend

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • In openshell-client.ts, buildCredentials uses a dynamic require("node:fs") inside an otherwise ESM-style module; consider switching to a regular import/readFileSync so the CLI bundle doesn’t rely on require at runtime, which can be brittle with esbuild and type: module.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `openshell-client.ts`, `buildCredentials` uses a dynamic `require("node:fs")` inside an otherwise ESM-style module; consider switching to a regular `import`/`readFileSync` so the CLI bundle doesn’t rely on `require` at runtime, which can be brittle with esbuild and `type: module`.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

duyet added a commit that referenced this pull request Jul 24, 2026
* feat(cli): optional OpenShell sandbox backend for the bridge daemon

Relayed sandbox ops (`sandbox_provider: "subprocess"`) can now execute
inside an OpenShell sandbox on the paired machine instead of `child_process`
on the host. Same relay protocol, isolated execution, gateway-enforced
egress — but an empty box: none of the user's repos, toolchains, or CLI
auth are visible. So it is explicit opt-in only, via `oma bridge setup`
(offered when a gateway is detected) or `BRIDGE_SANDBOX_BACKEND=openshell`.
Deliberately no "endpoint present → use it" auto-detect, unlike the
in-cluster k8s-bridge: silently flipping a laptop would break every agent
that relies on the host.

- extract a RelaySandboxExecutor seam with all 8 relay ops; subprocess
  stays the default and its behavior is unchanged
- vendor a lean OpenShell gRPC client (the CLI ships a zero-runtime-dep
  esbuild bundle and can't resolve the private @duyet/oma-sandbox), with a
  test asserting the proto stays byte-identical to the adapter's
- destroyAll now awaits box teardown under a bounded timeout
- `oma bridge status` reports the active backend + gateway reachability

* fix(web): stop underlining nav and footer links

Footer columns and header menus are marked up as <ul><li><a>, so the
`li a` half of the prose link selector was matching them and applying
body-copy underlines to navigation. Opt nav/footer back out; those links
already signal hover with a color transition.

Also adds a draft blog post covering OpenShell sandbox support across
all four deployment shapes (self-host gRPC, bridge daemon, Cloudflare,
Kubernetes via Helm), with step-by-step setup and the limitations of
each. Kept as a draft until #295 and #296 merge — two of its four
paths ship in those PRs.

* style(web): restyle landing to AnyRouter design language

Warm-orange gateway brand family (AA-checked light/dark), blueprint
dotted-grid + corner-bracket viz utilities, and editorial ordinal
section markers. Hero HowItFits diagram preserved; only its framing
harmonized.

* style(web): unify landing on shadcn card + radius scale

shadcn New-York elevation on section cards, unified radius scale, and
both landing diagrams wrapped in the shared blueprint measurement-grid
plate. Hero diagram content preserved.

* style(web): animate landing flow diagram wires

Packet-dot motion streaming along the input→agent→result connectors
(AnyRouter gateway-flow style), gated behind prefers-reduced-motion.

* feat(web): add Anatomy blueprint section to landing

New 'What one request touches' section drawing OMA's real request
lifecycle (Session/event-log DO -> Harness -> Sandbox -> Vault proxy)
as an animated schematic; adds reusable .bp-wire / .arch-node vocab.
Sections renumbered 01-04.

* feat(web): add Reach provider fan-out diagram section

New 'Any model, any sandbox' section: animated SVG fan schematic
(providers fan in -> control plane -> sandboxes fan out), offset-path
packet motion, reduced-motion gated. Sections renumbered 01-05.

* feat(sandbox): browser-vm — run the agent sandbox as a WASM VM in a browser tab

A new relay-shaped sandbox provider whose runtime lives inside a user's
browser tab (v86 WASM VM by default) instead of a server-side container.
Reuses the RuntimeRoom WebSocket relay: the tab registers as a runtime
(kind=browser-vm) and services sandbox.op frames against the VM.

- packages/sandbox: BrowserVmSandbox adapter + wire protocol, provider
  seeding, classify branch (bridge/browser-vm).
- apps/agent: BrowserVmRelaySandbox (lazy RuntimeRoom sandbox-WS attach,
  kind-aware pickOnlineRuntimeId); resolveCfSandbox branches bridge by
  type so browser-vm no longer falls through to the laptop-daemon relay.
- apps/main: GET /sandbox-tab host page (COOP/COEP, v86 engine seam,
  OPFS workspace mirror, pairing + 25s heartbeat); _attach accepts
  ?access_token (browser WS can't set Authorization); /exchange + runtimes
  gain a kind discriminator.
- runtimes.kind migrations (main + main-node pg/sqlite).
- apps/console: RuntimesList browser-vm card with Open sandbox tab action.
- kind-aware /v1/hosting_types health; main-node reports not_configured.
- docs: AGENTS.md provider row + docs/browser-vm-sandbox.md.

Engines are BYO-license (v86 BSD-2 default; WebContainers/CheerpX slots).
Deferred: tab vault MITM, workspace backups, chunk streaming, mounts.

* chore(web): add shadcn components.json + @/* path alias

Enables future shadcn component adds; @/* -> src/* alias.

* fix(sandbox): browser-vm UTF-8-safe serial decode + exec timeout slack

- Host page v86 readFile decoded with bare atob() while writeFile
  encoded UTF-8-safely — non-ASCII file reads and all exec stdout/stderr
  (collected via readFile) mojibaked. Symmetric decode now.
- Adapter armed its pending-call timer at exactly the command budget,
  but the host needs that budget plus two post-exec output round trips;
  near-timeout commands failed client-side. #call gets timeoutMs + 15s
  slack (mirrors bridge-relay); wire timeout_seconds unchanged.

* docs(web): present the full sandbox-runtime matrix incl. browser-vm

- landing: browser-vm card in SandboxProviderGrid + infrastructure page,
  CF-providers FAQ answer, homepage SEO description
- fit-diagram: browser-vm app-window ProviderMark (shared with Console)
- blog: draft post 'Your Browser Tab Is Now an Agent Sandbox'
- docs + docs site: browser-vm rows in every sandbox-provider table
  (runtimes, features, self-host, how-it-works, architecture, node-docker)
@duyet
duyet merged commit fee5ad3 into main Aug 2, 2026
6 checks passed
@duyet
duyet deleted the feat/cli-openshell-sandbox-backend branch August 2, 2026 06:07
This was referenced Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant