رفتن به محتوا

استقرار و کلاینت‌ها

Control Center یک برنامهٔ واحد نیست. یک معماری کلاینت نازک است: یک سرور بدون رابط که مالک داده‌های شماست و مجموعه‌ای کلاینت که یک رابط را روی یک اتصال RPC واحد به آن رندر می‌کنند. فهم این تفکیک توضیح می‌دهد که چرا بعضی چیزها همان‌جا که هستند اجرا می‌شوند و هر کلاینت به چه چیزی می‌رسد و به چه چیزی نمی‌رسد.

فرایندی به نام cc_server پایگاه‌های داده را نگه می‌دارد — یک global.db کوچک برای وضعیت سرتاسری سرور به‌علاوهٔ یک فایل SQLite به‌ازای هر فضای کاری، که با تأخیر باز می‌شوند — و کارهای دیرپا را اجرا می‌کند: موتورهای پایپ‌لاین، همگراسازها، سطح ابزار MCP و شنوندهٔ ارکستراسیون. هر چیزی که باید از عمر یک پنجره بیشتر بماند، یا دو کلاینت باید یکسان ببینند، همان‌جا زندگی می‌کند.

هیچ کلاینتی خودش فایل پایگاه داده را باز نمی‌کند. در عوض، هر کلاینت یک RemoteRpcClient نگه می‌دارد و عملیات مخزن و اشتراک‌ها را از طریق آن صادر می‌کند. سرور اتصال را در برابر یک کلید از پیش به‌اشتراک‌گذاشته‌شدهٔ دستگاه جفت‌شده احراز هویت می‌کند — و بعد دیگر به اتصال کاری ندارد. یک نشست به فضای کاری گره نمی‌خورد. توزیع‌کننده بی‌حالت است: هر فراخوانی محدود به فضای کاری، workspace_id خودش را در آرگومان‌هایش حمل می‌کند و فراخوانی‌ای که آن را حذف کند رد می‌شود.

این عمدی است و همان چیزی است که به دو کلاینت روی یک سرور اجازه می‌دهد در فضاهای کاری متفاوت بنشینند، بدون «فضای کاری جاریِ» نگه‌داشتهٔ سرور که بر سرش اختلاف کنند. در عوض ایزوله‌سازی در هر فراخوانی و در چهار گام اعمال می‌شود:

  1. عملیات اعلام می‌کند که آیا محدود به فضای کاری است یا نه؛ اگر هست و هیچ workspace_id نرسیده باشد، آن فراخوانی رد می‌شود.
  2. شناسهٔ نام‌برده باید یک فضای کاری ثبت‌شده باشد و پیش از آنکه چیزی پایگاه داده‌ای باز کند بررسی می‌شود — در غیر این صورت، یک شناسهٔ ناشناس با هر درخواست یک workspace.db خالی و شبح‌وار می‌ساخت. هر خطا یعنی «یافت نشد».
  3. WorkspaceRole فراخوان باید به حداقلِ تعیین‌شدهٔ آن عملیات برسد که از نوعش برمی‌آید: خواندن مهمان می‌خواهد، تغییر عضو، و کارهای مخرب مدیر.
  4. عملیاتی که محتوای مخزن را آشکار می‌کند، افزون بر آن به مجوزی در سطح هر مخزن نیاز دارد — none / read / review / write — تا عضویت در فضای کاری هرگز بی‌سروصدا از میزبان کد اختیار بیشتری نگیرد.

جمله‌ای که مهم است: داشتن کلید جفت‌سازی مرز دسترسی نیست؛ عضویت است.

سه فشار به‌سوی یک سرور هل داد:

  1. یک منبع حقیقت. یک رومیزی و یک تلفن، یا یک لپ‌تاپ و یک رومیزی، باید وضعیت یکسان ناوگان را ببینند. اگر هرکدام پایگاه دادهٔ خودش را داشت، از هم دور می‌افتادند. یک سرور واحد یعنی همهٔ کلاینت‌ها همان دادهٔ زنده را می‌خوانند.
  2. وب نمی‌تواند خودش را سرویس دهد. یک مرورگر نمی‌تواند فرایند فرزند اجرا کند یا فایل محلی باز کند، پس یک نسخهٔ وب هرگز نمی‌تواند مالک پایگاه داده باشد یا عاملی را اجرا کند. باید با سروری حرف بزند که این کارها را می‌کند. به‌جای ساختن یک مسیر دادهٔ دوم و وب‌محور، رومیزی هم به همان مسیر آورده شد.
  3. کار بدون رابط. سروری که می‌توانید روی یک ماشین کوچک اجرا کنید — در دسترس از تلفنی در قطار یا مرورگری در هر جا — از یک GUI که باید باز نگه دارید سودمندتر است.

