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
+18
View File
@@ -0,0 +1,18 @@
"""Column helpers shared by every ORM module (kept here to avoid import cycles)."""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, String
from sqlalchemy.orm import Mapped, mapped_column
from app.domain.base import new_id, utcnow
def pk() -> Mapped[str]:
return mapped_column(String(32), primary_key=True, default=new_id)
def created() -> Mapped[datetime]:
return mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
+100
View File
@@ -221,6 +221,8 @@ class ProjectRow(Base):
__tablename__ = "projects"
__table_args__ = (
UniqueConstraint("organization_id", "slug", name="uq_project_org_slug"),
# v2 rule: one organization has exactly one application.
UniqueConstraint("organization_id", name="uq_project_one_per_org"),
# The project list is always scoped to one organization and ordered by
# creation, so one composite index serves both the filter and the sort.
Index("ix_project_org_created", "organization_id", "created_at"),
@@ -237,6 +239,94 @@ class ProjectRow(Base):
ForeignKey("users.id"), nullable=True, index=True
)
default_branch: Mapped[str] = mapped_column(String(80), default="main", nullable=False)
# --- v2 application profile (filled by the organization wizard) ---
app_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
language: Mapped[str | None] = mapped_column(String(32), nullable=True)
framework: Mapped[str | None] = mapped_column(String(48), nullable=True)
runtime_version: Mapped[str | None] = mapped_column(String(32), nullable=True)
package_manager: Mapped[str | None] = mapped_column(String(32), nullable=True)
setup_command: Mapped[str | None] = mapped_column(Text, nullable=True)
build_command: Mapped[str | None] = mapped_column(Text, nullable=True)
test_command: Mapped[str | None] = mapped_column(Text, nullable=True)
start_command: Mapped[str | None] = mapped_column(Text, nullable=True)
app_port: Mapped[int | None] = mapped_column(Integer, nullable=True)
healthcheck_path: Mapped[str] = mapped_column(String(255), default="/", nullable=False)
git_source: Mapped[str] = mapped_column(String(16), default="platform", nullable=False)
external_repo_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
wizard_completed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
created_at: Mapped[datetime] = _created()
class ProjectExternalGitRow(Base):
"""Credentials for a user-supplied git remote. Secrets are encrypted at rest."""
__tablename__ = "project_external_git"
project_id: Mapped[str] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), primary_key=True
)
auth_type: Mapped[str] = mapped_column(String(16), default="none", nullable=False)
username: Mapped[str | None] = mapped_column(String(160), nullable=True)
secret_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True)
known_hosts: Mapped[str | None] = mapped_column(Text, nullable=True)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False
)
class DeployTargetRow(Base):
"""A production deploy target (ssh / sftp / ftp / ftps). Secrets are encrypted."""
__tablename__ = "deploy_targets"
__table_args__ = (Index("ix_deploy_target_project", "project_id"),)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
project_id: Mapped[str] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
)
kind: Mapped[str] = mapped_column(String(8), nullable=False)
host: Mapped[str] = mapped_column(String(255), nullable=False)
port: Mapped[int] = mapped_column(Integer, nullable=False)
username: Mapped[str] = mapped_column(String(160), nullable=False)
auth_type: Mapped[str] = mapped_column(String(16), default="password", nullable=False)
secret_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True)
remote_path: Mapped[str] = mapped_column(String(512), nullable=False)
post_deploy_command: Mapped[str | None] = mapped_column(Text, nullable=True)
host_fingerprint: Mapped[str | None] = mapped_column(String(255), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
last_tested_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
last_test_ok: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
last_test_message: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at: Mapped[datetime] = _created()
class OrgModelGrantRow(Base):
"""A model the platform owner lets one organization's employees use."""
__tablename__ = "org_model_grants"
__table_args__ = (
UniqueConstraint("organization_id", "ai_model_id", name="uq_org_model_grant"),
)
id: Mapped[str] = _pk()
organization_id: Mapped[str] = mapped_column(
ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True
)
ai_model_id: Mapped[str] = mapped_column(
ForeignKey("ai_models.id", ondelete="CASCADE"), nullable=False
)
daily_credit_cap: Mapped[float | None] = mapped_column(Float, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_by_user_id: Mapped[str | None] = mapped_column(
ForeignKey("users.id"), nullable=True
)
created_at: Mapped[datetime] = _created()
@@ -926,3 +1016,13 @@ class QualityMetricRow(Base):
value: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
dimensions_json: Mapped[dict] = mapped_column(JSON, default=dict, nullable=False)
created_at: Mapped[datetime] = _created()
# ---------------------------------------------------------------------------
# v2 tables owned by one workstream each (kept in separate modules so the three
# parallel agents never edit the same file). Imported last so every table is
# registered on ``Base.metadata`` before ``create_all`` / alembic autogenerate.
# ---------------------------------------------------------------------------
from app.infrastructure.db import models_delivery as _models_delivery # noqa: E402,F401
from app.infrastructure.db import models_engine as _models_engine # noqa: E402,F401
from app.infrastructure.db import models_org as _models_org # noqa: E402,F401
@@ -0,0 +1,10 @@
"""ORM tables owned by Agent 3 (delivery & infra).
Use ``from app.core.database import Base`` and the helpers in
``app.infrastructure.db.columns`` (``pk()``, ``created()``). Do NOT import from
``app.infrastructure.db.models`` here: that module imports this one (import cycle).
Foreign keys to existing tables are written as plain strings, e.g.
``ForeignKey("organizations.id", ondelete="CASCADE")``. See docs/plan/PLAN-v2.md §6.
"""
from __future__ import annotations
@@ -0,0 +1,10 @@
"""ORM tables owned by Agent 2 (AI workforce engine).
Use ``from app.core.database import Base`` and the helpers in
``app.infrastructure.db.columns`` (``pk()``, ``created()``). Do NOT import from
``app.infrastructure.db.models`` here: that module imports this one (import cycle).
Foreign keys to existing tables are written as plain strings, e.g.
``ForeignKey("organizations.id", ondelete="CASCADE")``. See docs/plan/PLAN-v2.md §6.
"""
from __future__ import annotations
@@ -0,0 +1,10 @@
"""ORM tables owned by Agent 1 (org platform).
Use ``from app.core.database import Base`` and the helpers in
``app.infrastructure.db.columns`` (``pk()``, ``created()``). Do NOT import from
``app.infrastructure.db.models`` here: that module imports this one (import cycle).
Foreign keys to existing tables are written as plain strings, e.g.
``ForeignKey("organizations.id", ondelete="CASCADE")``. See docs/plan/PLAN-v2.md §6.
"""
from __future__ import annotations
@@ -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)
)