feat(delivery): docker sandbox adapter + owner-only infrastructure (v2 phase A3-1)

The execution boundary for v2. Adds the local docker/podman sandbox adapter
and the hardened host subprocess runner every other delivery adapter will use.

- adapters/process.py: the one place the platform shells out. argv-only (no
  shell), start_new_session + killpg on timeout, concurrent stdout/stderr
  drains with a byte cap, explicit minimal env (never os.environ), and a
  missing binary reported as spawn_failed rather than an exception.
- adapters/docker_sandbox.py: DockerSandboxAdapter over SandboxProviderPort.
  --user 1000:1000, --cap-drop ALL, no-new-privileges, pids/cpu/memory limits,
  --read-only with tmpfs scratch, one bind mount (the work tree, never the git
  dir), labels, no published ports. Path-safe file access; secrets refused in
  `docker exec -e`. Extras for later phases: start_demo_container (loopback
  publish only), list_managed_containers, remove_container.
- registry: SANDBOX_PROVIDER=docker, cached singleton so the workspace registry
  survives create_workspace -> start_session.
- sandbox_gateway worker: reap_orphans() removes labelled containers no live
  session points at, audited as sandbox.reap; run_pass() does both passes.
- sandbox router: every mutating route now requires the platform owner
  (PLAN-v2 §7.7); read-only status routes stay open to org members.
- sandbox-panel: canManageInfra prop (fails closed) hides the create form,
  the session panel and the action buttons from non-owners.

Tests: 346 API (+37), 67 web (+3); ruff, mypy, tsc and lint clean.
Three existing tests now drive the sandbox as the platform owner, since that
is who may touch it.

Report: docs/plan/reports/agent-3-phase-1.md
This commit is contained in:
mjalmoudy94
2026-10-13 23:57:09 +03:30
parent 0cd9194b64
commit a0c8473eb8
15 changed files with 2047 additions and 79 deletions
+9 -1
View File
@@ -23,6 +23,7 @@ from __future__ import annotations
import pytest_asyncio
from httpx import AsyncClient
from app.domain.identity.models import PlatformRole
from tests.conftest import auth_header, signup
@@ -34,7 +35,14 @@ async def _platform_seed(seeded):
async def test_end_to_end_smoke_path(client: AsyncClient):
# -- 1. signup -------------------------------------------------------- #
signup_body = await signup(client, "smoke@example.com", name="کاربر دود")
# The platform owner, not a plain manager: step 6 drives the sandbox
# directly, and v2 makes container/command access owner-only
# (PLAN-v2 §7.7). The org-scoped prefix still scopes the tenant and the
# audit trail, so this is the same path a customer walks minus the
# infrastructure step.
signup_body = await signup(
client, "smoke@example.com", name="کاربر دود", platform_role=PlatformRole.OWNER
)
token = signup_body["access_token"]
headers = auth_header(token)