هر کاربر دانا یک سندباکس دارد: یک کانتینر ماندگار با فایلسیستم، ترمینال و اجازه اجرای کد دلخواه. مدل داخل همین کانتینر فایل میسازد، pip install میزند و اسکریپت اجرا میکند. سندباکس بین گفتگوها پاک نمیشود؛ پروژهها پوشههای کنار هم در همان فایلسیستم هستند.
این یعنی ما به کد ناشناخته اجازه اجرا میدهیم. مدل هم میتواند فریب بخورد: یک فایل README آلوده کافی است تا از عامل بخواهد چیزی را جایی بفرستد. پس سؤال درست این نیست که «چطور جلوی کد بد را بگیریم»، بلکه این است که وقتی کد بد اجرا شد، تا کجا میتواند برود.
مسیر یک پیام
پیام شما اول به سرویس عامل میرسد. آن سرویس حلقهای دارد که پیامها را به مدل میدهد، خروجی را میخواند و هر وقت مدل ابزاری صدا زد، آن را اجرا میکند و نتیجه را برمیگرداند. حلقه تا وقتی میچرخد که مدل دیگر ابزاری نخواهد.
خود مدل روی سرور استنتاج جداست و از پشت gateway دانا صدا زده میشود؛ همان POST /v1/chat/completions که مستندات توضیح میدهد. سرویس عامل به سرور مدل دسترسی مستقیم ندارد و سندباکس هم ندارد.
وقتی مدل میگوید «این دستور را در ترمینال بزن»، سرویس عامل خودش دستور را اجرا نمیکند. درخواست را به شیم داخل کانتینر میفرستد. شیم یک سرویس HTTP کوچک با سطح تماس عمدا باریک است: اجرای دستور، کار با فایل (خواندن، نوشتن، ویرایش، جستوجو)، فهرست فایلها، افزودن به تاریخچه و گرفتن خروجی tar. همین و بس. هر چیزی که در این فهرست نباشد، از بیرون قابل درخواست نیست.
نتیجه از همان مسیر برمیگردد، بهصورت رویداد SSE به مرورگر میرسد و در پایگاه داده هم نوشته میشود. برای همین اگر وسط کار اینترنت شما قطع شود، تاریخچه گم نمیشود.
چهار دیوار
اجرای کد ناشناخته یک لایه دفاعی ندارد؛ چند لایه مستقل دارد که هر کدام باید جدا شکسته شوند.
هسته دوم. کانتینرها هسته لینوکس میزبان را به اشتراک میگذارند و همین، نقطه ضعف تاریخیشان است؛ زنجیره فرارهای runc در آبان ۱۴۰۴ دقیقا از همینجا آمد. سندباکس دانا با gVisor اجرا میشود: یک هسته کاربرفضا که فراخوانهای سیستمی کانتینر را میگیرد و خودش جواب میدهد. کد داخل سندباکس عملا با هسته میزبان حرف نمیزند. این تصمیم در پیکربندی سرویس sandboxd قفل است. اگر رانتایم روی production چیزی جز gVisor باشد، سرویس اصلا بالا نمیآید:
.refine((e) => e.NODE_ENV !== "production" || e.SANDBOX_RUNTIME === "runsc", {
message:
"SANDBOX_RUNTIME must be 'runsc' (gVisor) when NODE_ENV=production; " +
"refusing to start sandboxes on plain runc",
path: ["SANDBOX_RUNTIME"],
})
خطای صریح، بهتر از یک fallback بیصداست. اگر تنظیمات اشتباه باشد، هیچ سندباکسی روشن نمیشود.
کانتینر لخت. ریشه فایلسیستم read-only است، همه capabilityهای لینوکس حذف شدهاند، no-new-privileges روشن است و uidهای داخل کانتینر با userns به یک بازه بیاثر روی میزبان نگاشت میشوند. تنها جای نوشتنی ماندگار، volume خود کاربر است که روی /home/dana سوار میشود، بهعلاوه یک tmpfs کوچک. سقف حافظه، CPU و تعداد پروسه با cgroup بسته شده، پس fork bomb فقط سندباکس خودش را میخواباند.
شبکه بسته. فایروال میزبان روی پل شبکه سندباکسها سیاست drop دارد. مقصد سرور مدل، پایگاه داده و شبکه داخلی صراحتا drop میشوند و ارتباط کانتینرها با هم خاموش است. تنها سوراخ، یک پراکسی allowlist بیرون از سندباکس است که فقط آینههای بسته npm و pip را باز میگذارد. DNS هم از همان مسیر میرود تا نشود داده را در اسم دامنه بیرون فرستاد. جستوجوی وب هم به همین دلیل ابزار سمت gateway است، نه سمت سندباکس: سندباکس هیچوقت به اینترنت باز وصل نمیشود.
بدون راز. ایمیج پایه هیچ کلیدی ندارد: نه رمز پایگاه داده، نه توکن مدیریت راز، نه کلید API. توکن دسترسی به شیم دست سرویس عامل است و به یک کاربر محدود میشود. حتی اگر مهاجم کل فایلسیستم سندباکس را بخواند، چیزی برای دزدیدن پیدا نمیکند.
چرا این ترکیب، نه یک ماشین مجازی سبک
جواب کوتاه: /dev/kvm. ماشینهای ابری ایران معمولا مجازیسازی تودرتو نمیدهند و Kata و Firecracker بدون KVM بالا نمیآیند. gVisor به KVM نیاز ندارد، زیر همان docker compose کار میکند و همان کلاس آسیبپذیری را میبندد. اگر بعدا سختافزار اختصاصی با KVM اضافه شود، رانتایم یک خط تنظیمات است، نه بازنویسی معماری.
هزینهاش را هم پنهان نمیکنیم: gVisor لایهای بین کد و هسته میگذارد و کارهای سنگین ورودیخروجی کمی کندتر میشوند. برای ساخت پروژه و اجرای اسکریپت، این معامله ارزشش را دارد.
خواب و بیداری
سندباکس بیاستفاده خاموش میشود، ولی دیسکش میماند. اولین پیام بعدی کانتینر را دوباره start میکند و چون volume دستنخورده است، پروژهها، فایلهای نصبشده و تاریخچه سر جایشاناند. مدت بیکاری تا خاموشی، بسته به پلن، فرق میکند.
این حالتها را از UI پنهان نمیکنیم. رویداد sandbox_status وضعیت را به رابط میفرستد و هنگام بیدار شدن، یک نشانگر کوچک بالای گفتگو ظاهر میشود. تایپ کردن قفل نمیشود؛ پیام نگه داشته میشود و بهمحض آماده شدن سندباکس میرود.
یک نکته که کاربران تازه را غافلگیر میکند: انشعاب یک گفتگو، فایلسیستم را انشعاب نمیدهد. شاخه تازه همان پوشه پروژه را میبیند. متن گفتگو جدا میشود، فایلها مشترک میمانند.
حافظهای که میتوانید بخوانید
هر پروژه یک پوشه .dana/ دارد و آن پوشه، فایل معمولی است؛ نه پایگاه داده مخفی. dana.md دستورهای شماست، memory.md یادداشتهای خود عامل و history/ آینه گفتگوهاست. همه را در پنل فایل باز میکنید، ویرایش میکنید یا کل سندباکس را zip میگیرید و میبرید.
مرز باقیمانده
هیچ سندباکسی صددرصدی نیست و ادعایش را هم نمیکنیم. gVisor خودش کد است و ممکن است روزی باگ بدهد؛ پراکسی allowlist تنها راه خروج است و هر چه آنجا باز شود، سطح حمله میشود. کاری که میکنیم این است: هیچ لایهای را تنها خط دفاع نگذاریم و هر تصمیم را در کد قابل بازبینی نگه داریم.
اگر دنبال سمت دیگر همین موضوع هستید، یعنی محافظت از خودِ مدل در برابر ورودی آلوده، prompt injection و پاسخ دانا ادامه همین بحث است.