首页
/ Odysseus Codex Skill 深入解析:SKILL.md、Scoped API 与 Cookbook 模型调试通道

Odysseus Codex Skill 深入解析:SKILL.md、Scoped API 与 Cookbook 模型调试通道

2026-09-04 10:18:16作者:申梦珏Efrain

本文以 SKILL.md 为主体,完整讲解 Odysseus 官方 Codex 技能包的定义、配置与全部 /api/codex/* 端面的使用方法,并结合 codex_routes.py 与配套测试,还原其权限模型、命令校验与 Cookbook 模型服务调试闭环的底层实现。读完本文,你可以从零配好 Codex Agent token,让 Codex 通过受控 HTTP API 读写 Odysseus 的待办、邮件、记忆、日历、文档,并在 GPU 主机上排查、重启模型服务。

一、这个 Skill 是什么、在哪里

SKILL.md 是放在 integrations/codex/skills/odysseus/ 下的技能定义文件,YAML frontmatter 声明了技能名 odysseus 及其触发条件:

Use when the user asks Codex to read or write Odysseus data (todos, email, calendar, memory, documents) or to launch/monitor/stop a Cookbook model-serve task through the scoped Codex Agent API. Requires ODYSSEUS_URL and ODYSSEUS_API_TOKEN.

它与仓库内的 odysseus_api.py 辅助脚本一起,被打包为 Codex 插件分发(分发入口是 codex_routes.py 中的 GET /api/codex/plugin.zip,它把 integrations/codex 整棵子树打包为 zip 供终端会话下载)。同一个仓库还在 integrations/claude/skills/odysseus/SKILL.md 提供了 Claude Code 版本,运行时复用同一套 scope-gated /api/codex/* 端点——这一点在 codex_routes.pysetup_claude_routes() docstring 中有明确说明。

SKILL.md 的核心价值有两点:

  1. 给 Agent 划清数据访问的“唯一合法通道”:所有 Odysseus 用户数据的读写必须走 scoped HTTP API,且调用前先查 capabilities
  2. 给 Agent 一套可复制的操作手册:Todos、Email、Memory、Calendar、Documents、Cookbook 六个数据域各自的端点、请求体、所需 scope,以及 Cookbook 场景下的调试循环模式。

二、配置:两个环境变量与 token 的获取

SKILL.md 的 Configuration 一节要求终端会话具备两个环境变量:

  • ODYSSEUS_URL:用户 Odysseus 实例的 Base URL,例如 http://127.0.0.1:7000
  • ODYSSEUS_API_TOKEN:在 Odysseus 的 Settings > Integrations > Add Integration > Codex Agent 中创建的 scoped API token。

Skill 明确要求:任何一个值缺失时不要猜测凭据,而是提示用户去 Settings 创建 token 并把两个值暴露给终端会话。辅助脚本 odysseus_api.py_config() 正是这样实现的:读取并 strip 两个环境变量,缺失时向 stderr 打印 missing ODYSSEUS_URL / ODYSSEUS_API_TOKEN; create a Codex Agent token in Odysseus Settings 并返回 None(脚本以退出码 2 结束)。

完整安装流程

README.md 给出了用户侧的完整流程:

  1. 打开 Odysseus Settings > Integrations;
  2. Add a Codex Agent;
  3. 复制生成 token 后展示的完整 setup 命令;
  4. 勾选允许 Codex 使用的工具(即 scope 开关);
  5. 在终端 Codex 会话中执行:
export ODYSSEUS_URL=http://your-odysseus-host:7000
export ODYSSEUS_API_TOKEN=ody_generated_token
mkdir -p ~/plugins
curl -fsSL -H "Authorization: Bearer $ODYSSEUS_API_TOKEN" "$ODYSSEUS_URL/api/codex/plugin.zip" -o /tmp/odysseus-codex-plugin.zip
python3 -m zipfile -e /tmp/odysseus-codex-plugin.zip ~/plugins
python3 - <<'PY'
import json
from pathlib import Path

p = Path.home() / ".agents" / "plugins" / "marketplace.json"
p.parent.mkdir(parents=True, exist_ok=True)
if p.exists():
    data = json.loads(p.read_text())
else:
    data = {"name": "personal", "interface": {"displayName": "Personal"}, "plugins": []}

data.setdefault("name", "personal")
data.setdefault("interface", {}).setdefault("displayName", "Personal")
plugins = data.setdefault("plugins", [])
entry = {
    "name": "odysseus",
    "source": {"source": "local", "path": "./plugins/odysseus"},
    "policy": {"installation": "AVAILABLE", "authentication": "ON_INSTALL"},
    "category": "Productivity",
}
data["plugins"] = [item for item in plugins if item.get("name") != "odysseus"] + [entry]
p.write_text(json.dumps(data, indent=2) + "\n")
PY
codex plugin add odysseus@personal
  1. 验证安装:
python3 ~/plugins/odysseus/scripts/odysseus_api.py capabilities

从源码看,Settings 里的“工具开关”就是逐项的 scope 复选框。settings.jsshowAgentForm 渲染了 14 个 scope 开关:todos:read / todos:writedocuments:read / documents:writeemail:read / email:draft / email:sendcalendar:read / calendar:writememory:read / memory:writecookbook:read / cookbook:launch,其中 cookbook:launch 的说明文字直接写明它是“ Powerful: runs SSH commands on your configured servers, bounded by the same allowlist the UI uses”。token 按名称前缀区分归属(codex agent / claude agent,见 settings.jsAGENT_CONFIGS),保证 Codex 与 Claude 的 token 各走各的表单。

三、决策框架:什么数据进什么域

SKILL.md 的 “When to use what” 一节是这份技能的操作灵魂,它规定了四类意图的落点:

用户意图(示例) 正确落点 关键约束
提醒(“remind me at 5pm to do X”) TODO + due_date due_date 本身就是提醒:它会通过用户配置好的通道(浏览器/邮件/ntfy)自动触发通知。不要为提醒创建日历事件——名为 "Reminder" 的日历事件只是一个时间块,不会触发任何通知
日历事件(“meeting at 3pm”、“dentist Tuesday 10am”) calendar event 用于时间块、会议、预约、循环日程;事件本身的提醒需在 Odysseus 设置中单独配置
笔记 / 自由信息(“note that the wifi password is …”) memory 或无 due_date 的 todo 视其为“关于用户的事实”还是“行动项”二选一
用户的持久事实 / 偏好 memory

判定规则很明确:用户说了 “reminder” + 时间,默认走 TODO + due_date;只有用户显式提到 “calendar”、“event”、“meeting”、“appointment”,或描述的是一个时间段(time range),才切换到日历事件。

四、安全边界:唯一通道与 Forbidden Bypass Pattern

SKILL.md 的 Safety 一节逐条列出了 Agent 必须遵守的红线:

  • 所有 Odysseus 数据访问必须走 /api/codex/* 下的 scoped HTTP API;
  • 使用任一工具面前先检查 /api/codex/capabilities
  • 403 视为 Settings 的有意限制,不要绕过
  • 禁止用 SSH、Docker、直接 Python import、SQLite 查询、MCP 内部件、浏览器 cookie 或本地文件来读写 Odysseus 用户数据;
  • 即使拥有 shell 访问权,也不得直接调用 do_manage_notes、邮件 MCP 内部件或数据库会话;
  • 除非用户显式要求发送且 token 持有发送 scope,否则绝不直接发送邮件;
  • 所有动作限定在 token owner 的作用域内。

文末的 “Forbidden Bypass Pattern” 是对 Agent 的最后兜底:如果你正打算接触 Odysseus 主机/容器、import 应用内部件、查数据库或直接调 MCP 辅助模块,停下来——这些路径都绕开了 Settings 与 token scope,正确做法是请用户打开对应的 Codex Agent 工具开关。

这套“唯一通道”在两侧都有硬性实现:

  1. 客户端侧odysseus_api.py 在发出任何请求前强制校验路径以 /api/codex/ 开头,否则打印 refusing non-/api/codex path; use scoped Odysseus integration endpoints only 并以退出码 2 拒绝;请求统一携带 Authorization: Bearer <token> 头。
  2. 服务端侧codex_routes.py_scope_owner() 对每个请求检查 api_token_scopes 与目标 scope 集合的交集,缺 scope 时返回 403 并列出所需 scope(如 API token missing required scope: email:send);owner 来自 token 绑定的 api_token_owner,从而保证数据永远落在 token 属主名下。cookie 会话调用则走 require_user,且 Cookbook 类端点额外要求 admin(见下文 第六节)。

GET /api/codex/capabilities 的响应结构在 codex_routes.py 中定义:返回 token_scopes 全量列表,以及 todos / email / memory / calendar / documents / cookbook 六个工具面各自的 read / write(或 draft / send / launch)布尔值与 actions 清单,最后还固定返回 safety: {email_send_requires_confirmation: true, destructive_actions_should_confirm: true},把安全约定直接写进了能力声明里。

五、五个数据域的端点与命令

5.1 Todos(待办与提醒)

Codex API 支持 todos/checklists 两个端点:

  • GET /api/codex/todos(要求 todos:read,服务端还支持 archivedlabel 查询参数,见 codex_routes.py
  • POST /api/codex/todos(写操作要求 todos:write,读操作要求 todos:read

服务端实现是复用既有工具层:manage_todos 从 body 取出 action(缺省为 add),归一化后判断属于写动作集合 {"add", "create", "new", "save", "remind", "update", "delete", "toggle_item", "remove", "remove_item"} 还是读动作,分别要求 todos:writetodos:read,最终把整个 body 序列化后交给 do_manage_notes 执行——也就是说 Codex 与 UI 内部工具走的是同一套业务逻辑,只是权限判定从“登录态”换成了“token scope”。

支持的 todo 动作为 listaddupdatedeletetoggle_item。辅助脚本用法:

python3 integrations/codex/scripts/odysseus_api.py capabilities
python3 integrations/codex/scripts/odysseus_api.py todos list
python3 integrations/codex/scripts/odysseus_api.py todos add "Follow up"

提醒(带 due_date 的 todo)是这套 API 最容易踩坑的地方。SKILL.md 特别指出:后端会把 due_date 解析为结构化提醒,因此含时间的待办必须通过通用 POST 发送 due_date 字段,而不是把时间塞进标题字符串里——todos add TITLE 快捷方式只会设置标题。示例:

python3 integrations/codex/scripts/odysseus_api.py POST /api/codex/todos '{"action":"add","title":"Call dentist","due_date":"tomorrow at 5pm"}'

后端同时接受 ISO 时间戳与自然语言("tomorrow 5pm""next Monday 9am""in 2 hours"),并锚定到用户所在时区。

5.2 Email(读取与草稿/发送)

读取:

  • GET /api/codex/emails?folder=INBOX&limit=10&offset=0&filter=all
  • GET /api/codex/emails/{uid}?folder=INBOX
python3 integrations/codex/scripts/odysseus_api.py emails list 5
python3 integrations/codex/scripts/odysseus_api.py emails read UID

服务端 list_emails 要求 email:read(或 email:draft / email:send),并对 limit 做了 1–50 的钳制、offset 钳制为 ≥ 0;传 account_id 时会先经 _assert_owns_account 确认该账户属于 token owner,然后直接复用邮件路由的 GET /api/email/listGET /api/email/read/{uid} 端点函数。若 capabilitiesemail.read 不为 true,SKILL.md 要求不要窥视邮件,请用户去 Codex Agent 设置里打开 Email read 开关。

草稿与发送(SKILL.md 的 “Email draft + send” 一节):

  • 优先使用 POST /api/codex/emails/draft-document:它创建一份 language: "email" 的可编辑 Odysseus Document,不触碰 IMAP 也不发送。服务端实现 codex_email_draft_document 同时校验 email:draft(或 email:send)与 documents:write 两个 scope,且要求两个 scope 解析出的 owner 一致(不一致直接 403 API token owner mismatch),把 To/Cc/Bcc/Subject/In-Reply-To/References 与正文拼成邮件头格式的文档内容,返回中带 draft_type: "document"send_required_confirmation: true
  • POST /api/codex/emails/draft:body 匹配 SendEmailRequesttoccbccsubjectbodybody_htmlattachmentsaccount_idin_reply_toreferences),要求 email:draft(或 email:send);
  • POST /api/codex/emails/send:同一 body,要求 email:send未经用户明确指示绝不发送——capabilities 的 safety.email_send_requires_confirmation: true 与此呼应。

5.3 Memory(记忆)

  • GET /api/codex/memory —— 列出 token owner 的记忆(memory:readmemory:write
  • POST /api/codex/memory —— body 为 {"text": "...", "category": "fact", "source": "user", "session_id": null},要求 memory:write
  • DELETE /api/codex/memory/{memory_id} —— 删除一条记忆,要求 memory:write
python3 integrations/codex/scripts/odysseus_api.py GET /api/codex/memory
python3 integrations/codex/scripts/odysseus_api.py POST /api/codex/memory '{"text":"User prefers SI units","category":"preference"}'

服务端 codex_memory_add 会把 payload 映射为 MemoryAddRequestcategory 缺省 "fact"source 缺省 "user"),空文本返回 400,随后经由 _as_owner() 以 owner 身份调用记忆路由的 add 端点。

_as_owner()codex_routes.py)是理解整个 Codex 桥的关键机制:它把 request.state.current_user 临时替换为 token owner、api_token 置为 False,让被复用的既有路由内部 get_current_user / require_user 看到的是“受 scope 保护的属主”而非 bearer 中间件设置的 api 伪用户,执行完再还原。

5.4 Calendar(日历)

  • GET /api/codex/calendar/events?start=ISO&end=ISO —— 列出窗口内事件(calendar:readcalendar:write
  • POST /api/codex/calendar/events —— body 匹配 EventCreatesummarydtstartdtendall_daydescriptionlocationcalendar_hrefrrulecolor),要求 calendar:write
  • DELETE /api/codex/calendar/events/{uid} —— 按 uid 删除事件(uid 即 POST 响应中返回的值),要求 calendar:write

服务端 codex_calendar_create 会把 body 直接构造为日历路由的 EventCreate 模型,校验失败返回 400,成功后经 _as_owner 调用既有端点。

5.5 Documents(文档库)

  • GET /api/codex/documents?search=...&limit=50 —— 分页文档库(documents:readdocuments:write)。服务端 codex_documents_library 支持 searchlanguagesort(缺省 recent)、offsetlimit(钳制在 1–50)、archived 参数,并在响应中自动附加 next_offset(到末尾时为 null),方便 Agent 翻页;
  • GET /api/codex/documents/{doc_id} —— 取单篇文档;
  • POST /api/codex/documents —— body {"session_id": "...", "title": "...", "content": "...", "language": "markdown"},要求 documents:write
  • DELETE /api/codex/documents/{doc_id} —— 删除文档,要求 documents:write

5.6 capabilities 速查表

把 SKILL.md 各节要求的 scope 与服务端 scope 常量(codex_routes.pyTODO_READ_SCOPES 等)对照:

工具面 读 scope 写 scope actions(capabilities 返回值)
todos todos:readtodos:write 亦满足) todos:write list, add, update, delete, toggle_item
email email:read / email:draft / email:send draft 需 email:draft(或 email:send);send 需 email:send list, read, draft_document, draft, send
memory memory:read / memory:write memory:write list, add, delete
calendar calendar:read / calendar:write calendar:write list_events, create_event, delete_event
documents documents:read / documents:write documents:write library, read, create, delete
cookbook cookbook:read / cookbook:launch cookbook:launch tasks, servers, output, serve, stop

六、Cookbook 模型服务调试通道

这是 SKILL.md 中最实战的部分:当用户排查一个起不来的模型服务(compute-capability 报错、OOM、缺 kernel、attention backend 选错等)时,Agent 通过 Cookbook 面复刻人工在 Odysseus → Cookbook UI 里的完整操作——看哪些 serve 在跑、tail 其 tmux 输出找崩溃原因、改启动命令、重启、杀掉卡死的任务。

端点清单(均要求对应 scope):

端点 作用 scope
GET /api/codex/cookbook/tasks 列出活跃的 serve/download/install 任务(sessionId、type、status、repo_id、remoteHost、payload._cmd) cookbook:read
GET /api/codex/cookbook/servers 列出已配置服务器(name、host、port、env 类型+路径、modelDirs) cookbook:read
GET /api/codex/cookbook/cached?host=<NAME> 列出指定服务器上已缓存的模型(HF cache + Ollama + 额外 modelDirs)。serve 之前先调它看磁盘上已有什么 cookbook:read
GET /api/codex/cookbook/presets 列出已保存的 serve preset(model + host + port + cmd)。用户保存的 preset 通常就有能跑通的 cmd——先试 preset NAME 再自己拼 cookbook:read
GET /api/codex/cookbook/output/{session_id}?tail=400 读任务持久化日志文件最后 N 行(首选)或 tmux pane(回退)。日志文件跨 vllm 崩溃存活,因此能返回真正的 Python traceback,即使 pane 已被 bash prompt + neofetch banner 覆盖。默认 tail=400 cookbook:read
POST /api/codex/cookbook/serve 启动 serve 任务。body 匹配 ServeRequest{ repo_id, cmd, remote_host?, ssh_port?, env_prefix?, gpus?, platform? } cookbook:launch
POST /api/codex/cookbook/preset/{name} 按名字启动已保存 preset,复用用户已验证的 cmd + host cookbook:launch
POST /api/codex/cookbook/adopt 把外部启动的 tmux session 登记进 cookbook 跟踪。body { tmux_session, model, host?, port? }。当 serve_model 拒绝某 cmd、你回退到直接 ssh+tmux 时使用——不 adopt 的话 UI 看不到该 session cookbook:launch
POST /api/codex/cookbook/stop/{session_id} 杀掉 tmux session cookbook:launch

辅助脚本用法(注意 README 安装到 ~/plugins 后路径是 ~/plugins/odysseus/scripts/odysseus_api.py):

python3 ~/plugins/odysseus/scripts/odysseus_api.py cookbook tasks
python3 ~/plugins/odysseus/scripts/odysseus_api.py cookbook output serve-abc12345 400
python3 ~/plugins/odysseus/scripts/odysseus_api.py cookbook stop serve-abc12345
python3 ~/plugins/odysseus/scripts/odysseus_api.py cookbook serve \
  /mnt/HADES/models/Qwen3.5-397B-A17B-AWQ \
  "vllm serve /mnt/HADES/models/Qwen3.5-397B-A17B-AWQ --host 0.0.0.0 --port 8001 --tensor-parallel-size 8 --max-model-len 262144 --gpu-memory-utilization 0.90 --dtype auto --max-num-seqs 8 --trust-remote-code --enable-expert-parallel --enable-auto-tool-choice --tool-call-parser qwen3_coder --reasoning-parser qwen3" \
  pewds@192.168.1.12

调试循环模式(SKILL.md 原文)tasksoutput SID 600(找根因;若输出引用了“上文”就请求更大的 tail)→ stop SIDserve repo "new cmd" → 等待约 20 秒 → 对新 sessionId 再 output

该面在源码层面强制的硬性限制:

  • cookbook serve 的 cmd 白名单 + shell 元字符拒绝POST /cookbook/servecodex_routes.py)最终委托给 /api/model/serve,由其中的 _validate_serve_cmd 校验:首个二进制必须是 vllm / python3 / sglang / llama-server / ollama / node / npx 之一,任何 cd …source … 前缀或 && / || / ; / $(...) 链式都会被拒。venv 激活(env_prefix)由宿主机保存的配置自动注入,所以 cmd 只传裸二进制 + 参数。服务端还做了防呆归一化:把 Agent 常用的别名 hostremote_hostmodelrepo_id 自动映射,避免“传了 host 却没生效、任务悄悄跑在本地”这种 Agent 自己永远查不出来的 bug;
  • cookbook stop / output 的 session_id 正则:必须匹配 [a-zA-Z0-9_-]+outputstop),否则 400;outputtail 钳制在 20–4000;
  • cookbook:launch 确实很强——/api/model/serve 会在用户主机上执行 SSH 命令,所以 codex_routes.py 的注释明确:cookbook:read 只能列任务 + tail 输出 + 列服务器;cookbook:launch 才能 start/stop(等价于宿主机 shell exec);
  • 密钥脱敏:返回给 Agent 的任务经 _redact_task 剥离 hf_token_secrets 等字段;servers 只保留挑主机所需的最小字段;
  • Agent 可以拉起占用 GPU 的长驻进程——因此 SKILL.md 要求重启前必须 cookbook stop 上一次尝试,避免多实例抢占显存。

一个容易被忽略的安全细节:output / stop 要从 cookbook_state.json 里解析任务的 remoteHost / sshPort 拼进 ssh 命令,_ssh_prefix_for_task 会先经 validate_remote_host / validate_ssh_port 校验,被篡改的 host 含 shell 元字符时返回 400 而不是注入执行——tests/test_codex_ssh_host_validation.py{"remoteHost": "box; rm -rf ~"} 这类篡改条目钉死了这个回归。

七、双通道权限模型:API token 走 scope,cookie 会话走 admin

Cookbook 面暴露宿主机拓扑、任务日志、tmux 命令与模型服务控制,因此 codex_routes.py 为它单独设了 _require_cookbook_scope:API token 调用方只受 scope 约束;cookie 会话调用方则额外要求 admin 权限tests/test_codex_cookbook_admin_gate.py(对应 issue #4542 的回归测试)验证了四条行为:非 admin cookie 会话访问 read/launch 均 403;admin cookie 会话放行且 owner 为其本人;API token 带 cookbook:read 放行、scope 不符 403。测试里还有一个静态检查 TestSourceCodeGate:扫描 codex_routes.py 源码,确保所有 cookbook 路由都走 _require_cookbook_scope 而非裸 _scope_owner

这解释了 SKILL.md 为什么把 403 定义为“Settings 的有意限制”:对 token 而言 403 意味着 scope 未授予(该去 Settings 打开开关);对普通 cookie 会话访问 Cookbook 而言 403 意味着权限等级不足(该用管理员账号或 token)。

八、速查:从 SKILL.md 到源码的映射

SKILL.md 规则 服务端实现 测试佐证
只允许 /api/codex/* 客户端脚本拒非 codex 路径;服务端 router 前缀 /api/codex
先查 capabilities /api/codex/capabilities 按 scope 交集输出各工具面可用性
403 = Settings 限制,勿绕过 _scope_owner / _scope_owner_all 缺 scope 即 403 并列出所需 scope test_codex_cookbook_admin_gate.py
owner 隔离 _as_owner 把既有路由跑在 token owner 身份下 test_codex_ssh_host_validation.py
serve cmd 白名单、禁 shell 元字符 复用 UI 的 _validate_serve_cmd(vllm/python3/sglang/llama-server 等) test_codex_ssh_host_validation.py(host/port 注入回归)
邮件发送需显式确认 safety.email_send_requires_confirmation、draft-document 返回 send_required_confirmation: true
重启前先 stop session_id 正则 + tmux kill-session 封装

九、小结与使用前提

这套 Codex skill 的设计要点可以概括为:一份 SKILL.md 定义 Agent 的决策与操作手册,一个 odysseus_api.py 在客户端钉死“只走 scoped API”的纪律,一个 /api/codex/* 路由面在服务器端用 scope + owner + 白名单三重闸门复用既有业务端点。使用时需要满足的前提:

  1. Odysseus 实例已在 ODYSSEUS_URL 可达的地址运行(默认端口 7000);
  2. 用户在 Settings > Integrations 中创建了 Codex Agent token 并勾选了所需 scope;
  3. 终端会话导出 ODYSSEUS_URLODYSSEUS_API_TOKEN 后,通过 /api/codex/plugin.zip 分发安装插件到 ~/plugins
  4. 涉及 Cookbook 调试时,宿主机/远程服务器上已有可用的 tmux 与模型运行时环境,且 token 持有 cookbook:launch

遇到 403 时的正确动作永远只有一个:回到 Odysseus Settings,打开对应的 Codex Agent 工具开关。

登录后查看全文
热门项目推荐
相关项目推荐