پروفایل مجوز کدکس یعنی یک فایل TOML که تعیین می‌کند ایجنت اجازه دارد کجا را بنویسد و کجا را فقط بخواند. اینجا با codex sandbox و یک پروفایل واقعی اندازه گرفتم که نوشتن داخل ریشه‌ی کاری کدکس روی دیسک دیده می‌شود، نوشتن بیرون از آن با کد خروج صفر گم می‌شود، خواندن کلیدهای میزبان شکست می‌خورد و شبکه از DNS هم بسته است. لحظه‌ی خواندن: ۱۰ مهر ۱۴۰۵، کدکس 0.158.0 روی اوبونتو 24.04.5 LTS.

چرا فقط sandbox_mode کافی نیست

پروفایل مجوز جایگزین ترکیب قدیمی sandbox_mode و sandbox_workspace_write شده است و این دو با هم ترکیب نمی‌شوند. مستندات کدکس می‌گویند اگر sandbox_mode در هر فایل پیکربندی بارگذاری‌شده باشد، کدکس تنظیمات قدیمی را به کار می‌برد و نه default_permissions را [1]. تفاوت سندباکس و سیاست تأیید هم جداگانه تعریف شده است [3]. یعنی یک خط باقی‌مانده از پیکربندی قدیمی، پروفایل تازه‌ی شما را بی‌صدا بی‌اثر می‌کند.

پیکربندی در فایل ~/.codex/config.toml می‌نشیند و کلید بالایی یک نام پروفایل است که به یک جدول هم‌نام ارجاع می‌دهد [5]. سه پروفایل درون‌ساخت وجود دارد: :read-only، :workspace و :danger-full-access [2]. نام‌های دلخواه خودتان هم با جدول‌های [permissions.NAME] تعریف می‌شوند [8]. مخزن کدکس همین قواعد را نگه می‌دارد [7].

نکته‌ای که در خواندن مستندات به چشم نمی‌آید: خطای default_permissions requires a [permissions] table یعنی شما مقدار رشته‌ای داده‌اید و کدکس یک جدول می‌خواهد. با نوشتن default_permissions = "read-only" به جای نام پروفایل، همین خطا را می‌گیرید.

پروفایل آماده‌ای که واقعا کار می‌کند

این پروفایل دو کار می‌کند: داخل ریشه‌ی کاری می‌نویسد و بیرون از آن فقط می‌خواند. شبکه در آن خاموش است، چون permissions.NAME.network.enabled پیش‌فرض روی false است [2].

کدکس از پیش روی این میزبان نصب بود، ولی اگر نصب نبود، همین یک خط کار می‌کند. پیش از آن، نسخه‌ی نصب‌شده را می‌بینیم:

$ npm install -g @openai/codex@0.158.0

$ codex --version
codex-cli 0.158.0

حالا فایل پیکربندی را می‌سازیم. کلید بالایی نام پروفایل است و به جدول هم‌نام ارجاع می‌دهد:

# ~/.codex/config.toml
default_permissions = "demo-ws"

[permissions.demo-ws]
description = "Write only inside the workspace root, no network"

# هر مسیری که اینجا true باشد، ریشه‌ی کاری پروفایل است
[permissions.demo-ws.workspace_roots]
"/root/.hermes/cache/scratch/ws-demo" = true

[permissions.demo-ws.filesystem]
":minimal" = "read"          # خواندن فایل‌های سیستمی
":workspace_roots" = "write"  # نوشتن فقط داخل ریشه‌های کاری

پیش از این پروفایل، codex sandbox بدون --permission-profile خطای the following required arguments were not provided می‌داد. بدون جدول [permissions] هم خطای default_permissions requires a [permissions] table می‌داد. با نوشتن نام پروفایلی که در فایل تعریف نشده بود، خطای default_permissions refers to undefined profile گرفتم. سه خطای جدا، سه تله‌ی جدا.

چهار آزمون و عددهای واقعی

روش ثابت همه‌ی آزمون‌ها یکی بود: اجرای یک دستور در سندباکس و بعد نگاه کردن به دیسک میزبان. سندباکس روی این میزبان با bubblewrap 0.9.0 ساخته می‌شود. فرمان‌های به‌کاررفته با مرجع خط فرمان کدکس هم‌خوان است [4] و نسخه‌ی نصب‌شده 0.158.0 است، یک نسخه بعد از 0.157.1 که در فهرست تغییرات آمده [6].

$ cd /root/.hermes/cache/scratch/ws-demo
$ echo "hello from the project" > app.txt

$ codex sandbox --permission-profile demo-ws \
    --cd /root/.hermes/cache/scratch/ws-demo \
    --sandbox-state-readable-root /root/.hermes/tools \
    --sandbox-state-readable-root /usr \
    -- /usr/bin/busybox sh -c 'echo written > out.txt; echo "exit=$?"; wc -c < out.txt'
exit=0
8

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

