codex app-server همان چیزی است که افزونه‌ی VS Code کدکس پشتش می‌نشیند: یک سرور JSON-RPC که تاریخچه‌ی گفتگو، تاییدها و رویدادهای جریانی ایجنت را به هر کلاینتی می‌دهد. در این نوشته روی نسخه‌ی 0.158.0 یک کلاینت کوچک نوشتم که بدون کلید API بالا می‌آید، دست initialize را کامل می‌کند، فهرست مدل‌ها را می‌گیرد و دروازه‌ی experimentalApi را می‌سنجد. لحظه‌ی خواندن داده: ۱۲ مهر ۱۴۰۵، برابر با ۴ اکتبر ۲۰۲۶.

مستندات app-server آن را رابطی می‌خواند که کدکس برای تغذیه‌ی کلاینت‌های غنی از آن استفاده می‌کند، و پیاده‌سازی‌اش متن‌باز در پوشه‌ی codex-rs/app-server مخزن کدکس است.

مستندات سه واحد درونی‌تر را نام می‌برد و هر کدام نامی جدا در متدها دارد: یک گفتگو، یک درخواست کاربر با کاری که پس از آن می‌آید، و واحدهای ورودی و خروجی مثل پیام ایجنت یا اجرای فرمان.

دست‌دادن و اینکه چه چیزی را به کلاینت می‌دهد

روی هر اتصال فقط یک بار initialize می‌فرستید و بعد یک اعلان initialized. هر چیزی پیش از آن با خطا رد می‌شود. این را اندازه گرفتم، نه از روی مستندات: یک درخواست thread/list پیش از دست‌دادن فرستادم و سرور پاسخ داد Not initialized.

