CodeDocs Vault

Sandbox & Runtime Environments

Omnigent runs agent sessions in throwaway cloud sandboxes. The twist is breadth: one host process can be launched into Modal, Daytona, Islo, E2B, CoreWeave, Kubernetes, NVIDIA OpenShell, Boxlite — or Databricks Lakebox internally — through a single small interface. This doc covers that provider abstraction, two worked examples, the managed-host flow, and the credential proxy (the most interesting security idea in the codebase).

There are two distinct isolation layers. Keep them apart:

  1. Remote sandbox launchers — provision a whole machine (VM / microVM / container / Pod) from a provider, ship Omnigent into it, run omnigent host there. Lives in omnigent/onboarding/sandboxes/.
  2. Local in-process sandboxing — when the agent runs a shell tool, that subprocess is jailed with bwrap (Linux) or Seatbelt (macOS) plus an L7 egress proxy. Lives in omnigent/inner/ (+ thin re-exports in omnigent/sandbox/). The credential proxy plugs in here.

1. The launcher abstraction

Everything provider-specific hides behind one ABC, SandboxLauncher (omnigent/onboarding/sandboxes/base.py:152). The generic bootstrap flow composes its primitives; provider modules supply the transport. The docstring states the contract bluntly: "Everything provider-specific … lives behind a SandboxLauncher implementation; everything provider-agnostic … lives in bootstrap" (base.py:9-12).

The interface is small. Required methods:

Method Role Ref
prepare() Local preflight: verify provider tooling + creds on the calling machine base.py:200
provision(name) Create (or reserve) a sandbox; return its id base.py:212
run(id, cmd) Run a shell command, capture output → RemoteCommandResult base.py:364

Methods with a shared default:

Method Default Ref
start_host(...) EXEC model: probe $HOME, mkdir workspace, optional git clone, start omnigent host detached base.py:230
run_background(...) setsid nohup … & so the process outlives the exec session base.py:379

Optional capabilities — default to a clean SandboxCapabilityError naming what's missing — put (ship files), stream_exec (PTY stream), exec_foreground (hold a process open), forward_local_port, terminate, resume, keep_alive, attach, wheel_install_command (base.py:330-557).

Behaviour is declared with four ClassVars, checked before any remote work so a provider fails fast instead of mid-flow:

Two boot models share start_host:

The registry

Providers register name → "module:Class" in _LAUNCHERS (omnigent/onboarding/sandboxes/__init__.py:57-73). Modules import lazily (each pulls an optional SDK extra) and may be absent from a build — available_providers() uses find_spec with no import side effects (omnigent/__init__.py:76-94), and the OSS distribution simply omits the internal lakebox module. get_launcher(provider) resolves a name to an instance (omnigent/__init__.py:97).

The default image

Across providers, a sandbox boots from one prebaked image: ghcr.io/omnigent-ai/omnigent-host:latest (base.py:32). It bakes the full Omnigent install + git/tmux/curl + the coding-harness CLIs, so sandbox creation skips dependency install — provider + server_url is a complete config. For local-checkout development, sandbox create overlays freshly-built wheels with pip install --force-reinstall --no-deps (base.py:42-76) — --force-reinstall because the image already bakes 0.1.0, so pip would otherwise skip silently.

One interface, nine backends The whole point: provider lock-in collapses to one ABC and a registry line. A new sandbox provider is a single file implementing prepare / provision / run / terminate — the managed flow, the CLI, and the credential plumbing all come for free.


2. Two worked examples

ModalSandboxLauncher (omnigent/onboarding/sandboxes/modal.py), provider = "modal" (omnigent/onboarding/sandboxes/modal.py:268). Modal's managed container platform via the modal SDK.

E2B (template-based, real process kill)

E2BSandboxLauncher (omnigent/onboarding/sandboxes/e2b.py), provider = "e2b" (omnigent/onboarding/sandboxes/e2b.py:353). The e2b SDK.


3. Managed hosts: the server provisions the sandbox

There are two ways an omnigent host comes to exist.

The managed flow is where most providers actually run. Providers with managed-launch support: modal, daytona, boxlite, cwsandbox, islo, e2b, openshell, kubernetes (omnigent/server/managed_hosts.py:145).

How host and sandbox relate

The host abstraction (omnigent/host/) is the durable identity and tunnel; the sandbox is disposable compute. They are deliberately decoupled:

"The host's identity is DURABLE while its sandbox is not" — the hosts row carries the launch-token digest, provider, and sandbox id, and a relaunch overwrites them in place: a new sandbox generation under the same host_id, so session bindings survive a sandbox dying at the provider's lifetime cap (omnigent/server/managed_hosts.py:11-15).

A managed sandbox host never persists anything to its own config file. The server generates the identity + token and injects three env vars — OMNIGENT_HOST_TOKEN, OMNIGENT_HOST_ID, OMNIGENT_HOST_NAME — which override the on-disk identity entirely (omnigent/host/identity.py:27-29, 77-85). The token rides a dedicated WebSocket header, X-Omnigent-Host-Token, not Authorization, so intermediate proxies can't confuse it with a user Bearer token (identity.py:36).

