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:
@@ -198,6 +198,29 @@ class SandboxSessionRepository:
|
||||
)
|
||||
return [_session(r) for r in (await self.session.execute(stmt)).scalars()]
|
||||
|
||||
async def list_active_external_ids(self, *, limit: int = 5_000) -> set[str]:
|
||||
"""External ids of every session that is not stopped.
|
||||
|
||||
The orphan reaper's query: a container whose id is not in this set has
|
||||
no live row pointing at it, so it is holding resources for nobody.
|
||||
"""
|
||||
stmt = (
|
||||
select(SandboxSessionRow.external_session_id)
|
||||
.where(
|
||||
SandboxSessionRow.status.in_(
|
||||
[
|
||||
str(SessionStatus.STARTING),
|
||||
str(SessionStatus.RUNNING),
|
||||
str(SessionStatus.PAUSED),
|
||||
]
|
||||
),
|
||||
SandboxSessionRow.external_session_id.is_not(None),
|
||||
)
|
||||
.limit(min(limit, 20_000))
|
||||
)
|
||||
rows = (await self.session.execute(stmt)).scalars().all()
|
||||
return {str(value) for value in rows if value}
|
||||
|
||||
async def create(self, sandbox_session: SandboxSession) -> SandboxSession:
|
||||
row = SandboxSessionRow(
|
||||
id=sandbox_session.id, workspace_id=sandbox_session.workspace_id,
|
||||
|
||||
Reference in New Issue
Block a user