permissions.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. // 把合并后的文件访问策略映射为 Codex 的权限配置(permission profile),随 thread/start 的 config 传入。
  2. // 已验证(2026-10-23,codex 0.164):":root" = "deny" 可以做默认拒绝;Codex 的沙箱辅助程序是指向 codex 二进制的
  3. // 链接,位于 CODEX_HOME/tmp/arg0-linux,这两处必须可读,否则沙箱无法启动(docs/design/03-providers.md §6)
  4. import path from 'node:path'
  5. import type { AppSettings, PermissionMode } from '@agent-server/shared'
  6. import type { ResolvedAccess } from '../../services/access-policy'
  7. import { hasGlob, realpathLoose } from '../../lib/paths'
  8. export const PROFILE_NAME = 'agent_server'
  9. /**
  10. * 服务场景下关闭的 Codex 功能(每次对话的 config 覆盖)。已验证(2026-10-23,codex 0.164):服务账号登录 ChatGPT 后,
  11. * Codex 会自动安装 openai-curated-remote 中的插件(Google Drive、Gmail、Pages、Sites 等),把插件技能、应用连接器
  12. * 和插件安装建议带进对话,调用方可能借此访问账号绑定的个人数据。对话只使用 AgentServer 提供的能力(技能、MCP Gateway)
  13. */
  14. export const DISABLED_FEATURES = {
  15. plugins: false,
  16. remote_plugin: false,
  17. apps: false,
  18. tool_suggest: false,
  19. skill_mcp_dependency_install: false,
  20. // 子代理的事件与审批不经过 AgentServer
  21. multi_agent: false,
  22. browser_use: false,
  23. computer_use: false,
  24. } as const
  25. type Access = 'read' | 'write' | 'deny'
  26. type FsTable = Record<string, Access | Record<string, Access> | number>
  27. export interface CodexThreadConfig {
  28. default_permissions: string
  29. permissions: Record<string, { filesystem: FsTable; network: { enabled: boolean } }>
  30. shell_environment_policy: { inherit: 'core'; set: Record<string, string> }
  31. features: Record<string, boolean>
  32. [key: string]: unknown
  33. }
  34. export function buildThreadConfig(opts: {
  35. access: ResolvedAccess
  36. permissionMode: PermissionMode
  37. settings: AppSettings
  38. codexBin: string
  39. codexHome: string
  40. /** Playground 中开放远程插件与应用连接器(插件安装建议、子代理等仍然关闭) */
  41. allowPlugins?: boolean
  42. }): CodexThreadConfig {
  43. const { access } = opts
  44. const fs: FsTable = {
  45. ':root': access.mode === 'broad' ? 'read' : 'deny',
  46. ':minimal': 'read',
  47. // Codex 沙箱辅助程序及其指向的二进制
  48. [realpathLoose(path.join(opts.codexHome, 'tmp', 'arg0-linux'))]: 'read',
  49. [path.dirname(realpathLoose(opts.codexBin))]: 'read',
  50. // 已安装的 Codex 技能(脚本与资源需要在沙箱中可读)
  51. [realpathLoose(path.join(opts.codexHome, 'skills'))]: 'read',
  52. }
  53. const workspace = realpathLoose(access.workspaceDir)
  54. for (const p of access.readable) if (p !== '/' && p !== workspace) fs[p] = 'read'
  55. for (const p of access.writable) if (p !== workspace) fs[p] = opts.permissionMode === 'readonly' ? 'read' : 'write'
  56. // 宽松模式下屏蔽其他会话的工作区与服务数据目录(当前工作区更具体,仍然可写)
  57. if (access.mode === 'broad') for (const p of access.softDeny) fs[p] = 'deny'
  58. const workspaceRules: Record<string, Access> = { '.': opts.permissionMode === 'readonly' ? 'read' : 'write' }
  59. let hasGlobRule = false
  60. for (const entry of access.hardDeny) {
  61. if (path.isAbsolute(entry) && !hasGlob(entry)) {
  62. fs[entry] = 'deny'
  63. } else if (!path.isAbsolute(entry)) {
  64. // 相对通配(如 **/*.env)作用于工作区
  65. workspaceRules[entry] = 'deny'
  66. hasGlobRule = true
  67. } else {
  68. fs[entry] = 'deny'
  69. hasGlobRule = true
  70. }
  71. }
  72. fs[':workspace_roots'] = workspaceRules
  73. if (hasGlobRule) fs.glob_scan_max_depth = 6
  74. const py = opts.settings.paths.pythonRuntime
  75. const agentDir = path.join(access.workspaceDir, '.agent')
  76. return {
  77. features: opts.allowPlugins ? { ...DISABLED_FEATURES, plugins: true, remote_plugin: true, apps: true } : { ...DISABLED_FEATURES },
  78. // 未开放插件时,即使有应用被启用也不暴露连接器工具
  79. apps: opts.allowPlugins ? { _default: { enabled: true } } : { _default: { enabled: false, destructive_enabled: false, open_world_enabled: false } },
  80. default_permissions: PROFILE_NAME,
  81. permissions: { [PROFILE_NAME]: { filesystem: fs, network: { enabled: false } } },
  82. // 只继承基础环境变量(PATH、HOME 等),不把服务进程中的密钥带进命令
  83. shell_environment_policy: {
  84. inherit: 'core',
  85. set: {
  86. PATH: `${path.join(py, 'bin')}:/usr/local/bin:/usr/bin:/bin`,
  87. VIRTUAL_ENV: py,
  88. MATPLOTLIBRC: opts.settings.paths.matplotlibrc,
  89. MPLCONFIGDIR: path.join(agentDir, 'mpl'),
  90. NODE_PATH: opts.settings.paths.nodeModules,
  91. TMPDIR: path.join(agentDir, 'tmp'),
  92. },
  93. },
  94. }
  95. }