Launch sequence

launch_managed_host (omnigent/server/managed_hosts.py:1690) orchestrates it:

  1. launcher.prepare() then launcher.provision(host_name)sandbox_id (omnigent/server/managed_hosts.py:1746-1748).
  2. _arm_and_start_host mints a 32-byte launch token and pre-registers the host row with its token digestbefore the host process starts (omnigent/server/managed_hosts.py:1888-1898). This closes the dial-back race: the credential resolves the instant the host first connects.
  3. launcher.start_host(...) — exec model execs in and backgrounds the host; entrypoint model (k8s) creates the Pod (omnigent/server/managed_hosts.py:1905-1916).
  4. _wait_for_host_online polls the hosts table up to 120s (omnigent/server/managed_hosts.py:1941, MANAGED_HOST_ONLINE_TIMEOUT_S).
  5. Any post-provision failure tears the sandbox down and revokes the token before re-raising — otherwise the box leaks until the provider's lifetime cap (omnigent/server/managed_hosts.py:1918-1937).

Token TTLs are set to outlive the sandbox, so a live box always re-authenticates across tunnel reconnects, while a token leaked from a dead box expires. Modal: 25h (24h cap + slack, omnigent/server/managed_hosts.py:163). Daytona / Islo / OpenShell / Kubernetes: 7 days policy bound (omnigent/server/managed_hosts.py:172-199). CoreWeave / E2B derive the TTL from the operator-overridable lifetime cap (omnigent/server/managed_hosts.py:201-205).

flowchart TD
    A["POST /v1/sessions<br/>host_type=managed"] --> B["launcher.prepare()"]
    B --> C["launcher.provision(name)<br/>→ sandbox_id"]
    C --> D["mint launch token<br/>pre-register host row<br/>(digest + TTL)"]
    D --> E{boot model}
    E -- exec --> F["start_host: $HOME probe,<br/>mkdir workspace,<br/>git clone, host detached"]
    E -- entrypoint --> G["k8s: create token Secret<br/>+ Pod (cmd = omnigent host)"]
    F --> H["host dials back<br/>X-Omnigent-Host-Token"]
    G --> H
    H --> I{online within 120s?}
    I -- yes --> J["session binds runner,<br/>tunnel live"]
    I -- no / error --> K["terminate sandbox +<br/>revoke token, raise 502"]
    J --> L["session ends / cap hit →<br/>terminate; relaunch reuses host_id"]

Config is supplied two ways, one seam: a YAML sandbox: block parsed by parse_sandbox_config (omnigent/server/managed_hosts.py:604), or — for embedding deployments — a ManagedSandboxConfig built directly with a custom launcher factory (omnigent/server/managed_hosts.py:87-97). Provider credentials are never in the YAML (12-factor): Modal reads MODAL_TOKEN_*, Daytona reads DAYTONA_API_KEY, Islo reads ISLO_API_KEY, all from the server process environment (omnigent/server/managed_hosts.py:72-83).

For your project — Swisscheese: This is the harness shape Swisscheese needs: a durable session identity that survives the underlying compute being recycled. Run N reviewer agents as N managed hosts, each in its own provider sandbox; when one hits a 24h cap, relaunch reuses the host_id and the review pipeline's bindings stay intact. The launcher ABC means you can mix providers (cheap container for the writer, GPU OpenShell box for a model-heavy reviewer) behind one interface.


4. The provider matrix

Provider Mechanism (SDK) Isolation level Notes
Modal modal SDK Managed container Hard 24h cap, no extend (omnigent/onboarding/sandboxes/modal.py:58); no port-forward; pidfile kill workaround
E2B e2b SDK Managed sandbox Boots templates, not registry images (omnigent/onboarding/sandboxes/e2b.py:89); 24h Pro / 1h Hobby; real process kill
Daytona daytona SDK Cloud sandbox No lifetime cap; 15-min idle auto-stop disabled at provision; keep_alive keeps it off (omnigent/onboarding/sandboxes/daytona.py:391); free-tier egress allowlist
Islo raw httpx (no SDK extra) Cloud sandbox API-key + token cache; deleted on teardown; clears Islo's seeded apiKeyHelper when a Claude cred is injected (omnigent/onboarding/sandboxes/islo.py:375)
CoreWeave cwsandbox SDK CW cloud sandbox Hard cap, default 24h, env-overridable OMNIGENT_CWSANDBOX_MAX_LIFETIME_S; egress explicitly set internet (omnigent/onboarding/sandboxes/cwsandbox.py)
Kubernetes kubernetes client Pod (container on cluster) Entrypoint-as-host (omnigent/onboarding/sandboxes/kubernetes.py start_host); token via per-Pod Secret; Pod Security "restricted", runAsNonRoot, drop ALL caps, seccomp; amd64-only
OpenShell NVIDIA openshell SDK (gRPC) On-prem gateway (Docker/Podman/microVM/k8s driver) GPU-capable; file transfer via stdin→cat; no API key — uses active gateway
Boxlite boxlite SDK Micro-VM (local KVM/HVF) or remote pool Managed-only, supports_cli_bootstrap=False (omnigent/onboarding/sandboxes/boxlite.py:194); local mode runs micro-VMs on the server host; private-registry creds via env
Lakebox Databricks (internal) Databricks workspace sandbox Internal-only module; parses but rejects managed launch in OSS