$ ls -la /root/.hermes/cache/scratch/ws-demo/out.txt
-rw-r--r-- 1 root root 8 Oct  1 11:04 /root/.hermes/cache/scratch/ws-demo/out.txt

آزمون دوم همان دستور با مسیری بیرون از ریشه‌ی کاری است. نکته‌ی مهم اینجاست که کد خروج صفر می‌شود:

$ codex sandbox --permission-profile demo-ws --cd /root/.hermes/cache/scratch/ws-demo \
    -- /usr/bin/busybox sh -c 'echo escaped > /root/.hermes/cache/scratch/escape.txt; echo "exit=$?"'
exit=0

$ ls -la /root/.hermes/cache/scratch/escape.txt
ls: cannot access '/root/.hermes/cache/scratch/escape.txt': No such file or directory

کد صفر یعنی echo فایل را نوشت و بست. ولی روی میزبان چیزی نیست. برای فهمیدن این تناقض، همان نوشتن و خواندن را در یک فراخوانی انجام دادم:

$ codex sandbox ... -- /usr/bin/busybox sh -c \
    'echo markerXYZ > /root/.hermes/cache/scratch/probe-overlay.txt; cat /root/.hermes/cache/scratch/probe-overlay.txt'
markerXYZ

$ ls -la /root/.hermes/cache/scratch/probe-overlay.txt
ls: cannot access '/root/.hermes/cache/scratch/probe-overlay.txt': No such file or directory

پس نوشتن بیرون از ریشه‌ی کاری خطا نمی‌دهد؛ در یک لایه‌ی موقت نوشته می‌شود، در همان فراخوانی قابل خواندن است و با پایان سندباکس ناپدید می‌شود. اگر اسکریپت شما نتیجه را فقط از روی کد خروج بسنجد، این نوشتن را موفق می‌بیند. برای همین در اسکریپت‌های خودکار، مسیر خروجی را داخل ریشه‌ی کاری نگه دارید.

آزمون سوم خواندن کلیدهای میزبان بود و شکست خورد:

$ codex sandbox ... -- /usr/bin/busybox sh -c 'cat /root/.codex/config.toml; echo "exit=$?"'
cat: can't open '/root/.codex/config.toml': No such file or directory
exit=1

آزمون چهارم شبکه بود و پیش از هر اتصالی در DNS متوقف شد:

$ codex sandbox ... -- /usr/bin/busybox sh -c 'busybox wget -q -O /dev/null -T 8 https://example.com; echo "wget_exit=$?"'
wget: bad address 'example.com'
wget_exit=1

$ # همان درخواست روی میزبان، بدون سندباکس
$ curl -s -o /dev/null -w "host_http=%{http_code}\n" --max-time 8 https://example.com
host_http=200

تفاوت بین bad address و Operation not permitted دو لایه‌ی جدا را نشان می‌دهد. در آزمون nslookup پیام دوم برگشت، یعنی سوکت UDP حتی ساخته نمی‌شود. صفر و دویست روی میزبان در برابر خطای یک در سندباکس، فاصله‌ی کل یک درخواست شبکه است.

آزمونکد خروج در سندباکسنتیجه روی میزبان
نوشتن داخل ریشه‌ی کاری0فایل روی دیسک، ۸ بایت
نوشتن بیرون ریشه‌ی کاری0هیچ فایلی ساخته نشد
خواندن کلید میزبان1فایل دیده نشد
درخواست به example.com1DNS رد شد، بدون اتصال
همان درخواست بدون سندباکس0کد ۲۰۰

تله‌ای که وقت منا گرفت

پیش از آنکه پروفایل درست جواب بدهد، هر فراخوانی با این خطا می‌مرد:

bwrap: execvp /root/.hermes/tools/.../vendor/x86_64-unknown-linux-musl/bin/codex: No such file or directory

خود کدکس استاتیک لینک شده و روی میزبان مستقیم اجرا می‌شود، ولی مسیر نصب آن داخل زیرپوشه‌ی ابزارهای هرمز بود و سندباکس آن مسیر را نمی‌دید. راه‌حل اضافه کردن همان مسیر به‌عنوان ریشه‌ی خواندنی است. پرچم --sandbox-state-readable-root را می‌شود چند بار تکرار کرد و هر بار یک ریشه اضافه می‌کند.

$ codex sandbox --permission-profile demo-ws --cd /root/.hermes/cache/scratch/ws-demo \
    --sandbox-state-readable-root /root/.hermes/tools \
    --sandbox-state-readable-root /usr \
    -- /usr/bin/busybox sh -c 'pwd; ls'
/root/.hermes/cache/scratch/ws-demo
app.txt
out.txt

دو نکته در این خروجی هست که کسی که عجله دارد از روی آن‌ها رد می‌شود. اول اینکه مسیر دوم بدون استثنا لازم است: بدون ریشه‌ی /usr حتی خود busybox هم پیدا نمی‌شود، چون /bin روی این توزیع یک پیوند نمادین به usr/bin است و افزودن /bin چیزی را درست نمی‌کند. دوم اینکه بدون ریشه‌ی کاری، --cd اعمال نمی‌شود و دستور در /root اجرا می‌شود؛ pwd این را لو می‌دهد.

