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,48 @@
"""Delivery hooks — how the org wizard and the engine tell the delivery side to act.
Implemented by the delivery workstream; called by the org-platform workstream (app created)
and the engine (branch pushed). FROZEN contract: see docs/plan/PLAN-v2.md §5.
"""
from __future__ import annotations
from typing import Protocol, runtime_checkable
@runtime_checkable
class DeliveryHooks(Protocol):
async def on_app_created(self, *, organization_id: str, project_id: str) -> None: ...
async def on_branch_pushed(
self,
*,
organization_id: str,
project_id: str,
branch: str,
sha: str,
source: str, # "engine" | "poll" | "manual"
) -> None: ...
class NullDeliveryHooks:
"""Records calls and does nothing else (default until the real hooks are wired)."""
def __init__(self) -> None:
self.created: list[dict] = []
self.pushed: list[dict] = []
async def on_app_created(self, *, organization_id: str, project_id: str) -> None:
self.created.append({"organization_id": organization_id, "project_id": project_id})
async def on_branch_pushed(
self, *, organization_id: str, project_id: str, branch: str, sha: str, source: str
) -> None:
self.pushed.append(
{
"organization_id": organization_id,
"project_id": project_id,
"branch": branch,
"sha": sha,
"source": source,
}
)
@@ -0,0 +1,51 @@
"""Deploy target tester — checks an SSH/SFTP/FTP/FTPS production target.
Implemented by the delivery workstream; called by the org wizard. FROZEN contract:
see docs/plan/PLAN-v2.md §5.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol, runtime_checkable
DEPLOY_KINDS = ("ssh", "sftp", "ftp", "ftps")
@dataclass(slots=True, kw_only=True)
class DeployTargetSpec:
kind: str
host: str
port: int
username: str
auth_type: str = "password" # password | key
secret: str | None = None # plaintext, in memory for one call only
remote_path: str = "/"
known_fingerprint: str | None = None
@dataclass(slots=True, kw_only=True)
class DeployCheck:
ok: bool
#: Plain-language Persian message; never contains credentials.
message: str = ""
#: Host key fingerprint seen (SSH/SFTP), to be pinned on first success.
fingerprint: str | None = None
@runtime_checkable
class DeployTargetTester(Protocol):
async def test_connection(self, *, target: DeployTargetSpec) -> DeployCheck: ...
class FakeDeployTargetTester:
"""OK unless the host contains ``fail``."""
def __init__(self) -> None:
self.calls: list[DeployTargetSpec] = []
async def test_connection(self, *, target: DeployTargetSpec) -> DeployCheck:
self.calls.append(target)
if "fail" in target.host:
return DeployCheck(ok=False, message="اتصال برقرار نشد")
return DeployCheck(ok=True, message="اتصال موفق", fingerprint="SHA256:fake")
+76
View File
@@ -0,0 +1,76 @@
"""Org memory port — the self-improving memory of an organization.
Implemented by the org-platform workstream; used by the engine. FROZEN contract:
see docs/plan/PLAN-v2.md §5.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Protocol, runtime_checkable
MEMORY_KINDS = ("rule", "preference", "lesson", "fact", "code_map")
MEMORY_SOURCES = ("user", "manager", "system", "reflection")
@dataclass(slots=True, kw_only=True)
class TaskOutcome:
task_id: str
title: str
status: str # completed | failed | cancelled
summary: str = ""
changed_files: list[str] = field(default_factory=list)
cycles: int = 0
gate_failures: list[str] = field(default_factory=list)
@runtime_checkable
class MemoryPort(Protocol):
async def build_context(
self, *, organization_id: str, role: str, query: str, budget_chars: int = 2500
) -> str: ...
async def record(
self,
*,
organization_id: str,
kind: str,
content: str,
source: str,
evidence_task_id: str | None = None,
key: str | None = None,
) -> str | None: ...
async def on_task_finished(self, *, organization_id: str, task: TaskOutcome) -> None: ...
class NullMemory:
"""No memory: empty context, nothing stored (default until the real service is wired)."""
def __init__(self) -> None:
self.recorded: list[dict] = []
self.finished: list[TaskOutcome] = []
async def build_context(
self, *, organization_id: str, role: str, query: str, budget_chars: int = 2500
) -> str:
return ""
async def record(
self,
*,
organization_id: str,
kind: str,
content: str,
source: str,
evidence_task_id: str | None = None,
key: str | None = None,
) -> str | None:
self.recorded.append(
{"organization_id": organization_id, "kind": kind, "content": content,
"source": source, "evidence_task_id": evidence_task_id, "key": key}
)
return None
async def on_task_finished(self, *, organization_id: str, task: TaskOutcome) -> None:
self.finished.append(task)
@@ -0,0 +1,50 @@
"""Model grants port — which models the platform owner lets an organization use.
Implemented by the org-platform workstream; used by the engine. FROZEN contract:
see docs/plan/PLAN-v2.md §5. ``tier`` is a neutral label; users never see model names.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Protocol, runtime_checkable
TIERS = ("fast", "balanced", "strong")
@dataclass(slots=True, frozen=True, kw_only=True)
class GrantedModel:
ai_model_id: str
tier: str = "balanced" # fast | balanced | strong
capabilities: tuple[str, ...] = field(default_factory=tuple)
daily_credit_cap: float | None = None
def tier_for(*, capabilities: list[str] | tuple[str, ...], output_price_per_1m: float) -> str:
"""Neutral tier label from capabilities and price (never from the model name)."""
if "reasoning" in capabilities:
return "strong"
return "fast" if output_price_per_1m < 2.0 else "balanced"
@runtime_checkable
class ModelGrantPort(Protocol):
async def granted_models(self, *, organization_id: str) -> list[GrantedModel]: ...
async def is_granted(self, *, organization_id: str, ai_model_id: str) -> bool: ...
class AllActiveModelsGrants:
"""Today's behaviour: every active model is usable by every organization.
Reads active models through a callable so the application layer needs no ORM import.
"""
def __init__(self, list_active_models) -> None: # noqa: ANN001 - async callable
self._list_active = list_active_models
async def granted_models(self, *, organization_id: str) -> list[GrantedModel]:
return list(await self._list_active())
async def is_granted(self, *, organization_id: str, ai_model_id: str) -> bool:
return any(m.ai_model_id == ai_model_id for m in await self._list_active())
@@ -0,0 +1,185 @@
"""Repo workspace port — the PLATFORM runs git on the host; the AI never does.
Implemented by the delivery workstream (``LocalGitWorkspaceAdapter``), used by the engine.
``FakeRepoWorkspace`` is an in-memory implementation for tests and for wiring before the
real adapter exists. FROZEN contract: see docs/plan/PLAN-v2.md §5.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Protocol, runtime_checkable
@dataclass(slots=True, frozen=True, kw_only=True)
class WorkspaceRef:
project_id: str
host_path: str
sandbox_path: str = "/workspace"
default_branch: str = "dev"
@dataclass(slots=True, frozen=True, kw_only=True)
class WorktreeRef:
project_id: str
task_id: str
branch: str
host_path: str
#: Path of this worktree INSIDE the workspace container.
sandbox_path: str
@dataclass(slots=True, kw_only=True)
class DiffResult:
text: str = ""
changed_files: list[str] = field(default_factory=list)
#: Paths of symlinks that point outside the repo (guard input).
escaping_symlinks: list[str] = field(default_factory=list)
largest_file_bytes: int = 0
@property
def is_empty(self) -> bool:
return not self.changed_files
@dataclass(slots=True, kw_only=True)
class MergeOutcome:
ok: bool
sha: str | None = None
conflict_files: list[str] = field(default_factory=list)
#: True when the base moved, so the engine must re-run the tests.
base_changed: bool = False
message: str = ""
@dataclass(slots=True, kw_only=True)
class PushOutcome:
ok: bool
sha: str | None = None
rejected: bool = False
message: str = ""
@dataclass(slots=True, kw_only=True)
class RemoteCredentials:
auth_type: str = "none" # none | token | ssh_key
username: str | None = None
secret: str | None = None # plaintext, only ever held in memory for one call
@dataclass(slots=True, kw_only=True)
class RemoteCheck:
ok: bool
default_branch: str | None = None
branches: list[str] = field(default_factory=list)
#: Plain-language Persian message; never contains credentials or raw stderr.
message: str = ""
@runtime_checkable
class RepoWorkspacePort(Protocol):
async def ensure_workspace(self, *, project_id: str) -> WorkspaceRef: ...
async def create_task_worktree(
self, *, project_id: str, task_id: str, base: str = "dev"
) -> WorktreeRef: ...
async def diff(self, *, worktree: WorktreeRef) -> DiffResult: ...
async def commit(self, *, worktree: WorktreeRef, message: str, author: str) -> str | None: ...
async def sync_with_base(self, *, worktree: WorktreeRef) -> MergeOutcome: ...
async def merge_into(
self, *, project_id: str, branch: str, target: str = "dev"
) -> MergeOutcome: ...
async def push(self, *, project_id: str, branch: str) -> PushOutcome: ...
async def remove_worktree(self, *, worktree: WorktreeRef) -> None: ...
async def head(self, *, project_id: str, branch: str) -> str | None: ...
async def test_remote(self, *, url: str, credentials: RemoteCredentials) -> RemoteCheck: ...
class FakeRepoWorkspace:
"""In-memory ``RepoWorkspacePort`` that records every call.
Tests can pre-load ``diffs`` (task_id → DiffResult), ``merge_outcomes`` /
``push_outcomes`` (queues consumed in order) and ``remote_checks`` (url → RemoteCheck).
"""
def __init__(self) -> None:
self.calls: list[tuple[str, dict]] = []
self.diffs: dict[str, DiffResult] = {}
self.merge_outcomes: list[MergeOutcome] = []
self.sync_outcomes: list[MergeOutcome] = []
self.push_outcomes: list[PushOutcome] = []
self.remote_checks: dict[str, RemoteCheck] = {}
self.heads: dict[tuple[str, str], str] = {}
self._commits = 0
def _rec(self, name: str, **kw: object) -> None:
self.calls.append((name, kw))
async def ensure_workspace(self, *, project_id: str) -> WorkspaceRef:
self._rec("ensure_workspace", project_id=project_id)
return WorkspaceRef(project_id=project_id, host_path=f"/fake/{project_id}/work")
async def create_task_worktree(
self, *, project_id: str, task_id: str, base: str = "dev"
) -> WorktreeRef:
self._rec("create_task_worktree", project_id=project_id, task_id=task_id, base=base)
return WorktreeRef(
project_id=project_id,
task_id=task_id,
branch=f"task/{task_id}",
host_path=f"/fake/{project_id}/work/.worktrees/{task_id}",
sandbox_path=f"/workspace/.worktrees/{task_id}",
)
async def diff(self, *, worktree: WorktreeRef) -> DiffResult:
self._rec("diff", task_id=worktree.task_id)
return self.diffs.get(worktree.task_id, DiffResult())
async def commit(self, *, worktree: WorktreeRef, message: str, author: str) -> str | None:
self._rec("commit", task_id=worktree.task_id, message=message, author=author)
if self.diffs.get(worktree.task_id, DiffResult()).is_empty:
return None
self._commits += 1
sha = f"{self._commits:040x}"
self.heads[(worktree.project_id, worktree.branch)] = sha
return sha
async def sync_with_base(self, *, worktree: WorktreeRef) -> MergeOutcome:
self._rec("sync_with_base", task_id=worktree.task_id)
return self.sync_outcomes.pop(0) if self.sync_outcomes else MergeOutcome(ok=True)
async def merge_into(
self, *, project_id: str, branch: str, target: str = "dev"
) -> MergeOutcome:
self._rec("merge_into", project_id=project_id, branch=branch, target=target)
if self.merge_outcomes:
return self.merge_outcomes.pop(0)
sha = self.heads.get((project_id, branch)) or "0" * 40
self.heads[(project_id, target)] = sha
return MergeOutcome(ok=True, sha=sha)
async def push(self, *, project_id: str, branch: str) -> PushOutcome:
self._rec("push", project_id=project_id, branch=branch)
if self.push_outcomes:
return self.push_outcomes.pop(0)
return PushOutcome(ok=True, sha=self.heads.get((project_id, branch)))
async def remove_worktree(self, *, worktree: WorktreeRef) -> None:
self._rec("remove_worktree", task_id=worktree.task_id)
async def head(self, *, project_id: str, branch: str) -> str | None:
return self.heads.get((project_id, branch))
async def test_remote(self, *, url: str, credentials: RemoteCredentials) -> RemoteCheck:
self._rec("test_remote", url=url, auth_type=credentials.auth_type)
return self.remote_checks.get(
url, RemoteCheck(ok=True, default_branch="main", branches=["main"])
)
@@ -0,0 +1,22 @@
"""Workforce bootstrap port — creates the starting AI team of a new organization.
Implemented by the engine workstream; called by the org wizard after commit.
FROZEN contract: see docs/plan/PLAN-v2.md §5.
"""
from __future__ import annotations
from typing import Protocol, runtime_checkable
@runtime_checkable
class WorkforceBootstrapPort(Protocol):
async def bootstrap_team(self, *, organization_id: str, project_id: str) -> None: ...
class NullWorkforceBootstrap:
def __init__(self) -> None:
self.calls: list[dict] = []
async def bootstrap_team(self, *, organization_id: str, project_id: str) -> None:
self.calls.append({"organization_id": organization_id, "project_id": project_id})