首页
/ Odysseus Codex 集成:为终端 Codex Agent 打造 Scope 受限的 /api/codex/* 数据网关

Odysseus Codex 集成:为终端 Codex Agent 打造 Scope 受限的 /api/codex/* 数据网关

2026-09-04 18:33:39作者:柏廷章Berta

本文将 integrations/codex/README.md 作为主线,完整走一遍 Odysseus 的 Codex 插件接入流程:在 Settings > Integrations 中创建 Codex Agent、生成带 scope 的 API token、下载并注册 ~/plugins/odysseus 插件、通过 odysseus_api.py 验证 capabilities,并结合 routes/codex_routes.py 的源码,讲清 token scope 如何逐一落到 todos、email、memory、calendar、documents 与 Cookbook 六组受限端点上,最终让外部 Codex 终端会话在"只走 Odysseus 官方设置、绝不绕过"的约束下安全地读写你的个人数据。

集成定位:插件包、Skill 与受限 API 三层结构

integrations/codex/ 目录是 Odysseus 提供给 Codex 的插件/技能捆绑包(README 原文称之为 "Codex plugin/skill bundle"),仓库中它的结构非常精简:

从源码结构看,这套集成的服务端实现集中在 routes/codex_routes.pysetup_codex_routes() 注册了一个前缀为 /api/codex 的 FastAPI Router,其文件头注释明确说明了设计意图——"small HTTP surfaces intended for the Codex plugin/MCP bridge. They reuse existing Odysseus helpers and enforce API-token scopes before touching user data"(小体量 HTTP 面,复用 Odysseus 既有 helper,并在触碰用户数据前强制 API-token scope)。也就是说,Codex 集成不是新造一套数据访问层,而是在既有 email/memory/calendar/document/cookbook 路由之上加了一层 scope 网关。

用户流程:从 Settings 到 codex plugin add 的完整操作

README 定义了一条 6 步操作链,下面逐步展开。

第 1–4 步:在 Settings > Integrations 中创建 Codex Agent

在 Odysseus 的 Settings > Integrations 页面添加一个 Codex Agent,系统会生成一个 token,并在其下方直接展示"完整安装命令"。前端的安装命令模板定义在 static/js/settings.jsAGENT_CONFIGS.codex.buildSetup(origin, token) 中——你在 UI 里复制的命令,就是把该页面 origin 和生成的 token 填进模板的产物。

同时,同一份 AGENT_CONFIGS 也服务于 Claude Agent(/api/claude/plugin.zip),两者共用同一套 scope 受限的 /api/codex/* 后端(routes/codex_routes.pysetup_claude_routes() 的 docstring 也确认了这一点)。

第 5 步:配置终端环境变量并安装插件

在 Codex 的终端会话中执行(与 README 完全一致,${origin} 即你的 Odysseus 地址):

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. 两个环境变量是脚本的唯一凭据来源odysseus_api.py_config() 只从 ODYSSEUS_URLODYSSEUS_API_TOKEN 读取配置,缺一即报错退出,并提示"create a Codex Agent token in Odysseus Settings"。SKILL.md 也明确要求:缺失时"do not guess credentials"(不要猜测凭据)。
  2. /api/codex/plugin.zip 动态打包整目录。服务端实现见 routes/codex_routes.pyplugin_zip 端点要求 require_authenticated_request,然后把 integrations/codex 目录(排除 __pycache__.pyc)整体打包为 zip 流式返回,Content-Disposition 文件名为 odysseus-codex-plugin.zip
  3. 解压到 ~/plugins 后,目录布局为 ~/plugins/odysseus/{scripts,skills},与 plugin_zipzf.write(path, Path("odysseus") / path.relative_to(root)) 的写入前缀一致。
  4. 内嵌 Python 脚本注册 marketplace:它读取(或初始化)~/.agents/plugins/marketplace.json,确保存在 name: personal 的 marketplace,然后把 odysseus 插件以 {"source": "local", "path": "./plugins/odysseus"} 写入 plugins 数组——且是幂等的:先过滤掉已有的 name == "odysseus" 条目再追加,重复执行不会造成重复注册。
  5. codex plugin add odysseus@personal 让 Codex 从 personal marketplace 中安装该本地插件。

第 6 步:验证安装

python3 ~/plugins/odysseus/scripts/odysseus_api.py capabilities

该命令请求 GET /api/codex/capabilities,返回体(见 capabilities 端点)包含:

  • integration: "codex"token_scopes(该 token 实际持有的 scope 列表);
  • toolstodos / email / memory / calendar / documents / cookbook 六组工具,每组给出 read/write(或 launch)布尔值和 actions 列表——布尔值就是"token scopes 与该组允许的 scope 集合的交集是否为空";
  • safetyemail_send_requires_confirmation: truedestructive_actions_should_confirm: true 两个安全声明。

SKILL.md 要求 Codex 在使用任何工具面前先检查 capabilitiesemail.readfalse 时禁止查看邮件,应请用户在 Codex Agent 设置里开启对应开关。

Scope 模型:设置页开关如何变成 403

设置页的每个工具开关对应一个 scope key,完整清单在 static/js/settings.jstoolScopes 数组中:

Scope 设置页 Label 作用
todos:read / todos:write Todos / Todos write 读取 / 创建、更新、删除、切换 todo 项
documents:read / documents:write Documents / Documents write 读取文档库 / 创建、更新草稿文档
email:read / email:draft / email:send Email / Email drafts / Email send 读邮件(需已启用邮件 API)/ 建草稿不发送 / 直接发送
calendar:read / calendar:write Calendar / Calendar write 读日历事件 / 创建、更新事件
memory:read / memory:write Memory / Memory write 读 / 写记忆
cookbook:read Cookbook 列 cookbook 任务、tail tmux 输出
cookbook:launch Cookbook launch 启动/停止 serve 任务(强警告:会在已配置服务器上执行 SSH 命令,受与 UI 相同的命令白名单约束)

服务端把每个 scope 集合定义为常量(routes/codex_routes.py),例如 EMAIL_READ_SCOPES = {"email:read", "email:draft", "email:send"}——注意读邮件允许三个 scope 中任意一个,因为持 email:send 的 token 必然也需要读。核心鉴权函数是 _scope_owner()

  • 若请求由 API token 发起(request.state.api_token 为真),先校验 token scopes 与允许集合有交集,否则抛 403 "API token missing required scope: ...";再取 api_token_owner 作为数据属主;
  • 若是 cookie 会话则回落到 require_user()
  • 数据一律以解析出的 owner 执行,保证 agent 只能看到 token 属主自己的数据。

Cookbook 面还有额外收紧:_require_cookbook_scope() 对非 API-token(即浏览器 cookie)调用方追加 require_admin(request),理由是"cookbook surfaces expose host topology, task logs, tmux commands, and model-serving controls"。相关行为有测试佐证:tests/test_codex_cookbook_admin_gate.py(cookie 调用方的 admin 门槛)与 tests/test_codex_ssh_host_validation.py(SSH host 校验)。

数据面端点逐组拆解

Todos(自然语言时间 = 提醒)

  • GET /api/codex/todos(可带 archivedlabel)与 POST /api/codex/todos,内部委托给 do_manage_noteslist_todosmanage_todos)。
  • POST 的 action 字段区分读写:属于 WRITE_ACTIONS = {add, create, new, save, remind, update, delete, toggle_item, remove, remove_item} 的需要 todos:write,否则按读处理(仅 todos:read)。
  • SKILL.md 给出了一个关键实践:todos add TITLE 快捷方式只写标题;带时间的提醒应走通用 POST 传 due_date,后端会解析自然语言("tomorrow at 5pm""next Monday 9am""in 2 hours" 或 ISO 时间戳),并锚定到用户时区。示例:
python3 ~/plugins/odysseus/scripts/odysseus_api.py POST /api/codex/todos '{"action":"add","title":"Call dentist","due_date":"tomorrow at 5pm"}'
  • SKILL.md 还划了一条语义边界:提醒("5pm 提醒我做 X")= 带 due_date 的 TODO,due_date 才是触发通知的机制(经用户配置的通知渠道);日历事件只是时间块,"名为 Reminder 的日历事件不会触发通知"。

Email(读、草稿、发送三级)

  • 读:GET /api/codex/emails?folder=INBOX&limit=10&offset=0&filter=allGET /api/codex/emails/{uid}?folder=INBOX。服务端 list_emailslimit 钳制在 1..50,account_id 非空时会用 _assert_owns_account 校验账户归属;两个 handler 都复用邮件路由既有的 email_list_endpoint/email_read_endpoint 并注入 owner
  • 草稿(推荐路径):POST /api/codex/emails/draft-documentcodex_email_draft_document)把 to/cc/bcc/subject/in_reply_to/references/body 组装成带 To:/Subject:/--- 结构的文本,创建一个 language: "email" 的 Odysseus Document,完全不触碰 IMAP/发送,且要求同时持有 email:draft(或 email:send)和 documents:write 且属主一致,否则 403。响应附 send_required_confirmation: true
  • 直发:POST /api/codex/emails/draft 需要 email:draft(或 email:send);POST /api/codex/emails/send 严格要求 email:send。SKILL.md 强调"Never send without explicit user instruction"(无明确指令绝不发送)。
  • 脚本快捷命令:emails list [limit](默认 10)、emails read UIDemails draft-doc JSON_PAYLOAD脚本)。

Memory、Calendar、Documents

  • MemoryGET /api/codex/memorymemory:readmemory:write)、POST /api/codex/memoryDELETE /api/codex/memory/{memory_id}(均需 memory:write)。POST 体为 {"text": "...", "category": "fact", "source": "user", "session_id": null},空 text 返回 400。
  • CalendarGET /api/codex/calendar/events?start=ISO&end=ISO(可读 scope 为 calendar:readcalendar:write)、POST /api/codex/calendar/events(体匹配 EventCreatesummarydtstartdtendall_daydescriptionlocationcalendar_hrefrrulecolor)、DELETE /api/codex/calendar/events/{uid}uid 是创建响应里返回的值)。
  • DocumentsGET /api/codex/documents?search=...&limit=50(分页,响应里附 next_offset,见 codex_documents_library)、GET /api/codex/documents/{doc_id}POST /api/codex/documents(体:session_id/title/content/language,默认语言 markdown)、DELETE /api/codex/documents/{doc_id}。写操作需 documents:write

Cookbook:给 Agent 的模型服务调试闭环

这是 SKILL.md 中篇幅最大的一块,用途是"复现一个人类在 Odysseus → Cookbook 里手动做的事":看哪些 serve 在跑、tail 它们的 tmux 输出找崩溃原因、改启动命令、重启、杀卡死任务。典型场景是模型服务器起不来(compute-capability 错误、OOM、缺 kernel、attention 后端选错)。

端点与 scope(实现见 routes/codex_routes.py,读面还需 cookbook:read/cookbook:launch 之一,cookie 调用方另需 admin):

端点 说明
GET /cookbook/tasks 列活跃 serve/download/install 任务;返回前经 _redact_task 剥掉 hf_token_secrets 等字段
GET /cookbook/servers 列配置服务器(name/host/port/env/modelDirs);剥离 SSH 凭据,仅保留选主所需字段
GET /cookbook/cached?host=NAME 列该服务器上已缓存模型(HF cache + Ollama + 额外 modelDirs),serve 前先查
GET /cookbook/presets 列保存的 serve 预设(model+host+port+cmd);用户保存的预设通常就是能跑的 cmd
GET /cookbook/output/{session_id}?tail=400 读任务持久日志(优先 /tmp/odysseus-tmux/{sid}.log,旧任务回退 tmux pane);session_id 必须匹配 [a-zA-Z0-9_-]+,远程任务经 validate_remote_host/validate_ssh_port 校验后拼 SSH 命令
POST /cookbook/serve 启动 serve;体匹配 ServeRequestrepo_id/cmd/remote_host?/ssh_port?/...);cmd 必须通过 _validate_serve_cmd:首词二进制须在 vllm/python3/sglang/llama-server/ollama/node/npx 白名单内,且拒绝 cdsource&&/`
POST /cookbook/preset/{name} 按名字启动已保存预设,复用用户已验证的 cmd + host
POST /cookbook/adopt 把外部 ssh+tmux 拉起的会话登记进 cookbook 跟踪(体:tmux_session/model/host?/port?);先 tmux has-session 验证会话存在,再经 atomic_write_json 写入状态文件
POST /cookbook/stop/{session_id} 杀 tmux 会话(本地 tmux kill-session 或对应 SSH 目标);session_id 同样受字符集校验

SKILL.md 给出的调试循环是:tasksoutput SID 600(定位根因,日志里引用"above"就加大 tail)→ stop SIDserve repo "new cmd" → 等约 20s → 对新 sessionId 再 output。示例(SKILL.md 原文):

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

另有两个值得注意的实现细节:/cookbook/serve 对 agent 常用的友好别名做了归一化(hostremote_hostmodelrepo_id),因为"传 host 却被静默映射到本地运行,是 agent 自己永远调不出来原因的 bug"(源码注释);_run_shell() 带 10–15s 超时兜底,超时会 kill 子进程。

客户端脚本 odysseus_api.py:第二道边界

odysseus_api.py 用纯标准库(urllib)实现,除封装上述命令外还有两条硬性防线:

  1. 路径白名单:任何最终路径若不以 /api/codex/ 开头,脚本直接拒绝并返回退出码 2——"refusing non-/api/codex path; use scoped Odysseus integration endpoints only"(L178-L182)。这意味着即使 token 本身有权限,脚本层面也不允许去打 Odysseus 的其他 API。
  2. 通用透传模式odysseus_api.py METHOD /api/codex/path [json-body] 可调用任意受限端点(如前文 todos 的 due_date 示例),body 会先做 JSON 解析校验,再带 Authorization: Bearer 头发出,超时 20s,HTTP 错误时把响应体打到 stderr 并返回 1。

安全红线:禁止的绕过模式

README 结尾与 SKILL.md 的 "Safety" / "Forbidden Bypass Pattern" 章节共同划定红线:Codex 访问用户数据必须/api/codex/*;SSH、Docker、直接 Python import、SQLite 查询、MCP 内部模块、浏览器 cookie、本地文件一律禁止用于读写 Odysseus 用户数据;甚至 shell 可用时也不得调用 do_manage_notes、email MCP internals 或数据库 session。收到 403 时应视为"有意的设置限制",正确做法是请用户打开对应工具开关,而不是找替代路径。SKILL.md 的收尾句是:"If you are about to reach the Odysseus host/container, import app internals, query the database, or call MCP helper modules directly, stop."

从架构上看,这套设计是自洽的:服务端 routes/codex_routes.py 保证"scope 不足即 403、数据只属于 token owner",客户端脚本保证"只能打 /api/codex/*",SKILL.md 保证"agent 行为层面也知道该怎么做"——三层各自独立,任何单层失效都有另外两层兜底。

适用前提与限制

  • 安装流程假设本机有 python3curl 和已登录的 codex CLI;插件注册目标文件是 ~/.agents/plugins/marketplace.json(personal marketplace)。
  • 端口 7000 是 README 示例地址,实际以你的 Odysseus 部署为准;ODYSSEUS_URL 支持任意 host:port。
  • email/memory/calendar/documents 各组端点在对应集成未启用时会返回 503 "…integration is not available"(源码中通过 _find_endpoint 探测既有路由是否存在,见 setup_codex_routes)。
  • cookbook:launch 能力最强——它最终执行的是"在用户配置的服务器上跑 SSH 命令",与 Cookbook UI 共用同一套命令白名单沙箱,授权前请明确这一点。

小结

Odysseus 的 Codex 集成把一个"外部终端 AI agent 能碰哪些数据"的问题,收敛为三件可验证的事:设置页上的 scope 开关(token scopes)、/api/codex/capabilities 的返回体、以及客户端脚本对 /api/codex/* 的硬编码路径白名单。安装只需六步命令,日常使用则以 odysseus_api.py 的一个入口覆盖 todos、email、memory、calendar、documents 与 Cookbook 六类数据面——这使它既可以作为 Codex 插件使用,也可作为人工排查时查阅受限 API 契约的活文档。

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