هزینهٔ این تصمیم آن است که سرور اکنون یک وابستگی است: اگر اجرا نشود، هیچ چیز کار نمی‌کند. پاسخ پیش‌فرض رومیزی به آن این است که خودش یکی راه بیندازد.

پنج کلاینت به سرور می‌رسند و هرکدام شکل و نیمرخ اعتماد متفاوتی دارند.

برنامهٔ رومیزی Flutter (macOS، Windows، Linux) کلاینت کامل است. هنگام نخستین اجرا می‌پرسد که چگونه اجرا شود و بعد انتخاب را به یاد می‌سپارد:

  • محلی: یک cc_server را روی این ماشین راه‌اندازی و نظارت کنید — راه‌اندازی پیش‌فرض، خودبسنده و تک‌کاربره. رومیزی سرور را اجرا می‌کند؛ سرور مالک پایگاه داده زیر ریشهٔ پشتیبانی برنامه است و از راه لوپ‌بک با آن حرف می‌زند.
  • راه دور: به یک cc_server که جای دیگری اجرا می‌شود، از راه یک WebSocket ایمن وصل شوید. داده روی همان سرور می‌ماند؛ این رومیزی صرفاً یک رندرکننده است.

در هر دو حالت، کلاینت RPC حاصل، تأمین‌کنندهٔ دادهٔ برنامه را بازنویسی می‌کند، پس تمام رابط کاربری — هر صفحه، هر قابلیت — از راه سرور می‌خواند و می‌نویسد. رومیزی هرگز به فایل پایگاه داده دست نمی‌زند.

نسخهٔ وب همان برنامهٔ Flutter است که برای اجرا در مرورگر کامپایل شده و در app.usectrl.dev میزبانی می‌شود، پس چیزی برای نصب وجود ندارد. مرورگر نمی‌تواند فرایند فرزند اجرا کند، پس کلاینت وب همیشه راه دور است: با wss:// با یک cc_server تماس می‌گیرد و رابط کامل رومیزی را رندر می‌کند. پس از اتصال همان صفحه‌ها را روی همان ناوگان نشان می‌دهد، چون همان کد روی همان RPC است. شکاف‌ها همان چند جایی هستند که به توانمندی بومی نیاز دارند که مرورگر ندارد — لایهٔ کیفیت ضبط جلسه در ادامه مهم‌ترینشان است و اعلان‌های سیستم‌عامل دیگری.

به همین دلیل «بدون دانلود» و «بدون سرور» دو ادعای متفاوت‌اند. کلاینت وب نصب را حذف می‌کند؛ سرور را حذف نمی‌کند. آنچه با آن تماس می‌گیرد می‌تواند یک cc_server روی همان لپ‌تاپ باشد — ws://localhost:9030 که به گواهی نیاز ندارد چون مرورگرها لوپ‌بک را قابل‌اعتماد می‌دانند — یا سروری روی ماشینی دیگر، که باید از راه wss:// با گواهی‌ای که مرورگر به آن اعتماد دارد در دسترس باشد. cc_server از اتصال به نشانی غیرلوپ‌بک بدون TLS سر باز می‌زند، پس سرور راه دور یعنی یا گواهی خودش، یا یک پروکسی پایان‌دهندهٔ TLS، یا یک تونل یا VPN که گواهی را تأمین کند.

خود بسته جدا از سروری که با آن حرف می‌زند میزبانی می‌شود. یک cc_server مستقل نقطهٔ پایانی RPC، سطح MCP و پروکسی‌هایش را ارائه می‌دهد، اما نه یک بستهٔ وب — مسیر فایل ایستا در سرور وجود دارد ولی هیچ ریشهٔ وب روی باینری مستقل پیکربندی نشده، پس چیزی برای ارائه در / ندارد. ایمیج کانتینر منتشرشدهٔ cc-webapp است که بسته را میزبانی می‌کند و scripts/build_web.sh آن را می‌سازد. یک سرور مبدأهای مرورگر را تنها در فهرست --allowed-origins خود می‌پذیرد که پیش‌فرضش https://app.usectrl.dev است، پس میزبانی خودتان از بسته یعنی افزودن مبدأ آن.

رسانه‌ها (آواتارها، تصاویر خوراک، اسکرین‌شات‌های PR) از نقطهٔ پایانی پروکسی سرور عبور می‌کنند نه اینکه مستقیم به میزبان‌های بالادستی برخورد کنند، پس مرورگر هرگز از مبدأی دلبخواه چیزی نمی‌گیرد. کلاینت مستقرشده برای هر درخواست، بر پایهٔ یک کوکی غیرحساس، سیاست امنیت محتوا وضع می‌کند تا تنها مبدأ سرور متصل مجاز باشد.

