Files
smart-org/docs/architecture/overview.md
T
mjalmoudy94 0b28a9f365 تغییر نام اپلیکیشن به Smart Organization
نام نمایشی و هویت اپلیکیشن در همهٔ سطوح کاربر-پسند از «AGY Web» به
«Smart Organization» تغییر کرد:

- UI: عنوان تب مرورگر، صفحهٔ ورود، برند و لوگوی پوستهٔ اپ (AG → SO)
- بک‌اند: مقدار پیش‌فرض app_name، نام و توضیح توزیع پایتون
- مانیفست‌ها: package.json ریشه و چهار پکیج، pyproject.toml
- مستندات: README، PLAN، TODO و دو سند docs

شناسه‌های داخلی عمداً دست‌نخورده مانده‌اند: scope پکیج‌های @agy/*،
کلیدهای localStorage، نام‌های logger، نام فایل پایگاهٔ داده، و اطلاعات
اتصال پایگاه داده در .env.example و infra/docker/docker-compose.yml.
2026-10-13 01:26:44 +03:30

20 KiB
Raw Blame History

نمای کلی معماری — نسخه وب مستقل Smart Organization

این سند تصویر فنی پروژهٔ web/ است: چه چیزی کجاست، چرا این‌طور چیده شده، و مرزهای اعتماد کدام‌اند. برای چک‌لیست امنیتی به ../security-checklist.md مراجعه کنید.


۱. هدف و قابلیت‌های محصول

یک SaaS چندمستأجری مستقل به‌نام «AI Workforce Platform». مشتری وارد می‌شود، سازمان می‌سازد، عضو دعوت می‌کند، پروژه تعریف می‌کند و به کارمندان هوش مصنوعی کار می‌سپارد. مشتری هرگز نمی‌بیند که پشت صحنه چه مدلی، چه providerی یا چه کلیدی است.

آنچه مشتری می‌بیند: سازمان، اعضا، پروژه، مخزن Git، دامنهٔ preview، محیط اجرای ایزوله، کارمند AI، گفتگو با مدیر، task و run، مصرف، صورتحساب، audit log.

آنچه فقط مالک سامانه می‌بیند: provider، مدل، سیاست مسیریابی، rate card، کلیدهای رمزنگاری‌شده، همهٔ سازمان‌ها.

مستقل از تلگرام: نسخهٔ وب هیچ import یا وابستگی runtime به bot.py, web_panel.py, caddy_manager.py, git_manager.py یا gitea_api.py ندارد. منطق Git و domain routing فقط به‌عنوان مرجع خوانده و در web/ به‌صورت adapter مستقل بازنویسی شده است.


۲. استک

لایه فناوری چرا
UI Next.js 15.1.6 (App Router) + React 19 + TypeScript RSC، مسیریابی فایل‌محور، یک پکیج مشترک برای کل محصول
استایل Tailwind CSS v3 RTL و dark-first بدون فایل CSS پراکنده
State/Data کلاینت fetch نازک + localStorage برای توکن بدون وابستگی اضافه؛ TanStack Query لازم نشد
API FastAPI + Pydantic v2 قرارداد تایپ‌دار، OpenAPI خودکار
ORM SQLAlchemy 2.x (async) هم‌راستا با مدل async FastAPI
Migration Alembic تنها مسیر تغییر schema در production
DB PostgreSQL (توسعه: SQLite حافظه‌ای برای تست) تست‌ها بدون سرویس بیرونی اجرا می‌شوند
Cache/Queue Redis آماده برای workerهای background
امنیت argon2 (رمز)، PyJWT (نشست)، cryptography/Fernet (رازها) انتخاب‌های استاندارد و مقاوم
تست pytest + pytest-asyncio (backend)، Vitest + Testing Library (frontend) —

۳. چیدمان مخزن

web/
├── apps/
│   ├── api/                  ← بک‌اند FastAPI (Clean/Hexagonal)
│   └── web/                  ← فرانت‌اند Next.js
├── packages/
│   ├── contracts/            ← تایپ‌های مشترک request/response
│   └── ui/                   ← کامپوننت‌های پایه (RTL، dark-first)
├── services/                 ← workerهای مستقل (ai-runtime, billing-worker, sandbox-gateway)
├── infra/docker/             ← docker-compose برای postgres/redis
├── docs/                     ← همین اسناد
├── PLAN.md                   ← سند مرجع محصول و معماری
└── TODO.md                   ← فهرست اجرای مرحله‌به‌مرحله

۴. معماری بک‌اند — Clean / Hexagonal

وابستگی‌ها همیشه به سمت داخل است. لایهٔ domain به هیچ فریمورک، ORM، Redis یا HTTP وابسته نیست (این با تست test_domain_layer_has_no_framework_dependencies تضمین شده است).

┌───────────────────────────────────────────────────────────────────┐
│ interfaces/http        routers + deps + error handlers            │
│   فقط route، schema ورودی/خروجی و تبدیل خطا                       │
├───────────────────────────────────────────────────────────────────┤
│ application            services (use case) + ports (driven iface) │
│   sandbox_service, workforce_service, project_service, ...        │
├───────────────────────────────────────────────────────────────────┤
│ domain                 قواعد خالص، بدون I/O                        │
│   identity, projects, workforce, sandbox, billing, audit,         │
│   knowledge, ai_control, platform/rules                           │
├───────────────────────────────────────────────────────────────────┤
│ infrastructure         adapters + repositories + db/models         │
│   mock, gitea, caddy, openai_compatible, sandbox_remote, stripe   │
└───────────────────────────────────────────────────────────────────┘
                        ↑ همهٔ پیکان‌ها به سمت domain

۴.۱ لایهٔ domain (بدون I/O)

هر bounded context یک پکیج است با models.py و قواعد مربوط به خود:

Context مسئولیت
identity کاربر، سازمان، عضویت، نقش، دعوت
projects پروژه، وضعیت مخزن، دامنهٔ preview
workforce قالب کارمند، کارمند، thread، message، task، run، artifact، مجوز ابزار (tools.py)
sandbox workspace، session، snapshot، NetworkPolicy، SandboxResourceLimit
billing پلن، rate card، usage ledger
audit audit event، trace، span، error، quality metric
knowledge منبع دانش، chunk
ai_control provider، مدل، سیاست مسیریابی
platform/rules.py قواعد امنیتی سکو: اعتبارسنجی برچسب دامنه، ضدتزریق host، ضدSSRF، پورت‌های رزرو/محافظت‌شده

platform/rules.py عمداً در domain است، نه در infrastructure: این قواعد باید بدون DB و بدون شبکه قابل تست باشند، چون بیشترین بار امنیتی را دارند.

۴.۲ لایهٔ application

سرویس‌ها (use case):

  • project_service — ساخت پروژه، اطمینان از مخزن، دامنهٔ preview
  • workforce_service — کارمند، prompt، مجوز ابزار، thread، task، اجرای run
  • sandbox_service — workspace، session، اجرای فرمان، clamp منابع، ثبت مصرف
  • billing_service — خلاصهٔ مصرف، صورتحساب
  • knowledge_service — منابع دانش و شمارش chunk
  • audit_service — audit log، trace

پورت‌ها (interfaceهای driven): در app/application/ports/

ModelProviderPort, PaymentProviderPort, SandboxProviderPort, GitProviderPort, DomainRouterPort, StoragePort, VectorStorePort, NotificationPort, UsageLedgerPort, AuditPort.

هر پورت حداقل یک adapter mock دارد، بنابراین کل استک بدون هیچ credential بیرونی بالا می‌آید.

۴.۳ لایهٔ infrastructure

انتخاب adapter — یک نقطهٔ تصمیم: infrastructure/adapters/registry.py تنها جایی است که تصمیم می‌گیرد کدام adapter پشت هر پورت بنشیند، فقط بر اساس سوئیچ‌های env:

env مقادیر مجاز adapter واقعی
AI_DEFAULT_PROVIDER mock | openai_compatible openai_compatible.py
GIT_PROVIDER mock | gitea gitea.py
DOMAIN_ROUTER mock | caddy caddy.py
SANDBOX_PROVIDER mock | daytona | e2b sandbox_remote.py
PAYMENT_PROVIDER mock | stripe stripe_payments.py

مقدار ناشناخته به ProviderUnavailableError منجر می‌شود — fail-closed، نه fallback بی‌صدا به mock.

Repositoryها: infrastructure/repositories/ — identity, projects, workforce, sandbox, billing, knowledge, ai_control, audit. هر متد لیست، limit/offset می‌گیرد و سقف min(limit, 500) را اعمال می‌کند.

مدل‌های DB: infrastructure/db/models.py — ۴۸ جدول با indexهای ترکیبی روی (organization_id, created_at) برای مسیرهای داغ داشبورد.

۴.۴ لایهٔ interfaces

interfaces/http/ شامل routers/ (۹ روتر: health, auth, organizations, projects, workforce, billing, knowledge, sandbox, admin)، deps.py (auth، org context، capability) و errors.py (تبدیل استثناها به پاسخ‌های یکدست). main.py تنها composition root است: روترها را به هم می‌بندد و CORS را تنظیم می‌کند.


۵. معماری فرانت‌اند

Next.js App Router با دو ناحیهٔ مسیر:

ناحیهٔ عمومی (بدون احراز هویت): /login, /signup, /onboarding

ناحیهٔ اپلیکیشن ((app) — نیازمند نشست): /dashboard, /workforce, /workforce/[id], /manager, /tasks, /tasks/[id], /projects, /projects/[id], /knowledge, /billing, /settings, /traces

ناحیهٔ مالک سامانه: /admin, /admin/providers, /admin/models, /admin/routing, /admin/rate-cards, /admin/organizations, /admin/errors

دو نقطهٔ اتصال مرکزی:

  • src/lib/api.ts — تنها جایی که base URL، bearer token و پوشش خطا را می‌شناسد. base URL از NEXT_PUBLIC_API_URL می‌آید (یک URL عمومی، نه راز).
  • src/lib/session.tsx — Context نشست: کاربر، سازمان‌ها، سازمان فعال، ورود/خروج.

پکیج مشترک UI: packages/ui/src/index.tsx کامپوننت‌های پایه را دارد — Button, Card, Input, Badge, Modal, Drawer, Table (با حالت empty/loading/error داخلی), Tabs, ToastStack, ArtifactViewer. RTL و dark-first به‌صورت پیش‌فرض.


۶. مسیر یک درخواست (مثال: اجرای یک task)

مشتری: دکمهٔ «اجرا» در /tasks/[id]
   │
   ├─ POST /api/organizations/{org}/tasks/{task}/runs   (Bearer token)
   │
   ├─ deps.get_current_user        → توکن را decode می‌کند، کاربر را می‌خواند (۴۰۱ اگر نامعتبر)
   ├─ deps.get_org_context         → عضویت فعال را می‌خواند (۴۰۴ اگر نباشد — نه ۴۰۳)
   ├─ deps.require_capability(...) → نقش کافی است؟ (۴۰۳ اگر نه)
   │
   ├─ workforce_service.start_run(...)
   │     ├─ انتخاب سیاست مسیریابی بر اساس mode (بدون افشای مدل به مشتری)
   │     ├─ اجرای گام‌ها → در صورت نیاز، sandbox_service.run_command(...)
   │     │     ├─ SandboxProviderPort (mock یا remote)
   │     │     ├─ clamp timeout/output از policy (نه از caller)
   │     │     └─ ثبت usage در UsageLedger
   │     └─ ثبت گام‌های timeline (kind="output" — بدون افشای «model»)
   │
   └─ پاسخ: run با timeline، وضعیت، و مصرف ثبت‌شده

۷. مرزهای اعتماد

  1. مرز مستأجر: هر endpoint چندسازمانی organization_id را enforce می‌کند. نبود عضویت → ۴۰۴. هیچ مسیری برای دیدن دادهٔ سازمان دیگر وجود ندارد.
  2. مرز مشتری/سکو: provider، مدل و کلید هرگز از API به مشتری نمی‌رسند. مشتری فقط یک نام سیاست مسیریابی می‌بیند.
  3. مرز اجرای کد: هیچ مسیری برای اجرا روی host وجود ندارد. همهٔ کد کاربر فقط از SandboxProviderPort عبور می‌کند، با egress پیش‌فرض بسته (DENY_ALL).
  4. مرز راز: رازها در حالت سکون با Fernet رمزنگاری می‌شوند؛ در پاسخ‌ها فقط نسخهٔ masked برمی‌گردد. frontend هیچ رازی ندارد.

۸. جریان داده و ثبت مصرف

usage ledger منبع حقیقت صورتحساب است — providerهای پرداخت فقط پول جابه‌جا می‌کنند و در محاسبه دخالتی ندارند. هر مصرف (model call، زمان sandbox، storage/artifact، tool call) یک رکورد در ledger می‌گذارد با organization_id, project_id, user_id, employee_id, run_id, unit, quantity, unit_price.

هر عملیات حساس یک AuditEvent هم می‌نویسد (ساخت سازمان، دعوت عضو، ساخت repo، ساخت دامنه، اجرای فرمان، حذف).


۹. تست

بک‌اند (apps/api/tests/) — ۲۱۶ تست:

فایل چه چیزی را می‌سنجد
test_auth.py ثبت‌نام، ورود، نشست، نشت‌نداشتن hash
test_tenant_isolation.py جداسازی مستأجر، ۴۰۴ به‌جای ۴۰۳، دعوت
test_admin_control_plane.py دسترسی مالک سامانه، mask شدن کلید
test_platform_rules.py برچسب دامنه، ضدتزریق host، ضدSSRF، پورت‌ها
test_project_repo.py مخزن خصوصی، نشت‌نداشتن توکن
test_workforce.py کارمند، مجوز ابزار (deny-by-default)، run، نشت‌نداشتن مدل
test_config_guard.py اعتبارسنجی رازها در production
test_pagination.py سقف صفحه‌بندی، رد offset نامعتبر
test_domain_isolation.py نبود وابستگی فریمورک در domain
test_e2e_smoke.py کل مسیر محصول + نامرئی‌بودن برای مستأجر دیگر
test_workers.py اجرای task بدون کلیک، idempotency صورتحساب، reaping سندباکس
test_seed.py قابل‌ورود بودن حساب‌های نمونه، idempotency seed
test_redact.py پاک‌سازی secret در ۴۲ حالت (ساختاری، الگویی، مقداری)
test_error_log.py مسیر کامل خطا: middleware → handler → recorder → دیتابیس
test_error_console.py دسترسی مالک سامانه، گروه‌بندی، بستن/بازگشایی، audit

فرانت‌اند (apps/web/src/test/) — ۲۵ تست: قرارداد API، کامپوننت‌های پایه، design system، و همبستگی request_id در کلاینت.


۹.۱ خط لولهٔ خطا

یک استثنا از محل وقوع تا کنسول چهار مرحله را طی می‌کند:

route raises
   │
   ├─ RequestContextMiddleware  → request_id + method/path/ip/user_agent روی request.state
   ├─ deps.get_current_user     → user_id  (پس از تأیید توکن)
   ├─ deps.get_org_context      → organization_id (پس از تأیید عضویت)
   │
   ├─ exception handler         → انتخاب اینکه ثبت شود یا نه + ساخت پاسخ استاندارد
   │    (errors.py)                (۵xx همیشه، ۴xx فقط با ERROR_LOG_CLIENT_ERRORS)
   │
   ├─ error_log.record_error    → redact → truncate → fingerprint
   │    (infrastructure)          سشن مستقل خودش؛ هرگز استثنا پرتاب نمی‌کند
   │
   └─ ObservabilityRepository   → INSERT در error_events

سه نکته که طراحی را تعیین کرده‌اند:

  1. recorder سشن خودش را باز می‌کند. سشن درخواست در لحظهٔ فراخوانی معمولاً غیرقابل‌استفاده است: یک IntegrityError آن را در وضعیت PendingRollbackError رها می‌کند. اگر از همان سشن استفاده می‌شد، سخت‌ترین خطاها برای ثبت — خطاهای دیتابیس — هرگز ثبت نمی‌شدند.
  2. recorder هرگز پرتاب نمی‌کند. یک خطای ثبتی نباید یک ۵۰۰ را به hang یا به یک استثنای مدیریت‌نشده تبدیل کند.
  3. X-Request-ID در خود هندلر ست می‌شود، نه فقط در middleware. ServerErrorMiddleware استارلت بیرون از همهٔ middlewareهای کاربر است، پس middleware هرگز پاسخی که برای یک استثنای مدیریت‌نشده ساخته می‌شود را نمی‌بیند. این باگ واقعاً رخ داد و با test_error_responses_carry_the_header_for_every_handler پوشش داده شد.

۱۰. اجرای پروژه

# ۱. زیرساخت
docker compose -f infra/docker/docker-compose.yml up -d postgres redis

# ۲. بک‌اند
cd apps/api
python3 -m venv .venv && .venv/bin/python -m pip install -e '.[dev]'
cp ../../.env.example ../../.env          # مقادیر mock کافی است
.venv/bin/python -m alembic upgrade head  # schema
.venv/bin/python -m app.seed              # دادهٔ نمونه (provider/مدل/rate card/قالب)

# ۳. فرانت‌اند
cd ../.. && corepack pnpm install
corepack pnpm dev                          # web روی ۳۰۰۰، api روی ۸۰۰۰

دستورهای کیفیت:

pnpm lint        # ESLint (web) + ruff (api)
pnpm typecheck   # tsc --noEmit (web) + mypy app (api)
pnpm test        # vitest (web) + pytest (api)
pnpm build       # next build

۱۱. تصمیم‌های معماری کلیدی

تصمیم دلیل
Modular monolith، نه microservice از روز اول مرزهای domain از ابتدا روشن‌اند؛ شکستن به سرویس بعداً ارزان است، اما پیچیدگی توزیع از روز اول گران است
یک adapter mock برای هر پورت کل محصول بدون credential قابل اجرا و تست است؛ §۲۴ TODO را برآورده می‌کند
۴۰۴ به‌جای ۴۰۳ برای نبود عضویت 403 وجود منبع را تأیید می‌کند و اجازهٔ شمارش می‌دهد
قواعد امنیتی در domain، نه infrastructure بدون DB/شبکه قابل تست‌اند و از لایهٔ I/O جدا می‌مانند
clamp منابع دولایه (service + adapter) defense in depth؛ خطای پیکربندی policy سقف مطلق را نمی‌شکند
usage ledger به‌عنوان منبع حقیقت محاسبهٔ صورتحساب از provider پرداخت جدا می‌شود
انتخاب adapter فقط با env، fail-closed پیکربندی اشتباه باید بلند شکست بخورد، نه بی‌صدا به mock برگردد
services/ فعلاً اسکلت است ai-runtime و billing-worker فعلاً داخل فرآیند API اجرا می‌شوند؛ مرز پورت‌ها آمادهٔ استخراج است