// 把合并后的文件访问策略映射为 Codex 的权限配置(permission profile),随 thread/start 的 config 传入。 // 已验证(2026-10-23,codex 0.164):":root" = "deny" 可以做默认拒绝;Codex 的沙箱辅助程序是指向 codex 二进制的 // 链接,位于 CODEX_HOME/tmp/arg0-linux,这两处必须可读,否则沙箱无法启动(docs/design/03-providers.md §6) import path from 'node:path' import type { AppSettings, PermissionMode } from '@agent-server/shared' import type { ResolvedAccess } from '../../services/access-policy' import { hasGlob, realpathLoose } from '../../lib/paths' export const PROFILE_NAME = 'agent_server' /** * 服务场景下关闭的 Codex 功能(每次对话的 config 覆盖)。已验证(2026-10-23,codex 0.164):服务账号登录 ChatGPT 后, * Codex 会自动安装 openai-curated-remote 中的插件(Google Drive、Gmail、Pages、Sites 等),把插件技能、应用连接器 * 和插件安装建议带进对话,调用方可能借此访问账号绑定的个人数据。对话只使用 AgentServer 提供的能力(技能、MCP Gateway) */ export const DISABLED_FEATURES = { plugins: false, remote_plugin: false, apps: false, tool_suggest: false, skill_mcp_dependency_install: false, // 子代理的事件与审批不经过 AgentServer multi_agent: false, browser_use: false, computer_use: false, } as const type Access = 'read' | 'write' | 'deny' type FsTable = Record | number> export interface CodexThreadConfig { default_permissions: string permissions: Record shell_environment_policy: { inherit: 'core'; set: Record } features: Record [key: string]: unknown } export function buildThreadConfig(opts: { access: ResolvedAccess permissionMode: PermissionMode settings: AppSettings codexBin: string codexHome: string /** Playground 中开放远程插件与应用连接器(插件安装建议、子代理等仍然关闭) */ allowPlugins?: boolean }): CodexThreadConfig { const { access } = opts const fs: FsTable = { ':root': access.mode === 'broad' ? 'read' : 'deny', ':minimal': 'read', // Codex 沙箱辅助程序及其指向的二进制 [realpathLoose(path.join(opts.codexHome, 'tmp', 'arg0-linux'))]: 'read', [path.dirname(realpathLoose(opts.codexBin))]: 'read', // 已安装的 Codex 技能(脚本与资源需要在沙箱中可读) [realpathLoose(path.join(opts.codexHome, 'skills'))]: 'read', } const workspace = realpathLoose(access.workspaceDir) for (const p of access.readable) if (p !== '/' && p !== workspace) fs[p] = 'read' for (const p of access.writable) if (p !== workspace) fs[p] = opts.permissionMode === 'readonly' ? 'read' : 'write' // 宽松模式下屏蔽其他会话的工作区与服务数据目录(当前工作区更具体,仍然可写) if (access.mode === 'broad') for (const p of access.softDeny) fs[p] = 'deny' const workspaceRules: Record = { '.': opts.permissionMode === 'readonly' ? 'read' : 'write' } let hasGlobRule = false for (const entry of access.hardDeny) { if (path.isAbsolute(entry) && !hasGlob(entry)) { fs[entry] = 'deny' } else if (!path.isAbsolute(entry)) { // 相对通配(如 **/*.env)作用于工作区 workspaceRules[entry] = 'deny' hasGlobRule = true } else { fs[entry] = 'deny' hasGlobRule = true } } fs[':workspace_roots'] = workspaceRules if (hasGlobRule) fs.glob_scan_max_depth = 6 const py = opts.settings.paths.pythonRuntime const agentDir = path.join(access.workspaceDir, '.agent') return { features: opts.allowPlugins ? { ...DISABLED_FEATURES, plugins: true, remote_plugin: true, apps: true } : { ...DISABLED_FEATURES }, // 未开放插件时,即使有应用被启用也不暴露连接器工具 apps: opts.allowPlugins ? { _default: { enabled: true } } : { _default: { enabled: false, destructive_enabled: false, open_world_enabled: false } }, default_permissions: PROFILE_NAME, permissions: { [PROFILE_NAME]: { filesystem: fs, network: { enabled: false } } }, // 只继承基础环境变量(PATH、HOME 等),不把服务进程中的密钥带进命令 shell_environment_policy: { inherit: 'core', set: { PATH: `${path.join(py, 'bin')}:/usr/local/bin:/usr/bin:/bin`, VIRTUAL_ENV: py, MATPLOTLIBRC: opts.settings.paths.matplotlibrc, MPLCONFIGDIR: path.join(agentDir, 'mpl'), NODE_PATH: opts.settings.paths.nodeModules, TMPDIR: path.join(agentDir, 'tmp'), }, }, } }