برنامهٔ Remote یک کلاینت جداگانه و سبک‌تر است — یک PWA وب Flutter در remote.usectrl.dev. عمداً نه برنامهٔ کامل است. از راه یک رلهٔ WebSocket با واسطه و سرتاسری مهروموم‌شده جفت می‌شود و بخشی عمدتاً خواندنی از سطح ابزار را می‌فهمد: خواندن تیکت‌ها، پیام‌ها و خوراک خبری؛ ارسال پاسخ یا به‌روزرسانی یک تیکت.

تلفن یک موجودیت کم‌اختیارتر از یک عامل محلی است. یک سیاست ابزار با پیش‌فرض رد، یک کلید از پیش به‌اشتراک‌گذاشته‌شده که هر قاب را مهروموم می‌کند، یک قابلیت نشست که عملیات ممتاز (مثلاً جفت‌سازی) را از یک کلاینت همراه بیرون نگه می‌دارد، بررسی‌های نقش و مجوز مخزن در هر فراخوانی که بالاتر گفته شد و محدودسازی نرخ، مانع می‌شوند یک جفت‌سازی تأییدشده (یا لو رفته) به کنترل کامل از راه دور تبدیل شود. برای مدل کامل امنیتی کنترل از راه دور و موبایل را ببینید.

ابزارهای بیرونی — ویرایشگرها، عامل‌های دیگر، CLIها — که پروتکل Model Context را می‌فهمند، به سطح MCP سرور وصل می‌شوند و رجیستری ابزار نوع‌دار را مصرف می‌کنند. این مسیر یکپارچه‌سازی برنامه‌ای است؛ یک GUI نیست. استفاده از سرور MCP و مرجع ابزارهای MCP را ببینید.

یک cc_worker باینری بدون‌رابط و خالص Dart است که با یک cc_server جفت می‌شود، توانمندی‌های میزبانش را اعلام می‌کند و کارهای اجاره‌ای را برای اجرا می‌کشد. رویدادهای فرایند را از همان RPC برمی‌گرداند و هیچ حالت دیرپایی نگه نمی‌دارد: نه پایگاه داده، نه احراز هویت، نه تأیید، نه بودجه — این‌ها هرگز از cc_server بیرون نمی‌روند. کارگر یک اندام است، نه مغز دوم. یک سرور مرجع، N اندام ساده؛ هیچ اجماعی نیست، هیچ ترافیک کارگر-به-کارگری نیست.

اجرای اجاره‌ای هنوز همان لوله‌کشی اجاره است. agentRun تنها گونه‌ای است که یک فرمان واقعی اجرا می‌کند (CC_JOB_COMMAND، یا یک پرامپت بازتاب‌شده اگر هیچ فرمانی داده نشده باشد). pipelineStep، codeIndex، goldenRender، benchmark و evalBatch در حال حاضر با git --version / rev-parse HEAD کاوش می‌کنند تا مسیر جریان‌سازی آزموده شود، بدون اینکه ادعا شود آن کارها اجرا شده‌اند.

آنچه اعلام می‌کند سیستم‌عامل، معماری CPU، تعداد هسته، RAM، در دسترس بودن یک SDK Flutter و اینکه میزبان چه بک‌اندهای سندباکسی دارد (روی Windows هیچ) است. یک کارگر اختصاصی هم همچنین همیشه alwaysOn و acceptsParallel را اعلام می‌کند، که زمان‌بند «تثبیت → ترجیح → سرریز» بر پایهٔ آن کار جا می‌دهد. هیچ کاوش GPU یا ML وجود ندارد.

یک رومیزی تنها هیچ‌یک از این‌ها را متوجه نمی‌شود، چون اعزام معمولی اصلاً از ناوگان نمی‌گذرد. برای میزبان سرور یک کارگر درون‌فرایندی ضمنی وجود دارد، اما این یک درزِ ثبت است که هیچ رانری به آن وصل نیست — کاری که واقعاً روی کارگر محلی گذاشته شود شکست می‌خورد تا اجرا نشود. امروز فقط یک cc_worker واقعی کارهای اجاره‌ای را اجرا می‌کند.

