v2 foundation: shared contracts, app profile schema, wiring stubs

- projects: app profile columns + one app per org; project_external_git,
  deploy_targets, org_model_grants tables (migration a0f0e1d2c3b4)
- ports + fakes: RepoWorkspace, DeliveryHooks, DeployTarget, Memory,
  ModelGrants, WorkforceBootstrap; AppProfile + AppProfileRepository
- per-workstream wiring (container_org/engine/delivery), config blocks,
  empty routers, delivery worker, MCP/delivery service shells, web stubs
- admin nav derives from ADMIN_ITEMS; plan docs updated for ext4 worktrees

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mjalmoudy94
2026-10-13 23:21:17 +03:30
co-authored by Claude Sonnet 5.5
parent 3a68b0fa07
commit 0cd9194b64
58 changed files with 1576 additions and 26 deletions
@@ -0,0 +1,82 @@
"""Read/write access to the app profile columns of ``projects``.
Foundation-owned and shared by all three workstreams (agents may call it, not edit it).
Every lookup is scoped by ``organization_id``.
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.projects.profile import AppProfile
from app.infrastructure.db.models import ProjectRow
#: Columns a caller may change through ``update_profile``.
EDITABLE_FIELDS = frozenset(
{
"app_type",
"language",
"framework",
"runtime_version",
"package_manager",
"setup_command",
"build_command",
"test_command",
"start_command",
"app_port",
"healthcheck_path",
"git_source",
"external_repo_url",
"default_branch",
}
)
class AppProfileRepository:
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def get_for_org(self, organization_id: str) -> AppProfile | None:
row = await self._row_for_org(organization_id)
return AppProfile.from_row(row) if row else None
async def get(self, *, organization_id: str, project_id: str) -> AppProfile | None:
row = await self._session.scalar(
select(ProjectRow).where(
ProjectRow.id == project_id, ProjectRow.organization_id == organization_id
)
)
return AppProfile.from_row(row) if row else None
async def update_profile(
self,
*,
organization_id: str,
project_id: str,
values: dict[str, object],
mark_wizard_completed: datetime | None = None,
) -> AppProfile | None:
unknown = set(values) - EDITABLE_FIELDS
if unknown:
raise ValueError(f"not editable profile fields: {sorted(unknown)}")
row = await self._session.scalar(
select(ProjectRow).where(
ProjectRow.id == project_id, ProjectRow.organization_id == organization_id
)
)
if row is None:
return None
for name, value in values.items():
setattr(row, name, value)
if mark_wizard_completed is not None:
row.wizard_completed_at = mark_wizard_completed
await self._session.flush()
return AppProfile.from_row(row)
async def _row_for_org(self, organization_id: str) -> ProjectRow | None:
return await self._session.scalar(
select(ProjectRow).where(ProjectRow.organization_id == organization_id)
)