کجای کار از این استفاده کنیم

اگر می‌خواهید یک دستور را جدا از ایجنت اجرا کنید و رفتارش را قبل از اعتماد کردن ببینید، همین فرمان کافی است. در خط لوله‌ی CI یا در تست یک دستور که به شبکه نیاز دارد، پروفایل پیش‌فرض بدون شبکه همان رفتاری را می‌دهد که از اجرای ایجنت انتظار دارید.

برای کارهایی که واقعاً به شبکه نیاز دارند، فقط یک کلید اضافه می‌شود، ولی یک شرط همراه آن است:

[permissions.with-net.network]
enabled = true

[features]
# بدون این، قوانین دامنه اعمال نمی‌شوند
network_proxy = true

[permissions.with-net.network.domains]
"example.com" = "allow"

شرط همراهش این است که روشن کردن شبکه به‌تنهایی پروکسی را بالا نمی‌آورد. مستندات می‌گویند بدون پروکسی فعال، دستورها مستقیم وصل می‌شوند و قوانین دامنه چیزی را محدود نمی‌کنند [1]. اگر network.enabled را روشن کنید ولی features.network_proxy را نه، شما دسترسی شبکه دارید بدون قوانین دامنه؛ یعنی بدتر از خاموش بودن کامل.

برای شبکه‌ی پیچیده‌تر، allow_local_binding به‌صورت پیش‌فرض false است و نام‌هایی که به نشانی محلی حل می‌شوند بسته می‌مانند [2]. در محیط محلی با داکر معمولاً لازم می‌شود و باز کردنش یک تصمیم آگاهانه است، نه یک پیش‌فرض.

قدم بعدی و محدودیت‌ها

پروفایل‌ها ارث‌بری دارند: extends یک پروفایل دیگر یا یکی از درون‌ساخت‌ها را می‌پذیرد و کدکس :danger-full-access و چرخه‌ها را رد می‌کند [2]. ساختار همین قواعد در سورس کدکس هم دیده می‌شود [9]. برای اینکه فقط یک مسیر اضافه کنید، پروفایلی با extends = ":workspace" بسازید و همان یک مسیر را در filesystem اضافه کنید، به جای آنکه کل قواعد را از نو بنویسید.

سه چیز را نتوانستم اندازه بگیرم و باید صریح بگویم. نخست، رفتار شبکه با پروفایل network.enabled = true را اجرا نکردم، چون برای این پست کافی می‌دیدم که مسیر بسته را نشان دهم. دوم، رفتار روی macOS و ویندوز را نسنجیدم؛ مستندات برای ویندوز راهنمای جداگانه‌ی سندباکس دارد [10] و پیاده‌سازی لینوکس از bubblewrap استفاده می‌کند که روی آن دو سیستم نیست. سوم، آنچه در جدول بالا آمده نتیجه‌ی یک اجرای تکی است؛ برای نتیجه‌گیری پایدار باید هر آزمون را چند بار تکرار کرد.

اگر در پروژه‌ی خودتان لایه‌ی محدودسازی مشابهی می‌خواهید، صفحه‌ی Permissions فهرست کامل کلیدها را دارد و صفحه‌ی Sandbox تفاوت مجوز و تأیید را توضیح می‌دهد. مرجع پیکربندی هم نوع هر کلید را دقیق می‌گوید و مخزن کدکس جایی است که این رفتار پیاده شده است. برای دیدن پرچم‌های فعال کدکس، مقاله‌ی codex features را نوشته‌ام و برای اجرای غیرتاملی در خط لوله، مقاله‌ی codex exec در CI مسیر را از اول نشان می‌دهد.

منابع

  1. Permissions – Codex | OpenAI Developers — قوانین پروفایل مجوز و شرط پروکسی شبکه
  2. Configuration Reference – Codex | OpenAI Developers — نوع و پیش‌فرض هر کلید پیکربندی
  3. Sandbox – Codex | OpenAI Developers — تفاوت سندباکس و سیاست تأیید
  4. Codex CLI – OpenAI Developers — مرجع خط فرمان
  5. Advanced Configuration – Codex | OpenAI Developers — پروفایل‌های پیکربندی و تنظیمات قدیمی سندباکس
  6. Permissions – ChatGPT Learn — همان مستندات با مثال‌های بیشتر
  7. Codex CLI changelog — نسخه‌ی 0.157.1 و انتشار GPT-6 Sol و Luna
  8. GitHub – openai/codex — مخزن اصلی کدکس
  9. codex-rs/config/src/permissions_toml.rs — ساختار پروفایل در سورس کد
  10. Windows sandbox – OpenAI Developers — تفاوت پیاده‌سازی در ویندوز