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

مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ 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
View File
+109
View File
@@ -0,0 +1,109 @@
"""AI Control Plane domain: providers, credentials, models, routing, rate cards.
Everything here is platform-owner-only. A normal customer never receives these
records through the API — the application layer strips them.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from app.domain.base import new_id, utcnow
class ProviderType(StrEnum):
MOCK = "mock"
OPENAI_COMPATIBLE = "openai_compatible"
ANTHROPIC = "anthropic"
GEMINI = "gemini"
OPENROUTER = "openrouter"
class RoutingMode(StrEnum):
FAST = "fast"
CHEAP = "cheap"
BALANCED = "balanced"
REASONING = "reasoning"
VISION = "vision"
CODING = "coding"
class Capability(StrEnum):
TEXT = "text"
VISION = "vision"
TOOLS = "tools"
REASONING = "reasoning"
LONG_CONTEXT = "long_context"
CODE = "code"
class RateUnit(StrEnum):
MODEL_INPUT_TOKENS = "model_input_tokens"
MODEL_OUTPUT_TOKENS = "model_output_tokens"
SANDBOX_CPU_SECOND = "sandbox_cpu_second"
SANDBOX_MEMORY_GB_SECOND = "sandbox_memory_gb_second"
STORAGE_GB_DAY = "storage_gb_day"
TOOL_CALL = "tool_call"
PREMIUM_EMPLOYEE_RUN = "premium_employee_run"
@dataclass(slots=True, kw_only=True)
class Provider:
name: str
type: ProviderType = ProviderType.MOCK
base_url: str = ""
id: str = field(default_factory=new_id)
is_active: bool = True
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class ModelCredential:
provider_id: str
name: str
encrypted_api_key: str
id: str = field(default_factory=new_id)
is_active: bool = True
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class AiModel:
provider_id: str
internal_name: str
display_name_for_admin: str
capabilities: list[str] = field(default_factory=list)
input_price_per_1m: float = 0.0
output_price_per_1m: float = 0.0
id: str = field(default_factory=new_id)
is_active: bool = True
created_at: datetime = field(default_factory=utcnow)
def supports(self, capability: str | Capability) -> bool:
return str(capability) in set(self.capabilities)
@dataclass(slots=True, kw_only=True)
class RoutingPolicy:
name: str
mode: RoutingMode = RoutingMode.BALANCED
description: str = ""
fallback_policy_json: dict = field(default_factory=dict)
budget_limits_json: dict = field(default_factory=dict)
id: str = field(default_factory=new_id)
is_default: bool = False
is_active: bool = True
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class RateCard:
name: str
unit: RateUnit
customer_price: float
internal_cost_estimate: float = 0.0
id: str = field(default_factory=new_id)
is_active: bool = True
created_at: datetime = field(default_factory=utcnow)
+153
View File
@@ -0,0 +1,153 @@
"""Observability & Audit domain: the immutable trail of who did what."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from app.domain.base import new_id, utcnow
class AuditAction:
"""Well-known auditable actions. Free-form strings are allowed but these are
the ones the application layer emits so far."""
LOGIN = "auth.login"
LOGOUT = "auth.logout"
SIGNUP = "auth.signup"
ORG_CREATE = "organization.create"
ORG_DELETE = "organization.delete"
MEMBER_INVITE = "member.invite"
MEMBER_ROLE_CHANGE = "member.role_change"
MEMBER_REMOVE = "member.remove"
EMPLOYEE_CREATE = "employee.create"
EMPLOYEE_UPDATE = "employee.update"
REPO_CREATE = "repo.create"
REPO_DELETE = "repo.delete"
DOMAIN_CREATE = "domain.create"
DOMAIN_DELETE = "domain.delete"
SANDBOX_COMMAND = "sandbox.command"
SANDBOX_DESTROY = "sandbox.destroy"
#: A platform worker stopped a sandbox session that had gone idle. System
#: action, not a tenant one — the actor is the platform, not a user.
SANDBOX_REAP = "sandbox.reap"
APPROVAL_DECISION = "approval.decision"
BILLING_CREDIT = "billing.credit"
BILLING_PAYMENT = "billing.payment"
PROVIDER_CHANGE = "provider.change"
MODEL_CHANGE = "model.change"
ROUTING_CHANGE = "routing.change"
PROJECT_CREATE = "project.create"
PROJECT_DELETE = "project.delete"
TASK_CREATE = "task.create"
RUN_CANCEL = "run.cancel"
#: A platform owner triaged an error in the console. Auditable so "who
#: decided this bug was fixed?" has an answer.
ERROR_RESOLVE = "error.resolve"
@dataclass(slots=True, kw_only=True)
class AuditEvent:
action: str
organization_id: str | None = None
actor_user_id: str | None = None
target_type: str = ""
target_id: str = ""
metadata_json: dict = field(default_factory=dict)
ip_address: str = ""
user_agent: str = ""
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Trace:
organization_id: str
name: str = ""
run_id: str | None = None
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Span:
trace_id: str
name: str
kind: str = "internal"
parent_span_id: str | None = None
attributes_json: dict = field(default_factory=dict)
id: str = field(default_factory=new_id)
started_at: datetime = field(default_factory=utcnow)
ended_at: datetime | None = None
@property
def duration_ms(self) -> int | None:
if self.ended_at is None:
return None
return int((self.ended_at - self.started_at).total_seconds() * 1000)
@dataclass(slots=True, kw_only=True)
class ErrorEvent:
"""A persisted failure, recorded so it can be diagnosed later.
Deliberately *not* a domain aggregate: nothing in the business rules depends
on it. It is an operational record, which is why it carries transport-level
detail (method, path, status code) that no domain model would ever hold.
"""
kind: str
message: str
organization_id: str | None = None
run_id: str | None = None
stack: str = ""
metadata_json: dict = field(default_factory=dict)
#: Where the failure happened: ``http``, ``worker`` or ``startup``.
source: str = "http"
#: ``error`` for anything unexpected, ``warning`` for expected 4xx noise,
#: ``critical`` for startup/infrastructure failures.
severity: str = "error"
#: Exception class name (``ValueError``, ``IntegrityError``, …). Empty when
#: the failure was not an exception.
error_type: str = ""
#: Stable hash of the failure's identity, so a thousand occurrences of the
#: same bug group into one row in the console instead of a thousand.
fingerprint: str = ""
status_code: int | None = None
method: str = ""
path: str = ""
user_id: str | None = None
#: Correlates this record with the response the caller received and with
#: every log line written while handling the same request.
request_id: str = ""
ip: str = ""
user_agent: str = ""
resolved_at: datetime | None = None
resolved_note: str = ""
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
class ErrorSource:
"""Where an error was caught. Kept as strings so a new source needs no
migration."""
HTTP = "http"
WORKER = "worker"
STARTUP = "startup"
class ErrorSeverity:
ERROR = "error"
WARNING = "warning"
CRITICAL = "critical"
@dataclass(slots=True, kw_only=True)
class QualityMetric:
organization_id: str
name: str
value: float
dimensions_json: dict = field(default_factory=dict)
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
+49
View File
@@ -0,0 +1,49 @@
"""Shared domain primitives.
Pure Python — no framework, no I/O. The domain layer must stay importable
without FastAPI, SQLAlchemy or a database connection.
"""
from __future__ import annotations
import re
import uuid
from datetime import UTC, datetime
_SLUG_RE = re.compile(r"[^a-z0-9]+")
def new_id() -> str:
"""Opaque, URL-safe entity id (portable across SQLite and PostgreSQL)."""
return uuid.uuid4().hex
def utcnow() -> datetime:
return datetime.now(UTC)
def slugify(value: str, *, max_length: int = 48) -> str:
"""ASCII slug. Non-ASCII input (e.g. Persian) collapses to a stable fallback.
Persian organization names do not transliterate, so we keep a short random
suffix appended by the caller when the slug would be empty.
"""
slug = _SLUG_RE.sub("-", value.strip().lower()).strip("-")
slug = slug[:max_length].strip("-")
if not slug:
slug = f"x{uuid.uuid4().hex[:8]}"
return slug
def ensure_unique_slug(base: str, taken: set[str], *, max_length: int = 48) -> str:
"""Append ``-2``, ``-3`` ... until the slug is free."""
candidate = base[:max_length]
if candidate not in taken:
return candidate
n = 2
while True:
suffix = f"-{n}"
candidate = f"{base[: max_length - len(suffix)]}{suffix}"
if candidate not in taken:
return candidate
n += 1
+127
View File
@@ -0,0 +1,127 @@
"""Billing & Usage domain.
The internal usage ledger is the source of truth. Stripe (or any other payment
provider) is an *adapter* that mirrors it — never the other way around.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from app.domain.ai_control.models import RateUnit
from app.domain.base import new_id, utcnow
class SubscriptionStatus(StrEnum):
TRIALING = "trialing"
ACTIVE = "active"
PAST_DUE = "past_due"
CANCELED = "canceled"
class InvoiceStatus(StrEnum):
DRAFT = "draft"
OPEN = "open"
PAID = "paid"
VOID = "void"
UNCOLLECTIBLE = "uncollectible"
class PaymentStatus(StrEnum):
PENDING = "pending"
SUCCEEDED = "succeeded"
FAILED = "failed"
REFUNDED = "refunded"
@dataclass(slots=True, kw_only=True)
class Plan:
name: str
monthly_price: float = 0.0
included_credit: float = 0.0
features_json: dict = field(default_factory=dict)
id: str = field(default_factory=new_id)
is_active: bool = True
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Subscription:
organization_id: str
plan_id: str
status: SubscriptionStatus = SubscriptionStatus.ACTIVE
external_subscription_id: str | None = None
current_period_end: datetime | None = None
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class CreditWallet:
organization_id: str
balance: float = 0.0
currency: str = "USD"
low_balance_threshold: float = 5.0
id: str = field(default_factory=new_id)
updated_at: datetime = field(default_factory=utcnow)
@property
def is_low(self) -> bool:
return self.balance <= self.low_balance_threshold
def can_afford(self, amount: float) -> bool:
return self.balance >= amount
def debit(self, amount: float) -> None:
self.balance -= amount
self.updated_at = utcnow()
def credit(self, amount: float) -> None:
self.balance += amount
self.updated_at = utcnow()
@dataclass(slots=True, kw_only=True)
class UsageEvent:
"""One billable unit of work. Append-only."""
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)
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Invoice:
organization_id: str
amount: float
status: InvoiceStatus = InvoiceStatus.DRAFT
currency: str = "USD"
external_invoice_id: str | None = None
period_start: datetime | None = None
period_end: datetime | None = None
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Payment:
organization_id: str
amount: float
status: PaymentStatus = PaymentStatus.PENDING
provider: str = "mock"
external_payment_id: str | None = None
invoice_id: str | None = None
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
+149
View File
@@ -0,0 +1,149 @@
"""Identity & Access domain: users, organizations, memberships, invites."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from app.domain.base import new_id, utcnow
class Role(StrEnum):
OWNER = "owner"
ADMIN = "admin"
MANAGER = "manager"
MEMBER = "member"
VIEWER = "viewer"
#: Higher rank implies every permission of the lower ranks.
ROLE_RANK: dict[Role, int] = {
Role.VIEWER: 10,
Role.MEMBER: 20,
Role.MANAGER: 30,
Role.ADMIN: 40,
Role.OWNER: 50,
}
#: Capabilities used across the application layer.
CAN_MANAGE_ORG = "org.manage"
CAN_MANAGE_MEMBERS = "org.members"
CAN_MANAGE_PROJECTS = "projects.manage"
CAN_MANAGE_EMPLOYEES = "employees.manage"
CAN_RUN_TASKS = "tasks.run"
CAN_VIEW_BILLING = "billing.view"
CAN_MANAGE_BILLING = "billing.manage"
CAN_VIEW_AUDIT = "audit.view"
CAN_USE_SANDBOX = "sandbox.use"
_ROLE_CAPABILITIES: dict[Role, set[str]] = {
Role.VIEWER: set(),
Role.MEMBER: {CAN_RUN_TASKS, CAN_VIEW_BILLING},
Role.MANAGER: {
CAN_RUN_TASKS,
CAN_VIEW_BILLING,
CAN_MANAGE_PROJECTS,
CAN_MANAGE_EMPLOYEES,
CAN_USE_SANDBOX,
},
Role.ADMIN: {
CAN_RUN_TASKS,
CAN_VIEW_BILLING,
CAN_MANAGE_BILLING,
CAN_MANAGE_PROJECTS,
CAN_MANAGE_EMPLOYEES,
CAN_USE_SANDBOX,
CAN_MANAGE_MEMBERS,
CAN_VIEW_AUDIT,
},
Role.OWNER: {
CAN_RUN_TASKS,
CAN_VIEW_BILLING,
CAN_MANAGE_BILLING,
CAN_MANAGE_PROJECTS,
CAN_MANAGE_EMPLOYEES,
CAN_USE_SANDBOX,
CAN_MANAGE_MEMBERS,
CAN_VIEW_AUDIT,
CAN_MANAGE_ORG,
},
}
def role_has_capability(role: Role, capability: str) -> bool:
return capability in _ROLE_CAPABILITIES.get(role, set())
class MembershipStatus(StrEnum):
ACTIVE = "active"
INVITED = "invited"
SUSPENDED = "suspended"
@dataclass(slots=True, kw_only=True)
class User:
email: str
password_hash: str
name: str = ""
id: str = field(default_factory=new_id)
avatar_url: str | None = None
is_platform_owner: bool = False
is_active: bool = True
created_at: datetime = field(default_factory=utcnow)
updated_at: datetime = field(default_factory=utcnow)
@property
def display_name(self) -> str:
return self.name.strip() or self.email.split("@")[0]
def normalized_email(self) -> str:
return self.email.strip().lower()
@dataclass(slots=True, kw_only=True)
class Organization:
name: str
slug: str
owner_user_id: str
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Membership:
organization_id: str
user_id: str
role: Role = Role.MEMBER
status: MembershipStatus = MembershipStatus.ACTIVE
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@property
def is_active(self) -> bool:
return self.status == MembershipStatus.ACTIVE
def has(self, capability: str) -> bool:
return self.is_active and role_has_capability(self.role, capability)
def outranks(self, other_role: Role) -> bool:
return ROLE_RANK[self.role] > ROLE_RANK[other_role]
@dataclass(slots=True, kw_only=True)
class Invite:
organization_id: str
email: str
role: Role
token_hash: str
expires_at: datetime
id: str = field(default_factory=new_id)
invited_by_user_id: str | None = None
accepted_at: datetime | None = None
created_at: datetime = field(default_factory=utcnow)
def is_expired(self, now: datetime | None = None) -> bool:
return (now or utcnow()) >= self.expires_at
def is_pending(self, now: datetime | None = None) -> bool:
return self.accepted_at is None and not self.is_expired(now)
+74
View File
@@ -0,0 +1,74 @@
"""Knowledge domain: sources, documents, chunks and retrieval policy.
Retrieval is permission-aware: an employee only sees knowledge scoped to its
organization (and optionally its project).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from app.domain.base import new_id, utcnow
class SourceKind(StrEnum):
FILE = "file"
TEXT = "text"
URL = "url"
REPO = "repo"
class EmbeddingJobStatus(StrEnum):
PENDING = "pending"
RUNNING = "running"
DONE = "done"
FAILED = "failed"
@dataclass(slots=True, kw_only=True)
class KnowledgeSource:
organization_id: str
name: str
kind: SourceKind = SourceKind.TEXT
uri: str = ""
project_id: str | None = None
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Document:
source_id: str
title: str
content: str = ""
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class DocumentChunk:
document_id: str
index: int
content: str
embedding_json: list[float] = field(default_factory=list)
id: str = field(default_factory=new_id)
@dataclass(slots=True, kw_only=True)
class EmbeddingJob:
source_id: str
status: EmbeddingJobStatus = EmbeddingJobStatus.PENDING
error: str | None = None
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class RetrievalPolicy:
organization_id: str
name: str
scope_json: dict = field(default_factory=dict)
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
+121
View File
@@ -0,0 +1,121 @@
"""Platform-wide validation rules carried over from the existing infrastructure.
These are pure functions with no I/O, so both the application layer *and* the
adapters can enforce them. Defence in depth: a rule that is only checked in one
place is a rule that will eventually be bypassed.
Reference: ``docs/current-infra-notes.md`` §1.
"""
from __future__ import annotations
import re
DEFAULT_ROOT_DOMAIN = "msa.artacloud.ir"
#: Ports that are never allocatable (system services + data stores).
SYSTEM_RESERVED_PORTS: frozenset[int] = frozenset(
{22, 53, 80, 443, 2019, 3000, 5432, 6379, 8008}
)
#: Ports of internal platform services. A tenant preview must never proxy to
#: these, otherwise a public hostname would expose another component.
SYSTEM_PROTECTED_PORTS: frozenset[int] = (
frozenset({3000, 35555, 35556, 35557}) | SYSTEM_RESERVED_PORTS
)
DEFAULT_PORT_RANGE_START = 8080
DEFAULT_PORT_RANGE_END = 8999
LABEL_MIN = 3
LABEL_MAX = 40
_LABEL_RE = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$")
_PROXY_TARGET_RE = re.compile(r"^(https?://)?(localhost|127\.0\.0\.1):(\d{1,5})$")
class RuleViolation(ValueError):
"""Raised when a request violates a platform safety rule."""
def validate_subdomain_label(label: str) -> str:
"""Normalise and validate a single DNS label.
Only ``a-z0-9-``, must start and end alphanumeric, 3-40 characters.
"""
clean = (label or "").strip().lower()
if not (LABEL_MIN <= len(clean) <= LABEL_MAX):
raise RuleViolation(
f"نام زیردامنه باید بین {LABEL_MIN} تا {LABEL_MAX} کاراکتر باشد."
)
if not _LABEL_RE.match(clean):
raise RuleViolation("نام زیردامنه فقط می‌تواند شامل حروف کوچک، عدد و خط تیره باشد.")
return clean
def build_host(label: str, root_domain: str = DEFAULT_ROOT_DOMAIN) -> str:
return f"{validate_subdomain_label(label)}.{root_domain.strip().lower()}"
def validate_managed_host(host: str, root_domain: str = DEFAULT_ROOT_DOMAIN) -> str:
"""Validate a fully-qualified host against the managed wildcard domain.
Rejects anything that is not a single label under the root domain — this is
the anti-injection check that keeps a crafted host from breaking out of a
Caddy site block or pointing at an arbitrary host.
"""
clean = (host or "").strip().lower()
suffix = f".{root_domain.strip().lower()}"
if not clean.endswith(suffix):
raise RuleViolation(f"دامنه باید زیرمجموعهٔ «{root_domain}» باشد.")
label = clean[: -len(suffix)]
if "." in label:
raise RuleViolation("فقط یک برچسب (single label) مجاز است.")
validate_subdomain_label(label)
return clean
def validate_proxy_target(target: str) -> tuple[str, int]:
"""Ensure the reverse-proxy target is a loopback address with a numeric port.
Blocks SSRF: a tenant must not be able to proxy a public hostname to an
internal service.
"""
clean = (target or "").strip()
match = _PROXY_TARGET_RE.match(clean)
if not match:
raise RuleViolation(
"مقصد reverse proxy باید localhost یا 127.0.0.1 با پورت عددی باشد."
)
port = int(match.group(3))
if not (1 <= port <= 65535):
raise RuleViolation("شمارهٔ پورت نامعتبر است.")
return match.group(2), port
def is_port_allocatable(
port: int,
*,
start: int = DEFAULT_PORT_RANGE_START,
end: int = DEFAULT_PORT_RANGE_END,
) -> bool:
if port in SYSTEM_RESERVED_PORTS:
return False
return start <= port <= end
def assert_port_allocatable(
port: int,
*,
start: int = DEFAULT_PORT_RANGE_START,
end: int = DEFAULT_PORT_RANGE_END,
) -> int:
if not is_port_allocatable(port, start=start, end=end):
raise RuleViolation(f"پورت {port} قابل تخصیص نیست.")
return port
def assert_not_protected_port(port: int) -> int:
"""A preview domain must never point at an internal platform port."""
if port in SYSTEM_PROTECTED_PORTS:
raise RuleViolation(f"اتصال به پورت محافظت‌شدهٔ {port} ممنوع است.")
return port
+56
View File
@@ -0,0 +1,56 @@
"""Projects domain: projects, their Git repositories and per-project credentials."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from app.domain.base import new_id, utcnow
@dataclass(slots=True, kw_only=True)
class Project:
organization_id: str
name: str
slug: str
description: str = ""
created_by_user_id: str | None = None
default_branch: str = "main"
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class ProjectGitRepo:
"""The Git repository attached to a project.
Named ``ProjectGitRepo`` rather than ``ProjectRepository`` on purpose: the
latter collided with the *infrastructure* repository that manages ``Project``
rows, and the shadowing silently broke every Git endpoint at runtime.
"""
project_id: str
provider: str = "gitea"
owner: str = ""
repo_name: str = ""
clone_url: str = ""
web_url: str = ""
default_branch: str = "main"
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@property
def full_name(self) -> str:
return f"{self.owner}/{self.repo_name}"
@dataclass(slots=True, kw_only=True)
class ProjectGitCredential:
"""Per-project Git identity. The password is encrypted at rest and never
returned to the frontend."""
project_id: str
username: str
encrypted_password: str
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
+128
View File
@@ -0,0 +1,128 @@
"""Sandbox domain: templates, workspaces, sessions, commands, snapshots, limits.
The host machine must never execute tenant code. Every command flows through a
SandboxProviderPort adapter and is bounded by a timeout and an output cap.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from app.domain.base import new_id, utcnow
class SandboxProviderKind(StrEnum):
MOCK = "mock"
DAYTONA = "daytona"
E2B = "e2b"
class WorkspaceStatus(StrEnum):
PENDING = "pending"
READY = "ready"
STOPPED = "stopped"
DESTROYED = "destroyed"
FAILED = "failed"
class SessionStatus(StrEnum):
STARTING = "starting"
RUNNING = "running"
PAUSED = "paused"
STOPPED = "stopped"
FAILED = "failed"
class NetworkPolicy(StrEnum):
DENY_ALL = "deny_all"
ALLOWLIST = "allowlist"
OPEN = "open"
@dataclass(slots=True, kw_only=True)
class SandboxTemplate:
name: str
provider: str = SandboxProviderKind.MOCK
image: str = "python:3.13-slim"
description: str = ""
default_cpu: float = 1.0
default_memory_mb: int = 1024
default_disk_mb: int = 2048
id: str = field(default_factory=new_id)
is_system: bool = True
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Workspace:
organization_id: str
project_id: str
template_id: str
provider: str = SandboxProviderKind.MOCK
external_workspace_id: str | None = None
status: WorkspaceStatus = WorkspaceStatus.PENDING
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class SandboxSession:
workspace_id: str
external_session_id: str | None = None
status: SessionStatus = SessionStatus.STARTING
id: str = field(default_factory=new_id)
started_at: datetime = field(default_factory=utcnow)
stopped_at: datetime | None = None
last_active_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class CommandRun:
session_id: str
command: str
exit_code: int | None = None
stdout_ref: str = ""
stderr_ref: str = ""
duration_ms: int = 0
truncated: bool = False
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@property
def succeeded(self) -> bool:
return self.exit_code == 0
@dataclass(slots=True, kw_only=True)
class Snapshot:
workspace_id: str
name: str
external_snapshot_id: str | None = None
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class PreviewUrl:
workspace_id: str
port: int
url: str
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class SandboxResourceLimit:
"""Limits are stored in policy, never trusted from the caller."""
cpu: float = 1.0
memory_mb: int = 1024
disk_mb: int = 2048
timeout_seconds: int = 120
max_output_bytes: int = 200_000
network_policy: NetworkPolicy = NetworkPolicy.DENY_ALL
scope: str = "organization"
scope_id: str | None = None
id: str = field(default_factory=new_id)
+252
View File
@@ -0,0 +1,252 @@
"""Agent Workforce domain: employees, chat threads, tasks, runs and artifacts."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from app.domain.base import new_id, utcnow
from app.domain.identity.models import Role
class EmployeeStatus(StrEnum):
ACTIVE = "active"
PAUSED = "paused"
ARCHIVED = "archived"
class RoleKey(StrEnum):
MANAGER = "manager"
PLANNER = "planner"
DEVELOPER = "developer"
REVIEWER = "reviewer"
TESTER = "tester"
RESEARCHER = "researcher"
CONTENT_CREATOR = "content_creator"
SUPPORT = "support"
class TaskStatus(StrEnum):
NEW = "new"
TRIAGED = "triaged"
WAITING_APPROVAL = "waiting_approval"
RUNNING = "running"
DONE = "done"
FAILED = "failed"
CANCELLED = "cancelled"
class RunStatus(StrEnum):
QUEUED = "queued"
PLANNING = "planning"
WAITING_APPROVAL = "waiting_approval"
RUNNING = "running"
TOOL_CALLING = "tool_calling"
SANDBOX_RUNNING = "sandbox_running"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
TIMED_OUT = "timed_out"
TERMINAL_RUN_STATUSES: frozenset[RunStatus] = frozenset(
{RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED, RunStatus.TIMED_OUT}
)
TERMINAL_TASK_STATUSES: frozenset[TaskStatus] = frozenset(
{TaskStatus.DONE, TaskStatus.FAILED, TaskStatus.CANCELLED}
)
#: Allowed run state transitions. Anything else raises in the application layer.
RUN_TRANSITIONS: dict[RunStatus, frozenset[RunStatus]] = {
RunStatus.QUEUED: frozenset({RunStatus.PLANNING, RunStatus.CANCELLED, RunStatus.FAILED}),
RunStatus.PLANNING: frozenset(
{RunStatus.RUNNING, RunStatus.WAITING_APPROVAL, RunStatus.FAILED, RunStatus.CANCELLED}
),
RunStatus.WAITING_APPROVAL: frozenset(
{RunStatus.RUNNING, RunStatus.CANCELLED, RunStatus.FAILED, RunStatus.TIMED_OUT}
),
RunStatus.RUNNING: frozenset(
{
RunStatus.TOOL_CALLING,
RunStatus.SANDBOX_RUNNING,
RunStatus.WAITING_APPROVAL,
RunStatus.COMPLETED,
RunStatus.FAILED,
RunStatus.CANCELLED,
RunStatus.TIMED_OUT,
}
),
RunStatus.TOOL_CALLING: frozenset(
{RunStatus.RUNNING, RunStatus.WAITING_APPROVAL, RunStatus.FAILED, RunStatus.TIMED_OUT}
),
RunStatus.SANDBOX_RUNNING: frozenset(
{RunStatus.RUNNING, RunStatus.FAILED, RunStatus.CANCELLED, RunStatus.TIMED_OUT}
),
RunStatus.COMPLETED: frozenset(),
RunStatus.FAILED: frozenset(),
RunStatus.CANCELLED: frozenset(),
RunStatus.TIMED_OUT: frozenset(),
}
def can_transition_run(current: RunStatus, target: RunStatus) -> bool:
return target in RUN_TRANSITIONS.get(current, frozenset())
class MessageRole(StrEnum):
USER = "user"
ASSISTANT = "assistant"
SYSTEM = "system"
class ToolCallStatus(StrEnum):
PENDING = "pending"
AWAITING_APPROVAL = "awaiting_approval"
APPROVED = "approved"
REJECTED = "rejected"
SUCCEEDED = "succeeded"
FAILED = "failed"
@dataclass(slots=True, kw_only=True)
class AgentTemplate:
name: str
role_key: RoleKey
description: str = ""
default_prompt: str = ""
default_tools_json: list[str] = field(default_factory=list)
default_routing_policy_id: str | None = None
id: str = field(default_factory=new_id)
is_system: bool = True
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Employee:
organization_id: str
name: str
role_key: RoleKey
template_id: str | None = None
project_id: str | None = None
status: EmployeeStatus = EmployeeStatus.ACTIVE
routing_policy_id: str | None = None
budget_limit_json: dict = field(default_factory=dict)
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@property
def is_available(self) -> bool:
return self.status == EmployeeStatus.ACTIVE
@dataclass(slots=True, kw_only=True)
class PromptVersion:
employee_id: str
version: int
content: str
created_by_user_id: str | None = None
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class EmployeeToolPermission:
employee_id: str
tool_key: str
allowed: bool = False
policy_json: dict = field(default_factory=dict)
id: str = field(default_factory=new_id)
@dataclass(slots=True, kw_only=True)
class Thread:
organization_id: str
title: str = ""
project_id: str | None = None
created_by_user_id: str | None = None
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Message:
thread_id: str
role: MessageRole
content: str
employee_id: str | None = None
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Task:
organization_id: str
title: str
description: str = ""
project_id: str | None = None
thread_id: str | None = None
assigned_employee_id: str | None = None
created_by_user_id: str | None = None
status: TaskStatus = TaskStatus.NEW
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
updated_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Run:
task_id: str
employee_id: str | None = None
status: RunStatus = RunStatus.QUEUED
error: str | None = None
id: str = field(default_factory=new_id)
started_at: datetime | None = None
finished_at: datetime | None = None
created_at: datetime = field(default_factory=utcnow)
@property
def is_terminal(self) -> bool:
return self.status in TERMINAL_RUN_STATUSES
@dataclass(slots=True, kw_only=True)
class RunStep:
run_id: str
index: int
kind: str
title: str = ""
payload_json: dict = field(default_factory=dict)
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class ToolCall:
run_id: str
tool_key: str
arguments_json: dict = field(default_factory=dict)
result_json: dict = field(default_factory=dict)
status: ToolCallStatus = ToolCallStatus.PENDING
requires_approval: bool = False
approved_by_user_id: str | None = None
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
@dataclass(slots=True, kw_only=True)
class Artifact:
organization_id: str
name: str
kind: str = "file"
content_ref: str = ""
project_id: str | None = None
run_id: str | None = None
size_bytes: int = 0
id: str = field(default_factory=new_id)
created_at: datetime = field(default_factory=utcnow)
#: Roles that may only be created by an org owner/admin.
ELEVATED_ROLE_KEYS: frozenset[RoleKey] = frozenset({RoleKey.MANAGER})
DEFAULT_EMPLOYEE_ROLE: Role = Role.MEMBER
+169
View File
@@ -0,0 +1,169 @@
"""Tool catalog for AI employees.
Three rules are encoded here as data rather than as scattered ``if`` statements:
1. Every tool has a stable key and an explicit approval requirement.
2. Dangerous tools require human approval **before** they run — the flag lives
with the tool, so a new call site cannot forget it.
3. Nothing is allowed by default; an employee needs an explicit permission row.
This module is pure: no I/O, no framework, no database.
"""
from __future__ import annotations
from dataclasses import dataclass
class ToolKey:
"""Stable tool identifiers. These appear in audit logs and permissions."""
GIT_ENSURE_REPO = "git.ensure_repo"
GIT_CREATE_BRANCH = "git.create_branch"
GIT_OPEN_PR = "git.open_pr"
GIT_MERGE_PR = "git.merge_pr"
GIT_DELETE_REPO = "git.delete_repo"
DOMAIN_CREATE_PREVIEW = "domain.create_preview"
DOMAIN_DELETE_PREVIEW = "domain.delete_preview"
SANDBOX_RUN_COMMAND = "sandbox.run_command"
SANDBOX_READ_FILE = "sandbox.read_file"
SANDBOX_WRITE_FILE = "sandbox.write_file"
SANDBOX_DELETE_FILE = "sandbox.delete_file"
SANDBOX_DESTROY = "sandbox.destroy"
KNOWLEDGE_SEARCH = "knowledge.search"
ARTIFACT_CREATE = "artifact.create"
@dataclass(frozen=True, slots=True)
class ToolSpec:
key: str
label: str
description: str
#: Human approval is required before this tool may execute.
requires_approval: bool = False
#: Irreversible or destructive: never auto-approved, always audited.
destructive: bool = False
#: Tools that touch the outside world are the ones worth approving.
category: str = "general"
TOOL_CATALOG: tuple[ToolSpec, ...] = (
ToolSpec(
key=ToolKey.GIT_ENSURE_REPO,
label="ساخت مخزن",
description="ساخت مخزن خصوصی Git برای پروژه.",
category="git",
),
ToolSpec(
key=ToolKey.GIT_CREATE_BRANCH,
label="ساخت شاخه",
description="ساخت یک شاخهٔ جدید از شاخهٔ مشخص.",
category="git",
),
ToolSpec(
key=ToolKey.GIT_OPEN_PR,
label="باز کردن درخواست ادغام",
description="ثبت یک PR از شاخهٔ کاری به شاخهٔ پایه.",
category="git",
),
ToolSpec(
key=ToolKey.GIT_MERGE_PR,
label="ادغام درخواست ادغام",
description="ادغام PR در شاخهٔ پایه. بازگشت‌ناپذیر.",
requires_approval=True,
destructive=True,
category="git",
),
ToolSpec(
key=ToolKey.GIT_DELETE_REPO,
label="حذف مخزن",
description="حذف کامل مخزن پروژه. بازگشت‌ناپذیر.",
requires_approval=True,
destructive=True,
category="git",
),
ToolSpec(
key=ToolKey.DOMAIN_CREATE_PREVIEW,
label="ساخت دامنهٔ تست",
description="انتشار موقت پروژه روی یک زیردامنه.",
category="domain",
),
ToolSpec(
key=ToolKey.DOMAIN_DELETE_PREVIEW,
label="حذف دامنهٔ تست",
description="برداشتن دامنهٔ تست از دسترس.",
requires_approval=True,
category="domain",
),
ToolSpec(
key=ToolKey.SANDBOX_RUN_COMMAND,
label="اجرای دستور در سندباکس",
description="اجرای یک دستور داخل محیط ایزوله با محدودیت زمان و خروجی.",
category="sandbox",
),
ToolSpec(
key=ToolKey.SANDBOX_READ_FILE,
label="خواندن فایل",
description="خواندن محتوای یک فایل در سندباکس.",
category="sandbox",
),
ToolSpec(
key=ToolKey.SANDBOX_WRITE_FILE,
label="نوشتن فایل",
description="نوشتن یا بازنویسی یک فایل در سندباکس.",
category="sandbox",
),
ToolSpec(
key=ToolKey.SANDBOX_DELETE_FILE,
label="حذف فایل",
description="حذف یک فایل از سندباکس. بازگشت‌ناپذیر.",
requires_approval=True,
destructive=True,
category="sandbox",
),
ToolSpec(
key=ToolKey.SANDBOX_DESTROY,
label="نابودی سندباکس",
description="پایان دادن به نشست سندباکس و آزادسازی منابع.",
requires_approval=True,
destructive=True,
category="sandbox",
),
ToolSpec(
key=ToolKey.KNOWLEDGE_SEARCH,
label="جست‌وجو در دانش",
description="جست‌وجو در پایگاه دانش سازمان.",
category="knowledge",
),
ToolSpec(
key=ToolKey.ARTIFACT_CREATE,
label="ساخت خروجی",
description="ثبت یک سند یا خروجی قابل مشاهده برای کاربر.",
category="artifact",
),
)
_BY_KEY: dict[str, ToolSpec] = {spec.key: spec for spec in TOOL_CATALOG}
#: Templates that reference an unknown tool key would silently grant nothing;
#: this set lets a test assert the seed data and the catalog agree.
KNOWN_TOOL_KEYS: frozenset[str] = frozenset(_BY_KEY)
def get_tool_spec(key: str) -> ToolSpec | None:
return _BY_KEY.get(key)
def is_known_tool(key: str) -> bool:
return key in _BY_KEY
def tool_requires_approval(key: str) -> bool:
"""Unknown tools are treated as requiring approval, never as safe."""
spec = _BY_KEY.get(key)
return True if spec is None else spec.requires_approval
def is_destructive(key: str) -> bool:
spec = _BY_KEY.get(key)
return False if spec is None else spec.destructive