feat(integration): wire v2 workstreams together (engine policy/budget, sandbox, tenant checks, order route, docs)

Engine honours org policy and monthly budget, legacy runner skips engine tasks,
sandbox session provisioned on demand, project fallback for threads, reviewer
fallback, tenant checks on project ids, memory reflection respects grants,
expiry worker, MCP pipeline_status, login redirect, sharing settings, e2e smoke.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mjalmoudy94
2026-10-14 20:57:56 +03:30
co-authored by Claude Sonnet 5.5
parent 36564e3f2b
commit fef5a9deab
43 changed files with 3024 additions and 221 deletions
+76 -10
View File
@@ -13,7 +13,8 @@ from typing import TYPE_CHECKING
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.ports.workforce_bootstrap import WorkforceBootstrapPort
from app.domain.sandbox.models import SessionStatus
from app.core.errors import AppError
from app.domain.sandbox.models import SessionStatus, WorkspaceStatus
from app.domain.workforce.models import Task
if TYPE_CHECKING:
@@ -87,11 +88,13 @@ def build_workforce_bootstrap(session: AsyncSession) -> WorkforceBootstrapPort:
class _EngineSandboxSessions:
"""The sandbox session the engine's turns run in.
The engine does not *create* sandboxes: a workspace and a running session
belong to the project and are started through the sandbox flow. The engine
only reuses the active session of the task's project. When there is none it
returns ``""``, and the tests gate treats that as red and sends the work
back — never "merge untested because there was no container".
The engine reuses the active session of the task's project. When there is
none, it starts the project's workspace session itself (see ``_provision``):
a fresh organization cannot start a sandbox through the API, because sandbox
mutations are owner-only, and without a session every tool turn fails. When
the session cannot be started it returns ``""``, and the tests gate treats
that as red and sends the work back — never "merge untested because there
was no container".
"""
#: A session in one of these states can still run a command.
@@ -107,6 +110,7 @@ class _EngineSandboxSessions:
self.session = session
async def for_task(self, task: Task) -> str:
from app.infrastructure.repositories.projects import ProjectRepository
from app.infrastructure.repositories.sandbox import (
SandboxSessionRepository,
WorkspaceRepository,
@@ -114,9 +118,12 @@ class _EngineSandboxSessions:
if not task.project_id:
return ""
workspaces = await WorkspaceRepository(self.session).list_for_project(
task.project_id
)
# A task only ever runs in its own organization's project, whatever the row says.
own = await ProjectRepository(self.session).list_for_org(task.organization_id)
if task.project_id not in {project.id for project in own}:
return ""
found = await WorkspaceRepository(self.session).list_for_project(task.project_id)
workspaces = [w for w in found if w.organization_id == task.organization_id]
sessions = SandboxSessionRepository(self.session)
for workspace in workspaces:
for sandbox_session in await sessions.list_for_workspace(workspace.id):
@@ -125,7 +132,66 @@ class _EngineSandboxSessions:
and sandbox_session.external_session_id
):
return sandbox_session.external_session_id
return ""
return await self._provision(task)
async def _provision(self, task: Task) -> str:
"""Start the project's workspace session and return its id, or ``""``.
A project keeps one workspace: the newest one that is not destroyed is
reused, and only a project with none gets a new workspace, built from the
platform template for the configured provider. A failure is recorded and
never raised, so the turn runs without a container and the gates say so.
"""
from app.application.container import build_sandbox_service
from app.core.config import settings
from app.infrastructure.error_log import record_error
from app.infrastructure.repositories.sandbox import (
SandboxTemplateRepository,
WorkspaceRepository,
)
project_id = task.project_id
if not project_id:
return ""
metadata = {"task_id": task.id, "project_id": project_id}
service = build_sandbox_service(self.session)
try:
found = await WorkspaceRepository(self.session).list_for_project(project_id)
workspaces = [w for w in found if w.organization_id == task.organization_id]
workspace = next(
(w for w in workspaces if w.status != WorkspaceStatus.DESTROYED), None
)
if workspace is None:
templates = await SandboxTemplateRepository(self.session).list()
if not templates:
await record_error(
kind="engine.sandbox_provision_failed",
message="no sandbox template is seeded",
organization_id=task.organization_id,
metadata=metadata,
)
return ""
preferred = [
t for t in templates if str(t.provider) == settings.sandbox_provider
]
workspace = await service.create_workspace(
organization_id=task.organization_id,
project_id=project_id,
template_id=(preferred or templates)[0].id,
)
started = await service.start_session(
organization_id=task.organization_id, workspace_id=workspace.id
)
except AppError as exc:
await record_error(
kind="engine.sandbox_provision_failed",
message=type(exc).__name__,
exc=exc,
organization_id=task.organization_id,
metadata=metadata,
)
return ""
return started.external_session_id or ""
def _build_manager(