# Sandbox profiles Customers own sandbox configuration and authorization in their provider plugin. Users choose an approved profile when starting or cloning a managed session. Omnigent stores the provider/profile revision and trusted execution identity. ## Sub-features - `catalog`: per-user discovery, loading, empty results, server search, a 20-result cap and retry. - `selection`: explicit selection for participating providers; exact revisions. - `authorization`: provider checks on create, clone, reuse, wake and replacement. - `gating`: default-off feature; ordinary providers retain deployment defaults. ## How to get to it (user POV) - Desktop home page or New session dialog: select a sandbox provider, then use the Sandbox profile menu beside the repository picker. - Mobile new session: close the navigation drawer if open and use the profile menu above the composer. - Clone on desktop/mobile: session actions → Clone session → sandbox destination → Sandbox profile. A same-provider fork defaults to the source profile and reports a newer offered revision. Starting/cloning requires an authorized choice. - Message-action branch clones, sidebar clone actions and the error-card fork action use this same profile-enabled fork dialog. - Reconnect → Clone uses the same searchable, capped picker and source-profile default when a sandbox destination supports profiles. - Settings has no sandbox editor. `/settings/sandboxes` falls back to General. ## Driving it with the repro environment Preconditions: read [Verify Omnigent](skills/verify-omnigent/SKILL.md). Enable `OMNIGENT_FEATURES=sandbox_profiles` and configure a provider with `manages_profiles = True`. The example uses a customer-owned local catalog. Build this checkout with `pnpm --filter web run build` first. `tests/e2e_ui/onboarding/test_sandbox_profiles.py::test_provider_owned_profiles` is an own-environment test. It installs the actual example package in a temporary directory and runs an isolated authenticated server with SQLite. It covers per-user desktop/mobile choices, clone, denied/omitted selection, revocation, provider outage and absent Settings. It needs no cluster or runner. ```sh OMNIGENT_SANDBOX_PROFILE_EVIDENCE_DIR=/tmp/sandbox-profile-proof \ .venv/bin/python -m pytest tests/e2e_ui/onboarding/test_sandbox_profiles.py \ --ui-skip-build --video=on --screenshot=on --reruns 0 \ --output=/tmp/sandbox-profile-proof/playwright ``` Allowed browser create/fork POSTs return an intercepted provisioning error; this proves request contents and preserved drafts, not infrastructure allocation. Direct API denials use the real server. Backend lifecycle coverage uses a recording launcher in `tests/server/integration/test_sandbox_profile_launch.py`. `tests/e2e_ui/onboarding/test_sandbox_profiles.py::test_default_launch_without_profile_feature` checks no profile queries, no picker and unchanged launch payloads with the flag off or older-server feature metadata. Enabled-provider opt-out is covered by `web/src/shell/NewChatDialog.test.tsx::uses only selections advertised by the server and provider`, including the unchanged launch payload and absent picker after discovery. Component checks cover the new-session dialog/home composer, attachments, clone and branch clone, scoped choices, stale drafts, retry and pagination: ```sh pnpm --dir web exec vitest run src/components/SandboxProfilePicker.test.tsx \ src/lib/sandboxProfilesApi.test.ts src/lib/sessionsApi.test.ts \ src/shell/NewChatDialog.test.tsx src/shell/NewChatDialog.projectPrefill.test.tsx \ src/shell/ForkSessionDialog.test.tsx ``` For actual allocation, follow the [example guide](../examples/sandbox-profile-provider/README.md#using-the-same-plugin-with-real-kubernetes) against a dedicated namespace. Inspect image/resources, repeat wake/replacement, revoke access, and terminate test sandboxes. Record cluster and directory integration evidence separately; the browser test does not exercise them. ## Gotchas - Empty catalogs for participating providers block launch. There is no Default fallback in the picker. Nonparticipating providers have no profile picker. - `sandbox.default_profile_id` gives a participating provider a server-side default for creates that name no profile (API clients, scheduled fresh sandboxes). It is authorized and saved like an explicit pick. `GET /v1/sandbox-profiles` reports it as `default_profile_ids`, and the composer and fork dialog preselect it when nothing else is chosen. - The composer starts on the profile last picked on that provider (saved on pick, per provider in `localStorage`), resolved by id on its current revision, ahead of the default. A profile the provider no longer lists falls back to the default. - Discovery is scoped to the selected provider so unrelated outages do not block ordinary launches. Listing is advisory; the server reauthorizes selection. - A stale or revoked choice requires explicit reselection. Saved sessions cannot silently switch revisions. A provider may retain old authorized revisions. - Forks authorize the new caller; wake authorizes the saved execution identity. Revocation blocks subsequent use but does not terminate an existing sandbox. - Cleanup uses stable provider configuration and sandbox IDs, independent of profile availability. Resource location must survive catalog changes. - The example owns image/resource configuration. Core has no configuration schema, admin editor, catalog writes, directory connector or identity management. - Scheduled fresh sandboxes reject profile-requiring defaults at task create/update unless the default provider has a default profile, which each fire then saves. The session-create tool accepts explicit provider/profile/revision fields for managed placement. Native `/clear` and `/fork` reauthorize and bind the replacement before detaching the source; the last directly bound runner session cannot detach. Host and profile mismatches remain blocked. claude-sdk `/clear` continues in an empty fork (authorized like `/fork`) bound to the source's host, workspace and live runner. It stays in the same sandbox, so it keeps the source's `omnigent.sandbox.*` labels: a relaunch re-clones the same repositories and routes by the same host id. No history or files are copied, and a fork that cannot be bound is deleted again. - Missing sandboxes use the bound session's reference for replacement. Legacy host recovery rejects histories over 200 sessions; use controller lookup for larger histories. Runner-host discovery and reconnect liveness have a separate limit of 10,000 attached sessions; launch a new runner on overflow. Rotation, adoption and rollback use a smaller source lookup of at most 201 candidates and preserve the binding if no source is found. Ordinary hosts skip recovery scans. Native `/clear` retains old sessions on the host, so a long-lived sandbox without controller lookup can reach the host limit through normal use. Ordinary-only deployments skip runner profile discovery; mixed deployments keep its safety bound. - Rollout is opt-in. Drain profiled workloads before disabling the flag or rolling back to a server without profile authorization; retain surviving session labels and controller sandbox assignments. No schema changes or new indexes are involved. Before first opt-in, complete the [legacy-label audit](../docs/extending/provider_profiles.md#rollout-rollback-and-verification) after upgrading every writer. Enable the flag before configuring a provider that requires profiles. - Existing unprofiled sessions retain wake, relaunch, missing-sandbox replacement and runner rotation after opt-in. The provider must retain its unselected lifecycle path; scoped lookup returns `None` only for an authorized legacy resource. Fresh sandbox creation still requires an explicit selection or a configured provider default, and saved profiles never fall back to defaults. The rollout regression lives in `tests/server/integration/test_sandbox_profile_launch.py::test_unprofiled_session_survives_provider_opt_in`. Controller recovery is covered in `tests/server/integration/test_sandbox_profile_controller.py`: last-session deletion, restart, historical revision, revocation and missing-sandbox replacement. The [controller walkthrough](../examples/sandbox-profile-provider/README.md#controller-recovery-demo) is runnable without a cluster.