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:
co-authored by
Claude Sonnet 5.5
parent
3a68b0fa07
commit
0cd9194b64
@@ -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)
|
||||
@@ -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)
|
||||
)
|
||||
Reference in New Issue
Block a user