mjalmoudy94 82cea1e690 feat(delivery): owner-only domains + idempotent route re-sync (v2 phase A3-4)
Domains are infrastructure, and in v2 infrastructure is the platform owner's
alone (D9). A tenant gets one automatic demo host and nothing else.

- admin_domains.py (owner-only): list every route with its owning app, attach an
  extra host, detach with the host typed back as confirmation (audited), re-sync
  the router from the database, and read the suggested MCP gateway route.
- domain/platform/rules.py: validate_extra_host. Two shapes only — a single
  label under the managed root, or a full custom FQDN when ALLOW_CUSTOM_DOMAINS
  is on. A wildcard is refused in both (it would match another tenant's
  subdomain), and so are 30 protected platform labels (git, mcp, panel, api...).
  Enforced in the router *and* again in the adapter.
- Both router adapters gained routes_fingerprint / list_all_routes /
  sync_routes. Re-sync writes and reloads only when the rendered state differs:
  a reload drops every in-flight connection on the host, including live demos.
  Adapter-level extras, not additions to the frozen DomainRouterPort.
- The member-facing preview-domain routes are owner-only now. They are kept
  because the demo host is still created through them at app-creation time.

Tests: 473 API (+58); ruff and mypy clean.

Report: docs/plan/reports/agent-3-phase-4.md
2026-10-14 00:57:08 +03:30

Smart Organization — پلتفرم نیروی کار هوش مصنوعی

نسخهٔ وب مستقل و چندسازمانی محصول. این پروژه کاملاً جدا از ربات تلگرام است و هیچ فایلی از ریشهٔ مخزن را در runtime خود import نمی‌کند.


۱. محصول چیست

یک وب اپلیکیشن چندسازمانی برای «نیروی کار هوش مصنوعی سازمانی».

کاربر نهایی این‌ها را می‌بیند: سازمان، مدیر هوش مصنوعی، کارمندهای هوش مصنوعی، پروژه، وظیفه، فایل/خروجی، مصرف و پرداخت.

کاربر نهایی این‌ها را نمی‌بیند: نام مدل، provider، base_url، API key، قیمت خام provider. این‌ها فقط در Admin Console و فقط برای مالک سامانه قابل مدیریت‌اند.

پلتفرم سه لایهٔ نقش دارد:

  1. مالک سامانه (platform owner) — دقیقاً یک حساب و صاحب کل سیستم. اکانت‌های agy، مدل‌ها، routing policy، تعرفه‌ها، sandbox templateها، مشاهدهٔ کل مصرف، و ساخت/تنزل مدیران. agy تنها ارائه‌دهندهٔ مدل سامانه است و بخش جداگانهٔ «ارائه‌دهنده‌ها» وجود ندارد.
  2. مدیر (platform manager) — حسابی که فقط مالک سامانه می‌سازد. هر مدیر می‌تواند سازمان‌های خودش را بسازد (و مالک آن‌ها شود) یا با دعوت صاحب سازمان، در سازمان مدیران دیگر عضو شود. مدیر به کنسول مالک سامانه دسترسی ندارد.
  3. کاربر عادی (platform user) — سازمان نمی‌سازد؛ فقط داخل سازمان‌هایی که با دعوت به آن‌ها پیوسته کار می‌کند.

ثبت‌نام عمومی وجود ندارد. حساب‌ها از دو مسیر ساخته می‌شوند:

  • ساخت مستقیم توسط مالک سامانه در /admin/users (برای مدیر یا کاربر عادی) که یک رمز موقت یک‌بارمصرف برمی‌گرداند؛
  • پذیرش لینک دعوت: دعوت‌شده اگر حساب نداشته باشد، در همان صفحه رمز خودش را تعیین می‌کند و حساب ساخته می‌شود. اگر حساب داشته باشد باید اول وارد شود — یک لینک دعوت هرگز جای reset رمز را نمی‌گیرد.

«فقط یک مالک» یک قاعدهٔ دیتابیسی است: ایندکس یکتای جزئی روی users.platform_role = 'owner'، پس حتی یک درج مستقیم هم نمی‌تواند مالک دوم بسازد. جابه‌جایی صندلی مالکیت فقط با POST /admin/transfer-ownership انجام می‌شود که در یک تراکنش، مالک فعلی را به مدیر تنزل می‌دهد و جانشین را مالک می‌کند.


۲. پیش‌نیازها

ابزار نسخه
Node.js >= 20
pnpm 9.x
Python 3.13
Docker + Compose برای postgres/redis

فعال‌سازی pnpm در صورت نبود:

