# نمای کلی معماری — نسخه وب مستقل AGY > این سند تصویر فنی پروژهٔ `web/` است: چه چیزی کجاست، چرا این‌طور چیده شده، و > مرزهای اعتماد کدام‌اند. برای چک‌لیست امنیتی به > [`../security-checklist.md`](../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` پوشش داده شد. --- ## ۱۰. اجرای پروژه ```bash # ۱. زیرساخت 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 روی ۸۰۰۰ ``` **دستورهای کیفیت:** ```bash 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 اجرا می‌شوند؛ مرز پورت‌ها آمادهٔ استخراج است |