موضوع سرور (cc_server) مجری ناوگان (cc_worker) کلاینت رومیزی کلاینت وب / تلفن
پایگاه‌های داده (Drift/SQLite) مالک آن‌هاست — — —
احراز هویت، تأییدها، بودجه‌ها، هویت مالک آن است — — —
پایپ‌لاین‌ها، همگراسازها، شنوندهٔ ارکستراسیون اجرا می‌کند — — —
سطح ابزار MCP ارائه می‌دهد — — —
اجرای عامل اجرا می‌کند (یا اجاره می‌دهد) کارهای اجاره‌ای را اجرا می‌کند — —
رندر رابط کاربری — — کامل کامل (وب) / عمدتاً خواندنی (تلفن)
ضبط جلسه (میکروفون + صدای جلسه) — — ضبط بومی فقط وب، از راه صدای اشتراک صفحه
OAuth تقویم (Google) توکن‌ها را نگه می‌دارد — — —

دو رده یکسان سندباکس نمی‌شوند. cc_server اجراهایی را که خودش انجام می‌دهد در سندباکس سیستم‌عامل میزبان می‌پیچد، جایی که بک‌اندی وجود داشته باشد — Seatbelt روی macOS، bubblewrap روی Linux و WSL2 — و روی Windows یا بدون bwrap و socat به دیگر مرزها برمی‌گردد. cc_worker هیچ سندباکسی ندارد. یک توانمندی sandbox اعلام می‌کند تا سرور بتواند کارها را بر پایهٔ پلتفرم جا دهد، اما هیچ چیز در کارگر یکی را اعمال نمی‌کند: یک کار اجاره‌ای مستقیم اجرا می‌شود و تنها به محیط اجاره و worktree داده‌شده محدود است. پس اجاره دادن کار به بیرون، اجرا را حتی روی میزبانی که سندباکس دارد بیرون از سندباکس می‌برد. برای اینکه در هر حالت چه چیزی اجرا را محدود می‌کند سندباکس و امنیت را ببینید.

ضبط جلسه روی یک کلاینت انجام می‌شود چون به میکروفون و صدای جلسه نیاز دارد — سروری در یک گنجه نمی‌تواند تماس شما را ضبط کند. سپس ضبط توسط پایپ‌لاین سرور خلاصه می‌شود و یادداشت‌ها جایی ذخیره می‌شوند که هر کلاینت بتواند آن‌ها را بخواند.

کلاینت وب هم ضبط می‌کند، چیزی که شکل جدول ممکن است پنهانش کند: میکروفون به‌علاوهٔ یک آهنگ صدای اشتراک صفحه را در مرورگر می‌گیرد و PCM 16 kHz را به سرور جریان می‌دهد. به آن آهنگ صوتی نیاز دارد، پس جایی که مرورگر یکی را فراهم نکند آشکارا شکست می‌خورد. آنچه واقعاً فقط مخصوص دسکتاپ است لایهٔ کیفیت پیرامون ضبط است — حذف پژواک در سطح سیگنال از راه پردازندهٔ بومی، نشانگر سطح ورودی و هشدار میکروفون قطع، اجرای دوبارهٔ خلاصه، لغو پردازش و کنش ضبط و پیوند تقویم.

هر کلاینت هویتش را با یک کلید از پیش به‌اشتراک‌گذاشته‌شده که یک بار تأمین شده اثبات می‌کند:

  • رومیزی در حالت محلی با هر راه‌اندازی یک کلید تازه می‌سازد و آن را از راه محیط به سرور راه‌اندازی‌شده می‌سپارد، پس هیچ رازی روی رومیزی ذخیره نمی‌شود.
  • کلاینت‌های راه دور (رومیزی-راه دور، وب، تلفن) از کلیدی استفاده می‌کنند که فرمان pair سرور ساخته و در جاکلیدی سیستم‌عامل (یا روی تلفن، به‌صورت محلی) ذخیره شده است.

ابطال زنده است، نه تنبل. سرور جدول دستگاه‌های جفت‌شده‌اش را می‌پاید و هر نشست بازی که دستگاهش از مجموعهٔ فعال بیرون رفته باشد را در عرض چند ثانیه می‌بندد — هم در مسیر مستقیم WebSocket و هم در مسیر رله. لازم نیست منتظر شوید دستگاه دوباره وصل شود تا آن را قطع کنید.

کلید هرگز از مسیری عبور نمی‌کند که یک میزبان ایستا بتواند ببیند. وقتی از راه پیوند می‌آید — روی تلفن یا در مرورگر — در قطعهٔ URL، همان بخش بعد از #، سوار می‌شود که مرورگر هرگز آن را به سرور میزبان صفحه نمی‌فرستد. این همان ناوردایی است که ارزش به یاد سپردن دارد و برای هر دو صادق است. کلاینت وب دو نقطهٔ ورود دیگر دارد: فرم اتصال، جایی که سرور و کلید را مستقیم جای می‌گذارید، و یک کد دعوت که آن را برای اعتباری صادرشده مخصوص همان مرورگر بازخرید می‌کند.