corepack enable pnpm
corepack prepare pnpm@9.12.0 --activate

۳. راه‌اندازی سریع

# ۱) نصب وابستگی‌های JS
pnpm install

# ۲) ساخت venv پایتون برای backend
pnpm api:venv

# ۳) کپی فایل env و پر کردن مقادیر (پیش‌فرض‌ها همه mock هستند)
cp .env.example .env

# ۴) بالا آوردن postgres و redis
docker compose -f infra/docker/docker-compose.yml up -d postgres redis

# ۵) اجرای migration و seed
pnpm db:migrate
pnpm db:seed

# ۶) اجرای هم‌زمان frontend و backend
pnpm dev

آدرس‌ها پس از اجرا:

حساب‌های نمونه (فقط محیط توسعه)

pnpm db:seed دو حساب می‌سازد:

نقش ایمیل رمز چه می‌بیند
مالک سامانه (admin console) owner@example.com owner12345 کنسول مالک سامانه، بدون هیچ سازمانی
مدیر نمونه demo@example.com demo12345 سازمان نمونه که خودش مالک آن است

مالک سامانه عضو هیچ سازمانی نمی‌شود — او مستأجرها را از کنسول می‌بیند، نه از داخل داده‌شان. برای دیدن داشبورد سازمانی با حساب مدیر نمونه وارد شوید.

این‌ها رمزهای توسعه هستند و در app/seed.py سخت‌کد شده‌اند. هیچ‌وقت در production از این‌ها استفاده نکنید و JWT_SECRET / ENCRYPTION_KEY واقعی بگذارید (نگهبان config در حالت production مقادیر placeholder را رد می‌کند).

نکته: دامنهٔ ایمیل عمداً example.com است. اگر .local یا .test بگذارید، email-validator آن را رد می‌کند و حساب ساخته می‌شود ولی ورود به آن ناممکن است. tests/test_seed.py همین را تضمین می‌کند.

اجرای کل استک با Docker

docker compose -f infra/docker/docker-compose.yml up --build

۴. اسکریپت‌ها

دستور کار
pnpm dev اجرای هم‌زمان Next.js و FastAPI
pnpm build build فرانت‌اند
pnpm lint ESLint (web) + ruff (api)
pnpm typecheck tsc (web) + mypy (api)
pnpm test vitest (web) + pytest (api)
pnpm format Prettier (web) + ruff format (api)
pnpm db:migrate alembic upgrade head
pnpm db:seed داده‌های نمونه (platform owner، کاربر نمونه، سازمان، templateها)
pnpm api:venv ساخت venv پایتون
pnpm worker:billing یک پاس worker صورتحساب
pnpm worker:ai-runtime یک پاس worker اجرای taskها
pnpm worker:sandbox-gateway یک پاس پاک‌سازی sessionهای بی‌استفاده

۵. معماری

۵.۱ سبک معماری

  • Clean Architecture + Hexagonal / Ports & Adapters
  • DDD برای مرزبندی bounded contextها
  • Event-Driven برای billing، audit و observability (الگوی Outbox)
  • Modular Monolith در فاز اول — microservice زودهنگام ممنوع

قاعدهٔ سخت: لایهٔ domain هیچ dependency به FastAPI، SQLAlchemy، Redis، HTTP یا provider خارجی ندارد. فقط application و infrastructure به آن وابسته‌اند، نه برعکس.

۵.۲ Bounded Contextها

identity · billing · ai_control · workforce · sandbox · knowledge · audit · projects · platform

۵.۳ Ports الزامی

ModelProviderPort · PaymentProviderPort · SandboxProviderPort · GitProviderPort · DomainRouterPort · StoragePort · VectorStorePort · NotificationPort · AuditPort · UsageLedgerPort

هر port حداقل یک adapter mock دارد تا کل استک بدون هیچ credential واقعی اجرا شود.

۵.۴ جریان درخواست

Browser → Web (Next.js/BFF) → API (FastAPI)
                                  ├── Domain Modules
                                  ├── Event Bus / Outbox → Workers
                                  ├── AI Runtime        → Model Providers
                                  ├── Sandbox Gateway   → E2B / Daytona
                                  └── Billing Processor → Stripe

۶. ساختار پوشه