$ python3 app.py
1 before-initialize -> {"error": {"code": -32600, "message": "Not initialized"}, "id": 1}
2 initialize        -> {"id": 2, "result": {"userAgent": "post-demo/0.158.0 (Ubuntu 24.4.0; x86_64) unknown (post-demo; 0.1.0)", "codexHome": "/root/.codex", "platformFamily": "unix", "platformOs": "linux"}}
3 model/list        -> {"id": 3, "result": {"data": [{"id": "gpt-6-astra", "model": "gpt-6-astra", "upgrade": null, "upgradeInfo": null, "availabilityNux": null, "displayName": "GPT-6-Astra", "description": "Frontier intelligence for the most demanding work.", "modelSpecialty":
4 diagnostics       -> {"error": {"code": -32600, "message": "server/diagnostics requires experimentalApi capability"}, "id": 4}

پاسخ initialize سه چیز به شما می‌دهد: رشته‌ی userAgent که به سرویس‌های بالادستی فرستاده می‌شود، و platformFamily و platformOs که در اجرای من unix و linux بودند.

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

import json
import subprocess

proc = subprocess.Popen(
    ["codex", "app-server"],            # پیش‌فرض روی stdio بالا می‌آید
    stdin=subprocess.PIPE, stdout=subprocess.PIPE,
    stderr=subprocess.DEVNULL, text=True, bufsize=1,
)


def send(msg):
    # یک پیام JSON-RPC در یک خط روی ورودی استاندارد
    proc.stdin.write(json.dumps(msg) + "\n")
    proc.stdin.flush()


def read_until(request_id):
    # سرور اعلان هم می‌فرستد، پس تا پاسخ همین شناسه جلو می‌رویم
    for _ in range(40):
        line = proc.stdout.readline()
        if not line:
            return None
        msg = json.loads(line)
        if msg.get("id") == request_id:
            return msg
    return None


send({"method": "initialize", "id": 2, "params": {"clientInfo": {
    "name": "post-demo", "title": "Post Demo", "version": "0.1.0"}}})
print("2 initialize        ->", json.dumps(read_until(2)))
send({"method": "initialized", "params": {}})

نکته‌ی عملی این است که یک readline ساده کافی نیست. سرور بین پاسخ شما اعلان می‌فرستد و اگر فقط یک خط بخوانید ممکن است به‌جای پاسخ، اعلان را بگیرید. در اولین اجرای خودم همین اتفاق افتاد: به‌جای پاسخ thread/start یک پیام remoteControl/status/changed گرفتم.

بدون کلید API چه چیزهایی واقعا کار می‌کنند

مهم‌ترین یافته‌ی این نوشته همین‌جاست. سرور بدون هیچ اعتباری بالا می‌آید، چون فقط خط لوله را می‌بندد و به سرویسی وصل نمی‌شود. یک codex doctor روی همین ماشین می‌گوید no Codex credentials were found، و در عین حال درخواست‌های زیر جواب واقعی برمی‌گردانند:

متدوضعیت بدون کلید APIآنچه برگشت
initializeپاسخ دادuserAgent و platform
model/listپاسخ دادفهرست مدل‌ها با نام نمایشی
skills/listپاسخ دادفهرست اسکیل‌های همین پوشه
config/readپاسخ دادکل پیکربندی با فیلدهای null
hooks/listپاسخ دادآرایه‌ی خالی با دو کلید بی‌خطا
app/installedپاسخ دادفهرست خالی
thread/listپاسخ دادآرایه‌ی خالی با نشانگر صفحه
server/diagnosticsرد شدپیام نیاز به experimentalApi

آن config/read با همه‌ی فیلدهای null نکته‌ی ریزی دارد که در جدول گم می‌شود. فیلدهایی مثل model و sandbox_mode همه null برگشتند، یعنی این سرور تنظیمات مؤثر را از فایل می‌خواند نه از دل کلاینت. اگر پیکربندی مؤثر را می‌خواهید، config/read ابزار درست است.

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

$ python3 probe6.py
config/read        -> {"id": 2, "result": {"config": {"model": null, "review_model": null, "model_context_window": null, ...
model/list         -> {"id": 3, "result": {"data": [{"id": "gpt-6-astra", "model": "gpt-6-astra", "displayName": "GPT-6-Astra", ...
skills/list        -> {"id": 4, "result": {"data": [{"cwd": "/root/.hermes/hoosh-blog", "skills": [{"name": "plugin-eval:evaluate-plugin", ...
hooks/list         -> {"id": 5, "result": {"data": [{"cwd": "/root/.hermes/hoosh-blog", "hooks": [], "warnings": [], "errors": []}]}}
app/installed      -> {"id": 7, "result": {"apps": []}}
thread/loaded/list -> {"id": 8, "result": {"data": [], "nextCursor": null}}
server/diagnostics -> {"error": {"code": -32600, "message": "server/diagnostics requires experimentalApi capability"}, "id": 6}

تفاوت hooks/list با بقیه این است که فقط یک آرایه نیست. هر عنصر سه کلید دارد: hooks، warnings و errors. یعنی می‌توانید بدون باز کردن فایل‌ها بفهمید قلاب‌های یک پوشه چه خطایی داده‌اند.

دروازه‌ی experimentalApi و اینکه چه چیزی واقعا پشتش است

مستندات می‌گویند بعضی متدها و فیلدها پشت capabilities.experimentalApi قفل شده‌اند و بدون آن خطای requires experimentalApi capability می‌گیرید. این را روی پنج متد سنجیدم و نتیجه یک‌دست نبود:

=========== experimentalApi: off
  mock/experimentalMethod              -> {"error": {"code": -32600, "message": "mock/experimentalMethod requires experimentalApi capability"}, "id": 2}
  thread/backgroundTerminals/list      -> {"error": {"code": -32600, "message": "thread/backgroundTerminals/list requires experimentalApi capability"}, "id": 2}
  thread/items/list                    -> {"error": {"code": -32601, "message": "thread/items/list is not supported yet"}, "id": 2}
  thread/loaded/list                   -> {"id": 2, "result": {"data": [], "nextCursor": null}}
=========== experimentalApi: on
  mock/experimentalMethod              -> {"id": 2, "result": {"echoed": null}}
  thread/backgroundTerminals/list      -> {"error": {"code": -32600, "message": "thread not found: 11111111-2222-3333-4444-555555555555"}, "id": 2}

دو چیز اینجا ارزش دانستن دارد. اول اینکه thread/items/list در هر دو حالت خطا داد ولی خطایش فرق کرد: بدون اجازه دروازه را زد و با اجازه گفت هنوز پیاده نشده است. یعنی اجازه دادن همه‌چیز را آزاد نمی‌کند.

دوم اینکه با اجازه، thread/backgroundTerminals/list از requires experimentalApi capability به thread not found رفت. یعنی دروازه باز شد و سرور واقعا به مرحله‌ی بعدی رفت، یعنی دنبال ترد می‌گشت. برای تست دروازه، همین جابه‌جایی پیام خطا را نگاه کنید، نه وجود یا نبود خطا.

نکته‌ی دومی هم هست که سرگردان‌کننده است: مستندات thread/turns/list را آزمایشی می‌خواند، ولی در اجرای من با هر سه حالت experimentalApi پاسخ thread not loaded داد و هرگز پیام نیاز به اجازه نداد. یعنی مستندات و رفتار این نسخه برای این یک متد یکی نیستند.

کد را از خود کلاینت بگیرید، نه از مستندات

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

$ codex app-server generate-json-schema --out ./schema
$ ls ./schema | wc -l
39
$ codex app-server generate-ts --out ./ts
$ ls ./ts | wc -l
96
$ codex app-server generate-json-schema --out ./schema-exp --experimental
$ ls ./schema-exp | wc -l
47
$ codex app-server generate-ts --out ./ts-exp --experimental
$ ls ./ts-exp | wc -l
102

کلید --experimental روی schema تولیدشده ۸ فایل JSON Schema و ۶ فایل TypeScript اضافه می‌کند. این تفاوت را خودم شمردم، نه از دل مستندات.

از همین schema می‌توانید فهرست کامل متدها را بیرون بکشید. الگوی نام‌گذاری را روی فایل ClientRequest.json اجرا کردم و ۱۲۵ نام متد بیرون آمد:

$ python3 schema_scan.py
methods found: 125
   account/read
   account/rateLimits/read
   account/usage/read
   command/exec
   config/read
   config/mcpServer/reload
   fs/readFile
   memory/status
   mock/experimentalMethod
   model/list
   plugin/install
   process/spawn
   skills/list
   thread/archive
   thread/fork
   thread/list
   thread/resume
   turn/interrupt
   turn/start
   ...

آن ۱۲۵ عدد فقط در یک نسخه معتبر است. با هر به‌روزرسانی کدکس می‌تواند عوض شود، پس schema را در مخزن پروژه‌ی خودتان نگه دارید و diff بگیرید.

همان سرور روی سوکت یونیکس و وب‌سوکت

تا اینجا همه چیز روی stdio بود. اگر می‌خواهید کلاینت از ماشین دیگر وصل شود، مستندات چهار حمل‌ونقل را نام می‌برند: stdio به‌عنوان پیش‌فرض، سوکت یونیکس، وب‌سوکت و off. این را اجرا کردم و رفتار واقعی سه نقطه‌ی کنترلی را دیدم:

$ codex app-server --listen ws://127.0.0.1:4517 \
    --ws-auth capability-token --ws-token-file ./token.txt &
pid=1135437
--- listening socket:
LISTEN 0      128        127.0.0.1:4517       0.0.0.0:*

$ curl -s -o /dev/null -w "code=%{http_code}\n" http://127.0.0.1:4517/readyz
code=200
$ curl -s -o /dev/null -w "code=%{http_code}\n" http://127.0.0.1:4517/healthz
code=200
$ curl -s -o /dev/null -w "code=%{http_code}\n" -H "Origin: http://evil.example" http://127.0.0.1:4517/healthz
code=403

عدد ۴۰۳ با هدر Origin یعنی همان قاعده‌ای که مستندات نوشته‌اند: درخواست‌های دارای Origin رد می‌شوند. خود سرور هم در خروجی استانداردش این را تکرار می‌کند و می‌گوید فقط لوکال‌هاست باید گوش دهد.

=== upgrade WITHOUT token
code=401
missing websocket bearer token
=== upgrade WITH wrong token
code=401
invalid websocket bearer token
=== upgrade WITH right token
code=101

دو پیام خطا جدا از هم هستند: نبودن توکن و توکن نادرست. اگر لاگ احراز هویت خودتان را می‌خوانید، این تمایز به شما می‌گوید کلاینت چه اشتباهی کرده.

در خروجی استاندارد سرور، نشانی‌های سلامت را چاپ می‌کند و هشدار می‌دهد:

codex app-server (WebSockets)
  listening on: ws://127.0.0.1:4517
  readyz: http://127.0.0.1:4517/readyz
  healthz: http://127.0.0.1:4517/healthz
  note: binds localhost only (use SSH port-forwarding for remote access)

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

قدم بعدی شما

اگر تا اینجا آمده‌اید، یک کلاینت دارید که دست‌دادن می‌کند و بدون کلید API جواب واقعی می‌گیرد. قدم بعدی thread/start و بعد turn/start است، ولی هر دو به اعتبار واقعی نیاز دارند و در اجرای من به همین دلیل جلو نرفتم.

همین خط لوله‌ی محلی بدون کلید API یک کاربرد روشن دارد: تست دود روی کلاینت خودتان. پیش از آنکه اعتبار اضافه کنید، می‌توانید مطمئن شوید که handshake و پاسخ‌خوانی درست کار می‌کند. برای دیدن شکل دقیق پیام‌ها، نمونه‌ی Node.js در مستندات را کنار همین کلاینت پایتونی بگذارید.

دو پرچم را با احتیاط مصرف کنید. --listen ws:// و --ws-auth هر دو آزمایشی‌اند و خود مستندات می‌گویند برای بار کاری تولید پشتیبانی نمی‌شوند. اگر می‌خواهید بدانید در نسخه‌ی شما چه چیزی پایدار است، بدوی codex app-server daemon version را نگاه کنید.

برای دیدن اینکه چه نسخه‌ای روی ماشین‌تان است و چه چیزی کهنه است، چطور خرابی کدکس را با doctor پیدا کنیم را بخوانید. عدد 0.158.0 در هر دو نوشته از یک ماشین آمده است.

منابع

  1. مستندات Codex App Server: پروتکل، چهار حمل‌ونقل، دروازه‌ی experimentalApi و نمونه‌ی Node.js
  2. متن همان مستندات در مخزن کدکس، همراه با رفتار فشار بازگشت و کد خطای -32001
  3. مرجع خط فرمان کدکس و روش نصب با install.sh
  4. مرجع پیکربندی پیشرفته، از جمله محل CODEX_HOME و پروفایل‌ها
  5. انتشارهای کدکس در گیت‌هاب
  6. کدکس doctor چیست و چطور خرابی کدکس را پیدا کنیم