Odysseus Codex 集成:为终端 Codex Agent 打造 Scope 受限的 /api/codex/* 数据网关
本文将 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"),仓库中它的结构非常精简:
- integrations/codex/README.md — 本文主线,面向用户的安装与验证流程;
- integrations/codex/scripts/odysseus_api.py — 终端侧的 HTTP 客户端脚本,所有命令最终都打到
/api/codex/*; - integrations/codex/skills/odysseus/SKILL.md — Codex 读取的 Skill 说明书,定义了"什么时候用哪个接口"以及安全红线。
从源码结构看,这套集成的服务端实现集中在 routes/codex_routes.py:setup_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.js 的 AGENT_CONFIGS.codex.buildSetup(origin, token) 中——你在 UI 里复制的命令,就是把该页面 origin 和生成的 token 填进模板的产物。
同时,同一份 AGENT_CONFIGS 也服务于 Claude Agent(/api/claude/plugin.zip),两者共用同一套 scope 受限的 /api/codex/* 后端(routes/codex_routes.py 中 setup_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
每一步的作用:
- 两个环境变量是脚本的唯一凭据来源。odysseus_api.py 的
_config()只从ODYSSEUS_URL和ODYSSEUS_API_TOKEN读取配置,缺一即报错退出,并提示"create a Codex Agent token in Odysseus Settings"。SKILL.md 也明确要求:缺失时"do not guess credentials"(不要猜测凭据)。 /api/codex/plugin.zip动态打包整目录。服务端实现见 routes/codex_routes.py:plugin_zip端点要求require_authenticated_request,然后把integrations/codex目录(排除__pycache__与.pyc)整体打包为 zip 流式返回,Content-Disposition文件名为odysseus-codex-plugin.zip。- 解压到
~/plugins后,目录布局为~/plugins/odysseus/{scripts,skills},与 plugin_zip 中zf.write(path, Path("odysseus") / path.relative_to(root))的写入前缀一致。 - 内嵌 Python 脚本注册 marketplace:它读取(或初始化)
~/.agents/plugins/marketplace.json,确保存在name: personal的 marketplace,然后把odysseus插件以{"source": "local", "path": "./plugins/odysseus"}写入plugins数组——且是幂等的:先过滤掉已有的name == "odysseus"条目再追加,重复执行不会造成重复注册。 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 列表);tools:todos/email/memory/calendar/documents/cookbook六组工具,每组给出read/write(或launch)布尔值和actions列表——布尔值就是"token scopes 与该组允许的 scope 集合的交集是否为空";safety:email_send_requires_confirmation: true与destructive_actions_should_confirm: true两个安全声明。
SKILL.md 要求 Codex 在使用任何工具面前先检查 capabilities;email.read 为 false 时禁止查看邮件,应请用户在 Codex Agent 设置里开启对应开关。
Scope 模型:设置页开关如何变成 403
设置页的每个工具开关对应一个 scope key,完整清单在 static/js/settings.js 的 toolScopes 数组中:
| 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(可带archived、label)与POST /api/codex/todos,内部委托给do_manage_notes(list_todos、manage_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=all与GET /api/codex/emails/{uid}?folder=INBOX。服务端 list_emails 把limit钳制在 1..50,account_id非空时会用_assert_owns_account校验账户归属;两个 handler 都复用邮件路由既有的email_list_endpoint/email_read_endpoint并注入owner。 - 草稿(推荐路径):
POST /api/codex/emails/draft-document(codex_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 UID、emails draft-doc JSON_PAYLOAD(脚本)。
Memory、Calendar、Documents
- Memory:
GET /api/codex/memory(memory:read或memory:write)、POST /api/codex/memory与DELETE /api/codex/memory/{memory_id}(均需memory:write)。POST 体为{"text": "...", "category": "fact", "source": "user", "session_id": null},空text返回 400。 - Calendar:
GET /api/codex/calendar/events?start=ISO&end=ISO(可读 scope 为calendar:read或calendar:write)、POST /api/codex/calendar/events(体匹配EventCreate:summary、dtstart、dtend、all_day、description、location、calendar_href、rrule、color)、DELETE /api/codex/calendar/events/{uid}(uid是创建响应里返回的值)。 - Documents:
GET /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;体匹配 ServeRequest(repo_id/cmd/remote_host?/ssh_port?/...);cmd 必须通过 _validate_serve_cmd:首词二进制须在 vllm/python3/sglang/llama-server/ollama/node/npx 白名单内,且拒绝 cd、source、&&/` |
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 给出的调试循环是:tasks → output SID 600(定位根因,日志里引用"above"就加大 tail)→ stop SID → serve 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 常用的友好别名做了归一化(host→remote_host、model→repo_id),因为"传 host 却被静默映射到本地运行,是 agent 自己永远调不出来原因的 bug"(源码注释);_run_shell() 带 10–15s 超时兜底,超时会 kill 子进程。
客户端脚本 odysseus_api.py:第二道边界
odysseus_api.py 用纯标准库(urllib)实现,除封装上述命令外还有两条硬性防线:
- 路径白名单:任何最终路径若不以
/api/codex/开头,脚本直接拒绝并返回退出码 2——"refusing non-/api/codex path; use scoped Odysseus integration endpoints only"(L178-L182)。这意味着即使 token 本身有权限,脚本层面也不允许去打 Odysseus 的其他 API。 - 通用透传模式:
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 行为层面也知道该怎么做"——三层各自独立,任何单层失效都有另外两层兜底。
适用前提与限制
- 安装流程假设本机有
python3、curl和已登录的codexCLI;插件注册目标文件是~/.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 契约的活文档。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00