"""Sandbox gateway worker — housekeeping for the only sanctioned execution path. Two jobs, both about not leaking resources: 1. **Reap idle sessions.** A sandbox session that nobody has touched for hours is still consuming CPU and disk on the provider. The reaper stops it and writes an audit event, because "the platform killed my sandbox" must be answerable from the audit log. 2. **Report the policy in force.** The gateway is the one place where untrusted code runs, so the worker logs the effective network policy and the absolute caps at startup. An operator should be able to see the safety envelope without reading the source. The worker never executes a command. It only stops sessions — the command path is ``SandboxService.run_command``, reachable from the API under a tenant check. """ from __future__ import annotations from datetime import timedelta from app.application.container import build_sandbox_service from app.application.ports.realtime import emit_sandbox_reaped from app.core.config import settings from app.domain.audit.models import AuditAction, AuditEvent from app.domain.base import utcnow from app.infrastructure.repositories.audit import AuditRepository from app.infrastructure.repositories.sandbox import WorkspaceRepository from app.workers.base import WorkerOutcome, worker_session #: Sessions are considered abandoned after this long with no activity. DEFAULT_IDLE_MINUTES = 120 class SandboxGatewayWorker: """Stops idle sandbox sessions and records why.""" name = "sandbox-gateway" async def reap( self, *, idle_minutes: int = DEFAULT_IDLE_MINUTES, limit: int = 200 ) -> WorkerOutcome: outcome = WorkerOutcome(name=self.name) idle_before = utcnow() - timedelta(minutes=max(1, idle_minutes)) async with worker_session() as session: sandbox = build_sandbox_service(session) workspaces = WorkspaceRepository(session) audit = AuditRepository(session) stopped = await sandbox.reap_idle_sessions( idle_before=idle_before, limit=limit ) for sandbox_session in stopped: # Resolve the tenant for the audit trail. A session whose # workspace has vanished is still stopped, just recorded # without an organization. workspace = await workspaces.get(sandbox_session.workspace_id) await audit.record( event=AuditEvent( action=AuditAction.SANDBOX_REAP, organization_id=workspace.organization_id if workspace else None, target_type="sandbox_session", target_id=sandbox_session.id, metadata_json={ "workspace_id": sandbox_session.workspace_id, "idle_minutes": idle_minutes, "reason": "idle_timeout", }, ) ) outcome.processed += 1 outcome.note(f"stopped session {sandbox_session.id}") # A reaped sandbox is a tenant-visible fact: push it so the # project page's terminal stops showing a live session. await emit_sandbox_reaped( organization_id=workspace.organization_id if workspace else None, session_id=sandbox_session.id, workspace_id=sandbox_session.workspace_id, ) if not stopped: outcome.skipped += 1 return outcome def policy_report(self) -> WorkerOutcome: """Describe the safety envelope this gateway is running under.""" outcome = WorkerOutcome(name=self.name) outcome.note(f"sandbox provider = {settings.sandbox_provider}") outcome.note( f"command timeout = {settings.sandbox_command_timeout_seconds}s " f"(absolute cap enforced in the service layer)" ) outcome.note(f"max output = {settings.sandbox_max_output_bytes} bytes") outcome.note("egress policy default = deny_all (set per resource limit)") return outcome __all__ = ["DEFAULT_IDLE_MINUTES", "SandboxGatewayWorker"]