mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[sandbox-ext] Phase 4: panel extension picker + allowlist docs
Project edit dialog (Sandbox section) exposes a per-service extension
picker — Switches from the allowlist grouped under each enabled service
(postgres: pgvector/PostGIS/pg_trgm/citext/uuid-ossp; redis: RediSearch/
RedisJSON/RedisBloom; mongo has none), mirroring the backend
SANDBOX_ENGINE_FEATURES allowlist. State holds a per-service Set; payload
builds sandbox_extensions only for enabled services with non-empty picks
(empty {} clears the column, mirroring sandbox_services' always-send —
exclude_unset + no exclude_none means an explicit {} writes NULL). The
picker renders only for opted-in services with activatable features.
Types: Project.sandbox_extensions (Record<string,string[]> | null),
ProjectUpdate.sandbox_extensions? (not on ProjectCreate, mirroring
sandbox_services). Mock create seeds null.
Docs name the allowlist (the security containment — no plpython3u), the
no-default-set rule (opters set explicitly, existing opters stay bare), the
standing-vs-per-call union, cache-by-features, kitchen-sink image selection,
and the recommendation to set the full set in project settings so agents
request subsets. sandbox-db.md gains an Extensions section; task-tools.md
and config-reference.md updated; CLAUDE.md sandbox paragraph extended.
Gate: panel typecheck + lint + prettier clean, 516 tests pass.
This commit is contained in:
@@ -156,7 +156,7 @@ Per-agent-spawn throwaway Postgres/Redis/Mongo, replacing (never coexisting with
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `ROBOCO_SANDBOX_DB_ENABLED` | `false` | Master switch. Off = spawning behaves exactly as today (the legacy `_append_gate_env` prod-creds injection, itself gated by `ROBOCO_TOOLCHAIN_MATCH_ENABLED`). Only projects with their `sandbox_services` column set (migration `057`) participate even when on. The valid service set is `VALID_SANDBOX_SERVICES` in `roboco/models/sandbox.py` (registry-derived: postgres / redis / mongo); adding an engine is one class + one registry line, no orchestrator edit. Env injected per engine: `ROBOCO_TEST_DB_*`, `ROBOCO_TEST_REDIS_*`, `ROBOCO_TEST_MONGO_*` (incl. `ROBOCO_TEST_MONGO_AUTH_DB=admin`). |
|
||||
| `ROBOCO_SANDBOX_DB_ENABLED` | `false` | Master switch. Off = spawning behaves exactly as today (the legacy `_append_gate_env` prod-creds injection, itself gated by `ROBOCO_TOOLCHAIN_MATCH_ENABLED`). Only projects with their `sandbox_services` column set (migration `057`) participate even when on. The valid service set is `VALID_SANDBOX_SERVICES` in `roboco/models/sandbox.py` (registry-derived: postgres / redis / mongo); adding an engine is one class + one registry line, no orchestrator edit. Env injected per engine: `ROBOCO_TEST_DB_*`, `ROBOCO_TEST_REDIS_*`, `ROBOCO_TEST_MONGO_*` (incl. `ROBOCO_TEST_MONGO_AUTH_DB=admin`). A project may also declare `sandbox_extensions` (migration `072`) — a per-service extension/module map activated post-ready via `docker exec`, bounded by a fixed allowlist (`SANDBOX_PG_EXTENSIONS` / `SANDBOX_REDIS_MODULES`; no `plpython3u`). See `docs/rag/architecture/sandbox-db.md`. |
|
||||
|
||||
## X (Twitter) Engine
|
||||
|
||||
|
||||
@@ -31,16 +31,20 @@ A second, per-project gate applies even when the flag is on: only a project with
|
||||
Provisioning is **on-demand**: nothing is provisioned at spawn. A developer or QA agent calls the `request_sandbox` content tool (on `roboco-do`) when it actually needs a sandboxed DB, and the orchestrator provisions it inline. Wiring: `roboco/api/schemas/v1/do.py` `RequestSandboxRequest` → `POST /api/v1/do/request_sandbox` → `ContentActions.request_sandbox` (`roboco/services/gateway/content_actions.py`) → `AgentOrchestrator.ensure_sandbox` → `roboco/mcp/do_server.py`'s `request_sandbox()` tool (1080s timeout — `ensure_sandbox` always provisions the project's full opted-in set on first call, so an all-three-engines-cold first request is the norm the timeout must cover, not a rare edge case).
|
||||
|
||||
```python
|
||||
request_sandbox(services: list[str] | None = None)
|
||||
request_sandbox(
|
||||
services: list[str] | None = None,
|
||||
extensions: dict[str, list[str]] | None = None,
|
||||
)
|
||||
```
|
||||
|
||||
`services` omitted means the project's whole opted-in set. Guards fire in order, each with a clean `invalid_state` envelope + `remediate`:
|
||||
`services` omitted means the project's whole opted-in set. `extensions` is an optional per-service map of extensions/modules to activate (see "Extensions and modules (on the fly)" below); it unions with the project's standing set. Guards fire in order, each with a clean `invalid_state` envelope + `remediate`:
|
||||
|
||||
1. `ROBOCO_SANDBOX_DB_ENABLED` off → refused before any DB lookup.
|
||||
2. No active, project-bound task (agent hasn't `give_me_work`'d) → refused.
|
||||
3. Project has no `sandbox_services` opted in → refused.
|
||||
4. A requested service outside the project's opted set → refused, remediate **names the allowed set**.
|
||||
5. Orchestrator handle unavailable (e.g. mid-restart) → refused, but **retryable** — the one guard that isn't a permanent no.
|
||||
5. A requested extension outside the per-service allowlist, or for a service not opted in → refused, remediate **names the allowed extensions**.
|
||||
6. Orchestrator handle unavailable (e.g. mid-restart) → refused, but **retryable** — the one guard that isn't a permanent no.
|
||||
|
||||
Only past all five does it call `ensure_sandbox` and provision. A genuine provisioning failure (image pull, readiness timeout) also surfaces as a retryable `invalid_state`, never a spawn refusal — sandbox trouble can no longer block a spawn or an agent's turn.
|
||||
|
||||
@@ -71,6 +75,22 @@ The `env` sub-dict (`SandboxInfo.as_payload()`, `roboco/models/sandbox.py`) carr
|
||||
|
||||
Only `developer` and `qa` roles carry `request_sandbox` in their spawn manifest (`roboco/services/gateway/role_config.py` `_DEV_DO` / `_QA_DO`) — the DB-needing gate roles. It is carried unconditionally on those manifests (declarative); the real gating is the project opt-in check inside the verb itself.
|
||||
|
||||
## Extensions and modules (on the fly)
|
||||
|
||||
A sandboxed DB/Redis can be built to a venture's declared extensions on the fly — "need a db? ok, extensions?" — instead of a fixed flavor. A project declares a per-service extension/module map in `projects.sandbox_extensions` (migration `072`, jsonb null), e.g. `{"postgres": ["vector", "postgis"], "redis": ["search"]}`. The provisioner activates them **post-ready** via `docker exec` (`CREATE EXTENSION IF NOT EXISTS <name>` for pg, `MODULE LOAD <so>` for redis) then verifies presence, so a missing extension file fails loudly at first provision rather than silently. Design spec: `docs/internal/specs/2026-07-13-sandbox-extensions-on-the-fly.md`.
|
||||
|
||||
**The allowlist is the security containment.** `SANDBOX_PG_EXTENSIONS = {vector, postgis, pg_trgm, citext, uuid-ossp}` and `SANDBOX_REDIS_MODULES = {search, json, bloom}` (`roboco/models/sandbox.py` `SANDBOX_ENGINE_FEATURES`) are the only extensions/modules the system will ever activate. A `plpython3u` (superuser-RCE) is excluded by construction — rejected at the Project pydantic boundary before it can be persisted, and again at the verb. Mongo has no activatable features and is intentionally absent from the map. The Project model normalizes order, drops empty feature lists (a service with `[]` is bare), and rejects unknown service keys or unallowed features with the allowlist named.
|
||||
|
||||
**No default set.** Opters set the extensions they need explicitly; an unset service is bare. Existing opted-in projects stay byte-for-byte unchanged on the bare path — no behavior change unless a venture declares a set.
|
||||
|
||||
**Standing set + per-call override.** The project field is the trusted standing set; a `request_sandbox(extensions=…)` call unions a per-call override with it, bounded to the opted-in service set and the allowlist. The union reaches `ensure_sandbox` as the `features` kwarg. **Recommendation: set the full set the venture needs in project settings, so agents can request subsets at runtime** — a subset request is a cache hit, a feature superset re-provisions (rotates creds), mirroring the services-superset case.
|
||||
|
||||
**Cache-by-features.** A cached `SandboxInfo` satisfies a new `ensure_sandbox` call iff the services are a subset AND every requested feature per service is already cached. A feature superset re-provisions with fresh creds (the pre-clear teardown removes the now-too-small container), exactly as a services superset does.
|
||||
|
||||
**Feature-aware image selection.** A bare request (no features) uses the light upstream image (`postgres:16-alpine` / `redis:8-alpine`); a request with features pulls a kitchen-sink image that carries all extension/module files (`roboco-sandbox-pg:latest` — `pgvector/pgvector:pg16` + postgis; `redis/redis-stack-server:latest`). So a bare project never pays for a heavier image, and the intersection case (pgvector AND postgis in one DB) just works — flavors are inadequate because the intersection isn't a flavor. `SandboxEngine.image_for(features)` picks; the provisioner pulls whichever it resolves to.
|
||||
|
||||
**Delivery.** The ok-envelope's per-service evidence entry carries `available_extensions` (the sorted list actually activated) so an agent doesn't guess what was turned on. The project's standing set is edited in the panel's project edit dialog (Sandbox section) — checkboxes from the allowlist grouped under each enabled service.
|
||||
|
||||
## Provisioning
|
||||
|
||||
`ensure_sandbox(agent_slug, requested, opted)` provisions `requested | opted` (in practice the project's whole opted-in set) through a single generic `_provision_engine` (no per-engine branch): it generates a random 32-hex-char password (`secrets.token_hex(16)`), pre-pulls the image, `docker run`s the sibling container, and polls the engine's readiness probe up to its deadline.
|
||||
|
||||
@@ -165,4 +165,4 @@ Your plan's steps are the progress checklist; the percentage is derived from com
|
||||
|
||||
## Sandbox DB/Redis/Mongo (Developer + QA)
|
||||
|
||||
`request_sandbox(services=None)` — a **content tool** on `roboco-do`, not a flow verb — provisions a throwaway sandbox Postgres/Redis/Mongo on demand, for a project that opted in (`projects.sandbox_services`). Only `developer` and `qa` carry it. Omit `services` for the project's whole opted-in set; requesting one outside it is rejected naming the allowed set. Creds come back in the envelope's `evidence`, one entry per service, including ready-to-`export` `ROBOCO_TEST_*` values for gate tooling. Calling it again is a cheap no-op (same creds). See `docs/rag/architecture/sandbox-db.md`.
|
||||
`request_sandbox(services=None, extensions=None)` — a **content tool** on `roboco-do`, not a flow verb — provisions a throwaway sandbox Postgres/Redis/Mongo on demand, for a project that opted in (`projects.sandbox_services`). Only `developer` and `qa` carry it. Omit `services` for the project's whole opted-in set; requesting one outside it is rejected naming the allowed set. `extensions` is an optional per-service map of extensions/modules to activate (e.g. `{"postgres": ["vector"]}`), unioned with the project's standing `sandbox_extensions` set and bounded by a fixed allowlist (pg: vector/postgis/pg_trgm/citext/uuid-ossp; redis: search/json/bloom — no `plpython3u`); an unallowed feature or a feature for a non-opted service is rejected naming the allowed set. Creds come back in the envelope's `evidence`, one entry per service, including ready-to-`export` `ROBOCO_TEST_*` values for gate tooling and an `available_extensions` list of what was activated. Calling it again is a cheap no-op (same creds) as long as the requested features are a subset of the cached set. See `docs/rag/architecture/sandbox-db.md`.
|
||||
|
||||
Reference in New Issue
Block a user