نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی

مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
mjalmoudy94
2026-10-13 00:39:28 +03:30
commit 717e53a7ce
185 changed files with 39910 additions and 0 deletions
@@ -0,0 +1,92 @@
"""Application-layer ports (driven interfaces).
Every port has at least one *mock* adapter so the whole stack runs with zero
external credentials. Concrete adapters live in ``app.infrastructure.adapters``
and are selected by the ``*_PROVIDER`` environment switches.
"""
from app.application.ports.audit import (
AuditPort,
UsageAggregate,
UsageBreakdown,
UsageLedgerPort,
UsageRecord,
)
from app.application.ports.domain_router import (
DomainRouterPort,
PreviewDomainInfo,
ReverseProxyResult,
)
from app.application.ports.git_provider import (
BranchInfo,
CommitInfo,
GitCredentials,
GitProviderPort,
MergeResult,
PullRequestInfo,
RepoInfo,
RepoStatus,
)
from app.application.ports.model_provider import (
ChatMessage,
ModelHealth,
ModelProviderPort,
ModelResponse,
)
from app.application.ports.payment_provider import (
CheckoutSession,
PaymentProviderPort,
PaymentResult,
)
from app.application.ports.sandbox_provider import (
CommandResult,
SandboxProviderPort,
SessionInfo,
SnapshotInfo,
WorkspaceInfo,
)
from app.application.ports.storage import (
Notification,
NotificationPort,
StoragePort,
StoredObject,
VectorHit,
VectorStorePort,
)
__all__ = [
"AuditPort",
"BranchInfo",
"ChatMessage",
"CheckoutSession",
"CommandResult",
"CommitInfo",
"DomainRouterPort",
"GitCredentials",
"GitProviderPort",
"MergeResult",
"ModelHealth",
"ModelProviderPort",
"ModelResponse",
"Notification",
"NotificationPort",
"PaymentProviderPort",
"PaymentResult",
"PreviewDomainInfo",
"PullRequestInfo",
"RepoInfo",
"RepoStatus",
"ReverseProxyResult",
"SandboxProviderPort",
"SessionInfo",
"SnapshotInfo",
"StoragePort",
"StoredObject",
"UsageAggregate",
"UsageBreakdown",
"UsageLedgerPort",
"UsageRecord",
"VectorHit",
"VectorStorePort",
"WorkspaceInfo",
]
+73
View File
@@ -0,0 +1,73 @@
"""Audit and usage-ledger ports.
``AuditPort`` writes the immutable trail; ``UsageLedgerPort`` is the billing
source of truth. Both are append-only by contract.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Protocol, runtime_checkable
from app.domain.ai_control.models import RateUnit
from app.domain.audit.models import AuditEvent
@dataclass(slots=True, kw_only=True)
class UsageRecord:
organization_id: str
unit: RateUnit
quantity: float
unit_price: float = 0.0
internal_cost_estimate: float = 0.0
total_price: float = 0.0
project_id: str | None = None
user_id: str | None = None
employee_id: str | None = None
run_id: str | None = None
metadata_json: dict = field(default_factory=dict)
@dataclass(slots=True, kw_only=True)
class UsageAggregate:
unit: RateUnit
quantity: float = 0.0
total_price: float = 0.0
internal_cost_estimate: float = 0.0
@dataclass(slots=True, kw_only=True)
class UsageBreakdown:
"""Grouped totals for the billing dashboard."""
by_unit: list[UsageAggregate] = field(default_factory=list)
by_employee: dict[str, float] = field(default_factory=dict)
by_project: dict[str, float] = field(default_factory=dict)
total_price: float = 0.0
total_cost: float = 0.0
@runtime_checkable
class AuditPort(Protocol):
async def record(self, *, event: AuditEvent) -> AuditEvent: ...
async def list_for_organization(
self, *, organization_id: str, limit: int = 100, offset: int = 0
) -> list[AuditEvent]: ...
@runtime_checkable
class UsageLedgerPort(Protocol):
async def record(self, *, usage: UsageRecord) -> None:
"""Append one usage event. Never raises for business reasons — a failed
ledger write is an infrastructure error and must surface loudly."""
...
async def breakdown(
self,
*,
organization_id: str,
since: datetime | None = None,
until: datetime | None = None,
) -> UsageBreakdown: ...
@@ -0,0 +1,55 @@
"""Domain router port — preview subdomains and the port ledger.
Security rules carried over from the current platform (see
``docs/current-infra-notes.md``) are enforced by the *adapters* as well as the
application layer:
* single-label hosts under ``PREVIEW_ROOT_DOMAIN`` only
* label charset ``[a-z0-9-]``, 3-40 chars
* proxy target must be ``localhost`` / ``127.0.0.1`` with a numeric port
* protected platform ports are never allocatable or proxied to
* the Caddy config is validated before it is reloaded, and rolled back on failure
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol, runtime_checkable
@dataclass(slots=True, kw_only=True)
class PreviewDomainInfo:
host: str
port: int
url: str = ""
protected: bool = False
status: str = "active"
@dataclass(slots=True, kw_only=True)
class ReverseProxyResult:
ok: bool
message: str = ""
url: str = ""
@runtime_checkable
class DomainRouterPort(Protocol):
async def allocate_port(
self, *, organization_id: str, project_id: str, service_name: str
) -> int: ...
async def claim_preview_domain(
self,
*,
organization_id: str,
project_id: str,
requested_subdomain: str,
port: int,
) -> PreviewDomainInfo: ...
async def configure_reverse_proxy(self, *, host: str, port: int) -> ReverseProxyResult: ...
async def remove_preview_domain(self, *, domain_id: str) -> None: ...
async def list_project_domains(self, *, project_id: str) -> list[PreviewDomainInfo]: ...
@@ -0,0 +1,124 @@
"""Git provider port — repository lifecycle without leaking the admin token."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Protocol, runtime_checkable
@dataclass(slots=True, kw_only=True)
class RepoInfo:
owner: str
repo_name: str
clone_url: str = ""
web_url: str = ""
default_branch: str = "main"
private: bool = True
@dataclass(slots=True, kw_only=True)
class BranchInfo:
name: str
sha: str = ""
@dataclass(slots=True, kw_only=True)
class CommitInfo:
sha: str
branch: str
message: str = ""
files_changed: int = 0
@dataclass(slots=True, kw_only=True)
class PullRequestInfo:
number: int
url: str
state: str = "open"
@dataclass(slots=True, kw_only=True)
class MergeResult:
merged: bool
message: str = ""
@dataclass(slots=True, kw_only=True)
class RepoStatus:
exists: bool
private: bool = True
default_branch: str = "main"
branches: list[str] = field(default_factory=list)
open_pull_requests: int = 0
@dataclass(slots=True, kw_only=True)
class GitCredentials:
"""Returned only to the application layer. Never serialised to a response."""
username: str
password: str
@runtime_checkable
class GitProviderPort(Protocol):
"""Repository lifecycle.
Identity is passed in, never guessed. A real forge (Gitea) addresses
repositories as ``owner/repo``; the platform stores that pair in
``ProjectRepository``. Passing it down keeps the adapter stateless — it holds
no ``project_id`` mapping that would be lost on restart and would drift from
the database.
The mock adapter ignores ``owner``/``repo_name`` because it keys everything
by ``project_id``; the defaults keep that adapter valid.
"""
async def ensure_project_repo(
self, *, project_id: str, repo_name: str, description: str = ""
) -> RepoInfo: ...
async def ensure_credentials(
self, *, project_id: str, owner: str = "", repo_name: str = ""
) -> GitCredentials: ...
async def create_branch(
self,
*,
project_id: str,
branch_name: str,
from_branch: str,
owner: str = "",
repo_name: str = "",
) -> BranchInfo: ...
async def commit_files(
self,
*,
project_id: str,
branch_name: str,
files: dict[str, str],
message: str,
owner: str = "",
repo_name: str = "",
) -> CommitInfo: ...
async def open_pull_request(
self,
*,
project_id: str,
head: str,
base: str,
title: str,
body: str = "",
owner: str = "",
repo_name: str = "",
) -> PullRequestInfo: ...
async def merge_pull_request(
self, *, project_id: str, pr_number: int, owner: str = "", repo_name: str = ""
) -> MergeResult: ...
async def get_repo_status(
self, *, project_id: str, owner: str = "", repo_name: str = ""
) -> RepoStatus: ...
@@ -0,0 +1,57 @@
"""Model provider port — the only sanctioned way to reach an LLM.
The adapter decides which concrete provider/model is used; the caller passes an
``AiModel`` record and never a provider API key. That keeps model identity
inside the AI Control Plane.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Protocol, runtime_checkable
from app.domain.ai_control.models import AiModel, ModelCredential
@dataclass(slots=True, kw_only=True)
class ChatMessage:
role: str # system | user | assistant | tool
content: str
@dataclass(slots=True, kw_only=True)
class ModelResponse:
content: str
model_internal_name: str = ""
input_tokens: int = 0
output_tokens: int = 0
latency_ms: int = 0
raw: dict = field(default_factory=dict)
@dataclass(slots=True, kw_only=True)
class ModelHealth:
ok: bool
detail: str = ""
@runtime_checkable
class ModelProviderPort(Protocol):
"""Driven port. Implemented by ``MockModelProviderAdapter`` and
``OpenAICompatibleModelAdapter``."""
async def complete(
self,
*,
model: AiModel,
credential: ModelCredential | None,
messages: list[ChatMessage],
system: str = "",
temperature: float = 0.2,
max_tokens: int = 2048,
) -> ModelResponse:
"""Run one completion. Must raise ``ProviderUnavailableError`` on failure."""
...
async def health(self) -> ModelHealth:
...
@@ -0,0 +1,41 @@
"""Payment provider port — an adapter over Stripe (or mock).
The internal usage ledger is the source of truth; this port only moves money.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol, runtime_checkable
@dataclass(slots=True, kw_only=True)
class CheckoutSession:
external_id: str
url: str
amount: float = 0.0
currency: str = "USD"
@dataclass(slots=True, kw_only=True)
class PaymentResult:
ok: bool
external_id: str = ""
message: str = ""
@runtime_checkable
class PaymentProviderPort(Protocol):
async def create_checkout(
self,
*,
organization_id: str,
amount: float,
currency: str,
success_url: str,
cancel_url: str,
) -> CheckoutSession: ...
async def refund(
self, *, external_payment_id: str, amount: float | None = None
) -> PaymentResult: ...
@@ -0,0 +1,75 @@
"""Sandbox provider port — the only path that may execute tenant code.
Implementations: ``LocalMockSandboxAdapter`` (default, executes nothing) and
``DaytonaSandboxAdapter`` / ``E2BSandboxAdapter`` behind ``SANDBOX_PROVIDER``.
Every method that runs something takes an explicit timeout. Callers must never
be able to request "no limit".
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Protocol, runtime_checkable
@dataclass(slots=True, kw_only=True)
class WorkspaceInfo:
external_id: str
status: str = "ready"
raw: dict = field(default_factory=dict)
@dataclass(slots=True, kw_only=True)
class SessionInfo:
external_id: str
status: str = "running"
raw: dict = field(default_factory=dict)
@dataclass(slots=True, kw_only=True)
class CommandResult:
exit_code: int
stdout: str = ""
stderr: str = ""
duration_ms: int = 0
truncated: bool = False
@dataclass(slots=True, kw_only=True)
class SnapshotInfo:
external_id: str
name: str = ""
@runtime_checkable
class SandboxProviderPort(Protocol):
async def create_workspace(
self, *, organization_id: str, project_id: str, template_id: str, image: str
) -> WorkspaceInfo: ...
async def start_session(self, *, workspace_external_id: str) -> SessionInfo: ...
async def run_command(
self,
*,
session_external_id: str,
command: str,
timeout_seconds: int,
max_output_bytes: int,
env: dict[str, str] | None = None,
) -> CommandResult: ...
async def read_file(self, *, session_external_id: str, path: str) -> str: ...
async def write_file(self, *, session_external_id: str, path: str, content: str) -> None: ...
async def snapshot(self, *, session_external_id: str, name: str) -> SnapshotInfo: ...
async def pause(self, *, session_external_id: str) -> None: ...
async def resume(self, *, session_external_id: str) -> SessionInfo: ...
async def destroy(self, *, session_external_id: str) -> None: ...
async def get_preview_url(self, *, session_external_id: str, port: int) -> str: ...
+68
View File
@@ -0,0 +1,68 @@
"""Storage, vector store and notification ports."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Protocol, runtime_checkable
@dataclass(slots=True, kw_only=True)
class StoredObject:
key: str
size_bytes: int = 0
url: str = ""
@runtime_checkable
class StoragePort(Protocol):
async def put(self, *, key: str, data: bytes, content_type: str = "") -> StoredObject: ...
async def get(self, *, key: str) -> bytes: ...
async def delete(self, *, key: str) -> None: ...
async def signed_url(self, *, key: str, expires_seconds: int = 900) -> str: ...
@dataclass(slots=True, kw_only=True)
class VectorHit:
chunk_id: str
document_id: str
content: str
score: float = 0.0
metadata: dict = field(default_factory=dict)
@runtime_checkable
class VectorStorePort(Protocol):
async def upsert(
self,
*,
organization_id: str,
chunks: list[tuple[str, str, list[float]]],
metadata: dict | None = None,
) -> int: ...
async def search(
self,
*,
organization_id: str,
query: str,
limit: int = 5,
project_id: str | None = None,
) -> list[VectorHit]: ...
async def delete_document(self, *, document_id: str) -> None: ...
@dataclass(slots=True, kw_only=True)
class Notification:
kind: str
subject: str
body: str
to: str = ""
@runtime_checkable
class NotificationPort(Protocol):
async def send(self, *, notification: Notification) -> bool: ...