浏览代码

fix: Codex 用 agents.enabled=false 真正关闭子代理

- 实测(codex 0.164,生产账号):只设 features.multi_agent=false 时,模型仍能调用 collaboration.spawn_agent
  创建并等待子代理;加上 agents.enabled=false 后工具消失,经 AgentServer 的对话也确认无法创建
- 每次对话(包括 Playground)都关闭子代理:子线程的事件、审批、中断与用量目前不经过 AgentServer
- 文档记录以后开放子代理前需要补齐的部分(线程归属、审批转发、中断级联、网关令牌清理、并发上限)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jerry Liang 1 周之前
父节点
当前提交
a8781493d7
共有 3 个文件被更改,包括 11 次插入 和 2 次删除
  1. 5 1
      apps/server/src/providers/codex/permissions.ts
  2. 3 0
      apps/server/test/codex.test.ts
  3. 3 1
      docs/design/03-providers.md

+ 5 - 1
apps/server/src/providers/codex/permissions.ts

@@ -19,7 +19,7 @@ export const DISABLED_FEATURES = {
   apps: false,
   tool_suggest: false,
   skill_mcp_dependency_install: false,
-  // 子代理的事件与审批不经过 AgentServer
+  // 子代理的事件与审批不经过 AgentServer。只关这个开关不够,真正生效的是下面的 agents.enabled = false
   multi_agent: false,
   browser_use: false,
   computer_use: false,
@@ -33,6 +33,7 @@ export interface CodexThreadConfig {
   permissions: Record<string, { filesystem: FsTable; network: { enabled: boolean } }>
   shell_environment_policy: { inherit: 'core'; set: Record<string, string> }
   features: Record<string, boolean>
+  agents: { enabled: boolean }
   [key: string]: unknown
 }
 
@@ -82,6 +83,9 @@ export function buildThreadConfig(opts: {
   const agentDir = path.join(access.workspaceDir, '.agent')
   return {
     features: opts.allowPlugins ? { ...DISABLED_FEATURES, plugins: true, remote_plugin: true, apps: true } : { ...DISABLED_FEATURES },
+    // 关闭子代理(collaboration.spawn_agent 等)。已实测(2026-10-23,codex 0.164):只设 features.multi_agent = false 时
+    // 模型仍能创建并等待子代理;加上 agents.enabled = false 后工具消失。Playground 同样关闭
+    agents: { enabled: false },
     // 未开放插件时,即使有应用被启用也不暴露连接器工具
     apps: opts.allowPlugins ? { _default: { enabled: true } } : { _default: { enabled: false, destructive_enabled: false, open_world_enabled: false } },
     default_permissions: PROFILE_NAME,

+ 3 - 0
apps/server/test/codex.test.ts

@@ -66,6 +66,7 @@ describe('CodexProvider(模拟 app-server)', () => {
     // 插件、应用连接器、插件安装建议与子代理在服务中关闭
     expect(cfg.features).toMatchObject({ plugins: false, remote_plugin: false, apps: false, tool_suggest: false, multi_agent: false })
     expect(cfg.apps._default.enabled).toBe(false)
+    expect(cfg.agents).toEqual({ enabled: false })
     // 网关以 HTTP MCP 方式配置,携带本次 Run 的令牌
     expect(cfg.mcp_servers.gateway).toMatchObject({ url: 'http://127.0.0.1:9/mcp', http_headers: { Authorization: 'Bearer rt_test' }, default_tools_approval_mode: 'approve' })
   })
@@ -76,6 +77,8 @@ describe('CodexProvider(模拟 app-server)', () => {
     const cfg = JSON.parse((t.events.find(e => (e.payload as { messageId?: string }).messageId === 'cfg')!.payload as { text: string }).text)
     expect(cfg.features).toMatchObject({ plugins: true, remote_plugin: true, apps: true, tool_suggest: false, multi_agent: false })
     expect(cfg.apps._default.enabled).toBe(true)
+    // Playground 中子代理同样关闭
+    expect(cfg.agents).toEqual({ enabled: false })
     expect(cfg.skills).toEqual({ config: [{ path: '/sys/skill-installer/SKILL.md', enabled: false }] })
     expect(t.interactions[0]).toMatchObject({ kind: 'approval', tool: 'mcp__codex_apps', title: '允许调用 Gmail 的 search_emails?' })
     expect(t.events.some(e => (e.payload as { text?: string }).text === 'app:accept')).toBe(true)

+ 3 - 1
docs/design/03-providers.md

@@ -135,7 +135,9 @@ Thread 参数:`approvalPolicy = on-request`、`cwd = workspaceDir`,沙箱使
   - `apps._default.enabled = false`
   - 已验证:覆盖后对话中只剩 AgentServer 提供的技能与 MCP Gateway
   - 例外:后台 Playground 创建的 Codex 会话开放插件与应用连接器(ADR-0018)
-  - 遗留问题:`collaboration.*`(子代理)工具在关闭 `multi_agent` 后仍然出现,子代理继承同一沙箱,待确认对应开关
+  - **子代理**另外用 `agents.enabled = false` 关闭(Playground 也关闭)。实测(codex 0.164):只设 `features.multi_agent = false` 时,模型仍能调用 `collaboration.spawn_agent` 创建并等待子代理;加上后工具消失。
+    - 关闭的原因:子线程的事件、审批、中断与用量目前都不经过 AgentServer
+    - 以后要开放,需要先补齐线程归属、子代理审批转发、中断级联与网关令牌清理(子代理不能在 Run 结束后继续使用会话令牌),并用 `agents.max_concurrent_threads_per_session` 限制并发
 - **网关令牌**:Codex 的会话在 app-server 中保持加载,`thread/resume` 不会更新已建立的 MCP 连接的请求头,所以 Codex 每个会话使用固定的令牌字符串:
   - 每次 Run 开始时绑定到当前 Run,结束后失效
   - 会话删除时清除