دادن دسترسی مستقیم شل به ایجنتهای کدنویسی، مانند قرار دادن یک کاربر ناشناس پشت خط فرمان با دسترسی بالا است. حتی در پروژههای محلی، یک مدل تحت فشار ممکن است به اشتباه دستوراتی مثل rm -rf را اجرا کند، فایلهای سیستمی حیاتی را دستکاری کند یا فرایندی بیپایان به راه بیندازد که سیستم را قفل کند. حفاظت از محیط توسعه در برابر این خطاهای ناشیانه، اولین گام پیش از سپردن کارهای واقعی به ایجنتها است.
در این درس یاد میگیرید چگونه پیش از رسیدن به ایزولهسازیهای سنگین کرنل، یک لایهی محافظتی سبک و سریع مبتنی بر پایتون بسازید. این لایه با تکیه بر تحلیل ساختار آرگومانها، مسدودسازی نام فایلهای اجرایی پرخطر، حبس مسیرهای فایل درون ریشهی پروژه (Path Jail) و قطع خودکار فرایندهای طولانی، امنیت پایهی اجرای دستورات را تضمین میکند.
گیت اعتبارسنجی (verification gate) تصمیم میگیرد که آیا یک فراخوانی ابزار (tool call) باید اجرا شود یا نه. ساندباکس (sandbox) تعیین میکند وقتی اجرا شد، چه اتفاقی بیفتد. در این درس، یک اجراکنندهی زیرفرایند (subprocess runner) میسازیم که جلوی فایلهای اجرایی خطرناک و آرگومانهای با ساختار ناامن را میگیرد، تمام مسیرهای فایلی را در ریشهی پروژه به زندان مسیر (path jail) میاندازد، خروجیهای بیشازحد حجیم را هرس (truncate) میکند و با اعمال یک مهلت زمانی بر اساس ساعت واقعی (wall-clock timeout)، فرایندهای مهارنشدنی را متوقف میسازد. این دومین لایه از دو لایهای است که بین مدل و سیستمعامل قرار میگیرند.
اهداف یادگیری
- پیادهسازی کلاس
Sandboxبهصورت پوششی دورsubprocess.runهمراه با مهلت زمانی، دریافت خروجی و هرس (کوتاهسازی). - جلوگیری از اجرای دستور بر اساس نام آن از طریق لیست سیاه (denylist) و بر اساس ساختار آن از طریق بازرس argv.
- رد کردن هر آرگومان مسیری که مقصد نهاییاش خارج از ریشهی تعیینشدهی پروژه باشد.
- رد کردن نویسههای کنترلی شل (metacharacters) وقتی حالت شل غیرفعال است.
- برگرداندن یک
SandboxResultساختاریافته که ابزارهای مشاهدهپذیری و بستر آزمایشی (eval harness) بتوانند آن را دریافت و پردازش کنند.
صورت مسئله
یک ایجنت (agent) کدنویسی که دسترسی اجرای دستورات شل را داشته باشد، میتواند تنها در یک نوبت گفتوگو (turn) در پشتی (backdoor) بگذارد، کلیدها را به بیرون درز دهد، لپتاپ توسعهدهنده را کاملاً از کار بیندازد و هزینهی سرسامآوری روی دست حساب ابری بگذارد. کمهزینهترین راه دفاع این است که اصلاً به آن شل ندهید. دومین راه کمهزینه، استفاده از ساندباکسی است که جلوی لیست دقیق و مشخصی از الگوها را میگیرد.
سه دسته از حالتهای شکست (failure modes) مرتباً در ردگیری محاسبات (trace) ایجنتها دیده میشوند:
دستهی اول: فایلهای اجرایی خطرناک. مدلی که برای حل یک مشکل مسیر تحت فشار قرار میگیرد، سراغ sudo، chmod -R 777، rm -rf، mkfs یا dd میرود. هیچکدام از اینها جایی در اجرای یک ایجنت ندارند. لیست سیاه با بررسی نام اصلی و نامهای مستعار جلوی آنها را میگیرد.
دستهی دوم: ترفندهای argv. مدلی که به آن گفته شده شل در کار نیست، حملهی خود را از طریق یک مفسر هدایت میکند: دستوراتی مثل python3 -c "import os; os.system('rm -rf /')", bash -c '...', node -e '...' یا perl -e '...'. ساندباکس باید بداند اجرای هر مفسری با پرچمی مشابه -c، عملاً همان فراخوانی شل با یک مرحلهی اضافه است.
دستهی سوم: گریز از مسیر (path escape). به مدل گفته میشود فایل ./src/main.py را بخواند، اما در عوض سراغ ../../etc/passwd میرود. ساندباکس با پیدا کردن مسیر نهایی از طریق os.path.realpath و بررسی پیشوند، تکتک آرگومانهای مسیر را در یک زندان مسیر حبس میکند.
این ساندباکس به معنای سیستمعاملی کلمه یک مرز امنیتی نیست. یک مهاجم مصمم با دسترسی اجرای کد همچنان میتواند از آن عبور کند. این ساندباکس یک گاردریل (guardrail) در زمان توسعه است: حالتهای شکست رایج را با هیاهو و بهصورت صریح آشکار میکند و مانع از آن میشود که ناشیگری و خطای محض ایجنت آسیبی بزند.
مفاهیم پایه
این ساندباکس چهار محور رد کردن دارد: نام، argv، مسیر و ساختار. هر محور یک تابع خالص (pure function) بر پایهی مشخصات فراخوانی است؛ پیش از آنکه هنوز زیرفرایندی اجرا شده باشد. زیرفرایند فقط زمانی متولد میشود که فراخوانی از سد هر چهار محور بگذرد.
کدهای خروج در SandboxResult همان کدهای مرسوم هستند: 0 برای موفقیت، عددی غیر از صفر برای شکست، بهعلاوهی سه کد نشانگر برای حالتهای رد شده (-100)، پایان مهلت زمانی (-101) و هرسشده (در این حالت کد خروج واقعی حفظ میشود و فقط یک پرچم مقدار میگیرد). درسهای بعدی بهجای پارس کردن متن stderr، مستقیماً از این نتیجهی ساختاریافته استفاده میکنند.
معماری
لیست سیاه یک frozenset از نام پایهی (basename) فایلهای اجرایی است. نامهای مستعار (/bin/rm، /usr/bin/rm) همگی به یک نام پایه میرسند. بازرس argv ساختار مفسرها را میشناسد: هر argv که عنصر argv[0] آن یک مفسر باشد و آرگومانهای بعدی با -c یا -e شروع شوند، رد خواهد شد. همچنین اگر در فراخوانی صراحتاً شل درخواست نشده باشد، دیدن نویسههای کنترلی شل (;, |, &, >, <, بکتیکها و $()) باعث رد دستور میشود.
زندان مسیر ظریفترین بخش کار است. ساندباکس در زمان ساخته شدن یک project_root تحویل میگیرد. هر آرگومانی که شبیه مسیر باشد (شامل / باشد یا با یک فایل موجود همنام باشد)، با os.path.realpath نرمالسازی میشود و سپس با realpath ریشهی پروژه مقایسه خواهد شد. اگر مقصد نهایی زیرمجموعهی پوشهی ریشه نباشد، دستور رد میشود. تلاش برای فرار از طریق پیوند نمادین (symlink) — یعنی وجود یک پیوند نمادین درون ریشهی پروژه که به بیرون اشاره میکند — با بررسی مسیر واقعی (realpath) و نه مسیر ظاهری، خنثی میشود.
آنچه میسازید
پیادهسازی شامل فایل main.py و یک پوشهی تست است:
- کلاس دادهی
SandboxResult: شامل exit_code، stdout، stderr، truncated، timed_out، denied، reason و duration_ms. - کلاس دادهی
SandboxConfig: شامل project_root، max_output_bytes، timeout_seconds، denylist و interpreter_block. - کلاس
Sandbox: متدrun(argv, *, shell=False, cwd=None)که یکSandboxResultبرمیگرداند. - توابع کمکی داخلی برای رد دستور:
_check_executable_denylist،_check_argv_interpreter،_check_shell_metacharsو_check_path_jail. - هرس خروجی با پرچم صریح
truncatedو قرار دادن یک خط نشانهگذاری درون متن دریافتی. - یک بخش نمایشی در انتهای فایل: اجرای دنبالهای از فراخوانیهای مجاز و مخرب به همراه نمایش خروجی هر کدام.
این ساندباکس بهطور پیشفرض از متد subprocess.run با گزینههای shell=False و capture_output=True استفاده میکند. مهلت زمانی بر اساس ساعت واقعی با آرگومان timeout اعمال میشود؛ در صورت بروز خطای TimeoutExpired، ساندباکس کل گروه فرایند را میکشد و یک شیء ساختاریافتهی SandboxResult میسازد.
چرا این یک ساندباکس واقعی نیست؟
ساندباکس این درس از قابلیتهای namespaces، cgroups، seccomp، gVisor، Firecracker یا هیچ ابزار ایزولهسازی دیگری در سطح کرنل (kernel) استفاده نمیکند. هر کاری که زیرفرایند بتواند انجام دهد، ساندباکس هم میتواند انجام دهد. حفاظت در اینجا ساختاری است: جلوی رایجترین دستورات خطرناک ایجنت گرفته میشود و این امتناع با صدای بلند در سیستمهای مشاهدهپذیری ثبت میشود، بهجای آنکه بیصدا اجرا شود.
برای ایجنتهای محیط عملیاتی (production)، لایههای بیشتری روی این ساختار اضافه میکنید: اجرا درون یک کانتینر Docker بدون دسترسیهای ویژه، اجرا در یک میکرو ماشین مجازی (microVM)، حذف قابلیتهای سیستمی (drop capabilities)، مانت کردن ریشهی پروژه بهصورت فقطخواندنی و یک پوشهی موقت بهصورت خواندنی-نوشتنی، تعیین ulimit روی رم و پردازنده، و پالایش متغیرهای محیطی با یک لیست مجاز کاملاً مطمئن. در درس 29 بخشی از این موارد را پیاده میکنیم. ایزولهسازی در سطح سیستمعامل خارج از دامنهی (scope) این درس است.
اجرای کد
cd phases/19-capstone-projects/26-sandbox-runner-denylist
python3 code/main.py
python3 -m pytest code/tests/ -v
بخش نمایشی یک پوشهی موقت میسازد، یک فایل سالم داخل آن میگذارد و سپس مجموعهای از دستورات مختلف را اجرا میکند. فراخوانیهای قانونی موفق میشوند. موارد ردشده یک شیء SandboxResult با مقدار denied=True و دلیل مشخص برمیگردانند. در صورت اتمام زمان، پرچم timed_out=True فعال میشود و برای خروجیهای بیشازحد بزرگ، پرچم truncated=True ست خواهد شد. در پایان، دمو یک جدول JSON از نتایج چاپ میکند و با کد صفر خارج میشود.
ارتباط این درس با ادامهی مسیر آموزشی
درس 25 زنجیرهی گیتها را پیاده کرد. درس 26 همان مجریای است که پس از صدور مجوز ALLOW از سوی گیت اجرا میشود. بستر آزمایشی درس 27 نتایج ساندباکس را با کدهای خروج موردانتظار برای هر وظیفه (task) مقایسه خواهد کرد. در درس 28، یک اسپن (span) با نام gen_ai.tool.execution دور هر فراخوانی Sandbox.run ثبت میکنیم. در نهایت، دموی سرتاسری (end-to-end) درس 29 یک ایجنت کدنویسی واقعی را با هر دو لایه متصل خواهد کرد.