No provider sets can_resume = True or supports_local_port_forward = True in the OSS tree — sandboxes are ephemeral and the in-sandbox OAuth port-forward is a Lakebox-era capability. Every provider's provision defers materialization where it can and arms the token first.


5. The credential proxy: secrets that never enter the sandbox

This is the standout security pattern, and it belongs to the local sandbox layer (designs/SANDBOX_CREDENTIAL_PROXY.md; omnigent/inner/credential_proxy.py, omnigent/inner/egress/proxy.py).

The problem. A sandboxed tool often needs to authenticate to an external host — gh api, git clone https://…, a Bearer SaaS API. The naive fix injects the real token into the sandbox env or a config file. That defeats the sandbox: any code the agent runs — including code a prompt-injection payload tricked it into running — can read the token from os.environ / ~/.gitconfig and exfiltrate it.

The mechanism. Omnigent already runs a mandatory L7 MITM egress proxy for all HTTP(S) leaving the sandbox (the egress allow-list). It extends that proxy to attach credentials on the way out. The default is swap-on-access: nothing credential-shaped enters the sandbox at all.

  1. Parent resolves the secret. The unsandboxed parent reads each configured secret from its source ({env} / {file} / {command}). The real value stays in the parent and the proxy's in-memory rewrite table — never serialized, never on argv, never on the sandbox disk.
  2. Inject on access. A tool fires its request with no Authorization header. The proxy recognises the bound host and injects Authorization: <scheme> <real> outbound. git, curl, python, node — any HTTP client — authenticate with zero in-sandbox wiring.
  3. Placeholder shim for gating clients. Some clients (notably gh) refuse to issue a request without seeing a local token, so there's no outbound request to decorate. For those, the parent mints a random single-use placeholder oa_cred_… and injects only that into the env var. The client believes it is authenticated; the proxy swaps the placeholder for the real secret on the wire.
  4. Leak guard. A placeholder presented for a host it is not bound to (exfiltration attempt) is rejected 403; an unknown oa_cred_* value is likewise rejected. Even if a tool reads the placeholder from its own env and replays it against an attacker host, the proxy attaches no real credential.
  5. No clobbering. A real Authorization header the client set itself is forwarded untouched and suppresses injection.
flowchart LR
    subgraph parent["parent (unsandboxed)"]
        S["resolve real secret<br/>→ proxy rewrite table"]
    end
    subgraph box["sandbox (jailed)"]
        T["git / curl / python<br/>GET /repo (NO auth header)"]
    end
    subgraph proxy["egress MITM proxy"]
        P["host bound?<br/>inject Authorization: scheme real<br/>(wrong-host placeholder → 403)"]
    end
    S -.holds secret.-> P
    T --> P
    P --> U["upstream → 200 OK"]

Why it's safe by construction. The YAML block requires egress_rules plus a hard-isolating backend — linux_bwrap or darwin_seatbelt — and the parser rejects it otherwise. Only those two backends can guarantee the MITM proxy is the only egress path: a tool can't open a raw socket around it. On Linux that's --unshare-net (omnigent/inner/bwrap_sandbox.py:449) with traffic forced through the proxy on loopback; on macOS it's Seatbelt's (deny default) egress baseline (omnigent/inner/seatbelt_sandbox.py:46). The resolved secret is parent-side only and deliberately excluded from SandboxPolicy serialization, so it can't leak into a dump.

For your project — EU AI Act compliance platform: For an audit-traceable GRC agent, this is the difference between "the agent had the deploy token in scope" and "the agent could never read the token, only cause an authenticated request." The egress proxy is a single chokepoint you can log: every authenticated outbound call, its host, and its bound credential — without the secret ever crossing the isolation boundary. That's a clean Article 12 record-keeping story and a concrete Article 9 risk control.

HTTP(S) only The credential proxy covers Bearer / Basic over HTTP(S). SSH git remotes are out of scope (design doc §Non-goals) — an ssh:// clone gets no swap-on-access. Plan repo access around HTTPS where you want secretless operation.


6. Local backends, in one breath

When the agent runs a shell tool inside a host, that subprocess is jailed by a registered SandboxBackend (omnigent/inner/sandbox.py:266):

Thin re-exports at omnigent/sandbox/bwrap.py and omnigent/sandbox/seatbelt.py keep the import surface stable while the legacy inner/ modules retire. Note: there is no gVisor / runsc path anywhere in the tree — isolation is namespaces + seccomp (Linux), Seatbelt (macOS), provider-native VMs/containers (remote), and the L7 egress proxy throughout.


7. Design observations

Good ideas.

Potential pitfalls.