web/
  PLAN.md                 پلن محصول و معماری
  TODO.md                 چک‌لیست اجرایی
  README.md               همین فایل
  package.json            اسکریپت‌های ریشه
  pnpm-workspace.yaml     تعریف workspace
  .env.example            نمونهٔ env (هیچ secret واقعی ندارد)
  apps/
    web/                  Next.js + TypeScript (UI)
    api/                  FastAPI + SQLAlchemy (backend)
  services/
    ai-runtime/           worker اجرای Agent
    sandbox-gateway/      wrapper ایزوله‌سازی اجرا
    billing-worker/       پردازش usage و invoice
  packages/
    contracts/            تایپ‌ها و قراردادهای مشترک
    ui/                   componentهای پایه (design system)
  infra/
    docker/               docker-compose
    migrations/           محل migrationها
  docs/
    architecture/         مستندات معماری
    product/              مستندات محصول
    current-infra-notes.md

۷. کجا کد بنویسیم

می‌خواهم... محل
صفحه یا component UI بسازم apps/web/src/app یا apps/web/src/features
component پایه/design system packages/ui
endpoint جدید بسازم apps/api/app/interfaces/http
use case بنویسم apps/api/app/application
منطق دامنه بنویسم apps/api/app/domain/<context>
adapter یا اتصال به سرویس بیرونی apps/api/app/infrastructure
worker پس‌زمینه بنویسم apps/api/app/workers (پوستهٔ اجرا در services/<name>)
خطا ثبت/پاک‌سازی کنم apps/api/app/core/redact.py + apps/api/app/infrastructure/error_log.py
قرارداد مشترک front/back packages/contracts

ممنوع: نوشتن هر کد جدید در فایل‌های ریشهٔ مخزن (bot.py, web_panel.py, bot_menus.py, bot_messages.py) و هر وابستگی به telegram, chat_id, callback_data, InlineKeyboardMarkup.


۸. سرویس‌های خارجی و حالت mock

همهٔ سرویس‌های بیرونی پیش‌فرض روی mock هستند. برای فعال‌سازی واقعی، این envها را پر کنید:

قابلیت env مقادیر
Git GIT_PROVIDER mock | gitea
دامنهٔ تست DOMAIN_ROUTER mock | caddy
Sandbox SANDBOX_PROVIDER mock | daytona | e2b
پرداخت PAYMENT_PROVIDER mock | stripe

اگر credential واقعی نداریم، قابلیت حذف نمی‌شود: مدل دیتابیس، interface، UI و adapter mock کامل ساخته می‌شود و adapter واقعی پشت همین flag آماده می‌ماند.

ارائه‌دهندهٔ agy (Google Antigravity) — تنها ارائه‌دهندهٔ مدل

agy کلید API ندارد؛ اکانت‌ها هویت Google هستند و از طریق OAuth وارد می‌شوند. کاتالوگ مدل‌ها در seed ساخته می‌شود و همان اول فعال است. برای شروع:

  1. با حساب مالک سامانه وارد کنسول مدیریت شوید و از /admin/agy-accounts → «افزودن اکانت agy» شروع کنید: لینک احراز هویت گوگل باز می‌شود، بعد از تأیید، URL بازگشت را از نوار آدرس مرورگر کپی و Paste کنید (صفحهٔ مقصد عمداً بارگذاری نمی‌شود؛ پارامترها همان‌جا هستند).
  2. در /admin/models زنجیرهٔ افزودن مدل کاملاً انتخابی است: ارائه‌دهنده (agy) → اکانت → مدل (کاتالوگ لحظه‌ای و زندهٔ همان اکانت) → افورت استدلال (فقط برای مدل‌های استدلالی). هیچ نام مدلی دستی تایپ نمی‌شود و مشتری نام مدل را هرگز نمی‌بیند.

هر تعداد اکانت Google که بخواهید اضافه کنید. پول چرخشی، بار را بین اکانت‌ها پخش می‌کند و اکانتی که ۴۲۹ گرفته تا پایان پنجرهٔ سهمیهٔ آن کنار گذاشته می‌شود (هم در حافظه و هم در دیتابیس، تا workerها همان تصویر را ببینند). refresh tokenها فقط encrypted at rest ذخیره می‌شوند و هیچ‌وقت از API برنمی‌گردند.


۹. Workerهای پس‌زمینه

سه worker وجود دارد. همه در apps/api/app/workers/ پیاده شده‌اند و services/<name> پوستهٔ اجرای آن‌هاست:

worker کار دستور
billing-worker جمع‌بندی usage ledger به فاکتور draft (هر دوره یک‌بار، idempotent) pnpm worker:billing
ai-runtime برداشتن taskهای در صف و اجرای آن‌ها از مسیر run engine pnpm worker:ai-runtime
sandbox-gateway متوقف‌کردن sessionهای بی‌استفاده و ثبت audit pnpm worker:sandbox-gateway

هر دستور یک پاس اجرا می‌کند و خارج می‌شود (شکل مناسب cron). برای اجرای مستمر --loop بدهید:

cd apps/api && .venv/bin/python -m app.workers billing --loop --interval 3600

اصل مهم: workerها هیچ منطق تجاری ندارند. هر سه از app/application/container.py همان سرویس‌هایی را می‌سازند که HTTP API می‌سازد، پس worker و API هرگز نمی‌توانند دربارهٔ اینکه کدام adapter پشت یک port است اختلاف داشته باشند. جزئیات هر کدام در services/*/README.md.


۱۰. اصول تغییرناپذیر

  1. اجرای کد کاربر یا agent روی host اصلی ممنوع — فقط از مسیر Sandbox Gateway.
  2. هر endpoint چندسازمانی باید organization_id را enforce کند.
  3. هیچ secret، token یا API key در frontend یا لاگ چاپ نمی‌شود.
  4. هر عملیات destructive باید confirmation و audit log داشته باشد.
  5. مدل و provider از دید کاربر عادی پنهان است.
  6. هر مصرفی در usage ledger داخلی ثبت می‌شود (Stripe فقط adapter است).

۱۱. کنسول خطا

هر خطایی که سامانه بگیرد — در درخواست HTTP، در worker پس‌زمینه، یا در زمان راه‌اندازی — با جزئیات کامل در جدول error_events ثبت می‌شود و در /admin/errors (فقط مالک سامانه) قابل مشاهده است.

چه چیزی ثبت می‌شود

منبع چه زمانی شدت
استثنای مدیریت‌نشده و خطاهای ۵xx همیشه critical / error
ValueError بدون نوع مشخص همیشه warning
سایر پاسخ‌های ۴xx فقط با ERROR_LOG_CLIENT_ERRORS=1 warning

۴xx به‌صورت پیش‌فرض ثبت نمی‌شود چون یک API عمومی دائماً ۴۰۴ از اسکنرها می‌گیرد و ثبت همهٔ آن‌ها خطاهای واقعی را زیر خود دفن می‌کند. خطاهای ۵xx همیشه ثبت می‌شوند، مستقل از این تنظیم.

هر رکورد چه چیزی دارد

  • kind، error_type، message و stack (ردِّ کامل خطا)
  • method، path، status_code و request_id
  • organization_id و user_id (پس از احراز هویت)
  • fingerprint — هش پایدار از هویت خطا، برای گروه‌بندی
  • metadata_json — جزئیات اضافی (مثلاً task_id)

گروه‌بندی و رسیدگی

خطاهای یکسان بر اساس fingerprint در یک ردیف جمع می‌شوند، پس یک باگ که ۴۰۰۰ بار رخ داده یک خط با شمارنده است، نه ۴۰۰۰ خط. fingerprint عمداً سازمان و کاربر را در نظر نمی‌گیرد: یک باگ که دو مستأجر را می‌زند هنوز یک باگ است. بستن یک گروه همهٔ رخدادهای باز آن را می‌بندد و در audit log ثبت می‌شود.

پاک‌سازی خودکار secretها

هیچ خطایی بدون عبور از app/core/redact.py ذخیره نمی‌شود. سه لایه: کلیدهایی که معنایشان secret است (password، api_key، …)، الگوهای شناختنی (JWT، Bearer، scheme://user:pass@host، sk-…، ghp_…)، و خودِ مقادیر secret تنظیم‌شده در settings. یک لاگ خطا که رمز عبور ذخیره کند از نداشتن لاگ خطا بدتر است.

استفاده از request_id

هر پاسخ X-Request-ID دارد و هر بدنهٔ خطا هم همان شناسه را در error.request_id برمی‌گرداند. کاربر می‌تواند فقط همین یک رشته را گزارش کند تا دقیقاً یک رکورد پیدا شود.

# ۵۰۰ بگیر، سپس با همان شناسه رکورد را پیدا کن
curl -i http://localhost:8000/api/whatever -H "X-Request-ID: my-trace"
curl "http://localhost:8000/api/admin/errors?search=my-trace" -H "Authorization: Bearer $OWNER_TOKEN"

اجرای دستی workerها و ثبت خطا

خطای یک pass کامل با source=worker ثبت می‌شود و exit code غیرصفر می‌دهد تا cron متوجه شود. خطای یک task مشخص نیز با task_id و سازمان مربوطه ثبت می‌شود.


۱۲. مستندات

S
Description
Smart Org v2
Readme
1.5 MiB
Languages
Python 73.5%
TypeScript 26.3%
CSS 0.1%