安全¶
v1.6.0 新增
v1.5.1 新增
- 一个不读取任何插件选项或凭据的会话开始 hook:
hooks/session_start.py取代了hooks/check_options.py。参见 凭据。 - 服务器只读取插件选项传给它的凭据: 它不再读取
ODOO_HTTP_BEARER。 - 本地诊断 只含元数据,并按允许清单检查。参见 隐私。
- 插件目录的检查报告了什么,以及在哪里核实每一项发现。
- 策略新增一项已知限制:在 Odoo 10 至 12 上按位置传递的上下文(参见 权限策略)。
v1.5.0 新增
凭据¶
- 作为插件 时,凭据是 插件选项(
/plugin→ odoo-tools → Configure options)。API key 和密码是sensitive选项:存储在操作系统的安全凭据存储中,绝不会写入settings.json或你的 shell 配置文件,并且只传给本地的 MCP 服务器进程,该进程只把它们发送到你配置的 Odoo URL。对于插件,不要在 shell 中导出ODOO_*。 - 插件有两个
SessionStarthook,它们都不联系任何服务:- 第一个运行
python3 --version,并且仅在python3无法运行时(MCP 服务器将无法启动)打印一条警告; - 第二个是
hooks/session_start.py(v1.5.1 新增,取代hooks/check_options.py),只在python3为 3.9 或更高版本时启动,并且不读取任何插件选项或凭据。它只读取CLAUDE_PLUGIN_DATA和CLAUDE_CODE_ENTRYPOINT(启动会话的 Claude 应用的名称)、事件的来源和会话 id、服务器诊断文件(diagnostics/server.jsonl及其轮换后的server.jsonl.1)的末尾、容器模式的标记是否存在、它自己的每日时间戳以及插件的版本:不读取插件文件夹之外的任何内容。当最近 14 天既没有记录到与 Odoo 的成功连接,也没有记录到带齐全部连接设置的服务器启动时,它会说明 Odoo 工具在哪里运行(不在 Cowork 或 Claude chat 中),在会话开始时每天最多提示一次,并告诉 Claude,同时附上一行供 doctor 使用的诊断信息(安装或升级插件后的第一次会话,以及没有数据文件夹的地方,只告诉 Claude);当上一次服务器启动被权限策略锁定时,它会连同 E 代码说明这一点。它为每个会话在diagnostics/hook.jsonl中写一行元数据(从不通过符号链接写入),为每日提示保存两个时间戳文件,并且从不阻止会话(总是以 0 退出)。使用/odoo-tools:deploy-local的容器模式时,它什么也不说。
- 第一个运行
- Claude Code 会把插件的每个已保存选项(包括敏感选项)以
CLAUDE_PLUGIN_OPTION_*的形式导出到插件每个 hook 进程的环境中。本插件的 hooks 不读取其中任何一个(由server/tests/test_product_rules.py检查)。 - 副本检查与选项检查(
run_stdio.py --check-copies和--check-options,引导路线和/odoo-tools:odoo-doctor会在 Claude Code 中运行它们)从标准输入读取一条 Claude Code 命令打印的 JSON(claude plugin list --json或claude plugin configure <id> --json):它们不运行任何程序、不打开 Claude Code 的任何文件、不联系任何地方,并且无论输入中有什么,都从不打印敏感值。对于 Odoo URL,选项检查只复述协议、主机和端口,从不复述路径、查询或片段(它们可能带有门户链接的令牌),并会把带有任何@的 URL 视为带有用户名或密码而拒绝,且不显示它的任何部分。两项检查都读取 UTF-8,从 Windows 的管道中读取时也是如此。参见 两个 odoo-tools 副本。 - 在独立模式下(Docker、
python -m odoo_mcp),服务器从 环境 中读取凭据(ODOO_URL、ODOO_DB、ODOO_LOGIN、ODOO_API_KEY/ODOO_PASSWORD)。服务器从不从客户端请求中获取 Odoo 的 URL 或密钥:基于请求头的多租户路径已在 v1.5.0 中移除,ODOO_MULTITENANT仍会被读取,但保留不用。服务器不读取任何其他凭据:它过去从环境中读取但从未使用的ODOO_HTTP_BEARER已在 v1.5.1 中移除。 - MCP 服务器 不会将密钥持久化到磁盘,也不会将其写入日志。会话由
url|db|login加上一个按进程计算的密钥 HMAC 来标识,因此相同的 URL、数据库和登录名配上另一个密钥,永远不会复用已认证的会话;密钥本身从不成为任何键或日志行的一部分。 Settings对象会 在repr()/str()中隐去凭据,以便一次意外的日志记录不会将其泄露(由tests/test_security_redaction.py保证)。- 优先使用 API keys(Odoo ≥ 14)而非密码。使用 最小权限 的 Odoo 用户,并在探索或演示时在插件选项中开启 Read-only mode。
- 会处理哪些数据、会联系哪些服务:参见 隐私。
备用 CLI 确实会写入一个包含凭据的文件
与 MCP 服务器不同,备用 CLI(/odoo-tools:odoo-setup-cli)会把你的凭据保存在 ~/.claude/tools/odoo-cli
中的一个本地 .env 文件里。设置过程会将它创建为仅你的用户可读、并带有占位符:由你自己在自己的编辑器中粘贴密钥;Claude 绝不会在聊天中向你索取它。如果你使用该接口:请保持这些严格的权限(相当于 chmod 600),不要将它提交到版本控制,并在停止使用 CLI 时将其删除。请以对待任何其他存于磁盘的密钥的相同频率来轮换 API key。
为智能体使用专用用户
为智能体创建一个专门的 Odoo 用户(例如 ai-bot@yourcompany.com),只授予它所需的权限。从该用户处生成 API key,并定期轮换。这样,即使 key 被泄露,你也能限制其影响范围。
你公司的数据会怎样¶
有必要区分两条数据路径:
- 从 MCP 服务器到 Odoo(直接)。 服务器运行在你的基础设施上,并通过 JSON-2 / JSON-RPC / XML-RPC 直接与你的 Odoo 实例通信。Transgenia 不充当中间人:我们不做代理,不存储也不查看你的数据或凭据。
- 从 AI 智能体到模型提供方。 当你通过一个智能体(Claude Code 或其他 MCP 客户端)操作 Odoo 时,每个工具的 结果(你查询到的 Odoo 记录)会返回给客户端,并发送给模型提供方(例如在 Claude 的情况下是 Anthropic),以便模型对其进行处理。这是任何 AI 智能体流程所固有的;并非本插件额外引入的。
实际影响:由你决定 查询什么 以及 使用哪个 Odoo 用户(最小权限)。请查阅你的模型提供方的数据处理政策,并避免带出智能体并不需要的敏感字段。对于受监管的数据,只查询必不可少的内容,并考虑在源头进行匿名化。
供应链¶
- 零运行时依赖。 从 v1.2.0 起,MCP 服务器只使用 Python 标准库(MCP 协议以及
JSON-2、JSON-RPC 和 XML-RPC 传输都基于它实现)。插件通过
python3 ${CLAUDE_PLUGIN_ROOT}/server/run_stdio.py启动它:没有软件包启动器(uv、uvx、npx),没有安装步骤,启动时也不下载任何东西。实际运行的代码就是插件文件夹中随附的、可直接阅读的代码。 run_stdio.py会把随附的源代码放在sys.path的最前面,因此机器上安装的其他odoo_mcp包无法替代经过审查的代码。- 可选附加项(
cache、metrics、otel)只有在你自行安装时(独立模式)才会被使用;插件不会安装任何附加项。 - 可选的备用 CLI 是唯一会安装软件包的部分:在你用
/odoo-tools:odoo-setup-cli配置它时,会运行npm install。
插件目录的检查报告了什么¶
Claude 插件目录为本插件列出了以下发现。下面是每一项(名称与目录中显示的英文一致),以及在哪里核实它:
- Uses a credential from the user's machine(
.claude-plugin/plugin.json、server/src/odoo_mcp/config.py)。MCP 服务器从环境中读取两个凭据,即 Odoo 的 API key 和 Odoo 的密码,这个环境由.claude-plugin/plugin.json根据sensitive选项odoo_api_key和odoo_password填充。它只把它们发送到你配置的 Odoo URL,也就是它们所属的服务:server/src/odoo_mcp/transport/中的三种传输方式只连接该 URL,并拒绝重定向。如果服务器从环境中读取任何其他凭据,server/tests/test_product_rules.py就会失败。 scripts/deploy_local.py为/odoo-tools:deploy-local的本地 Docker 沙箱生成随机密码(secrets.token_urlsafe),把它们保存在沙箱文件夹中一个仅你的用户可读的.env里,并且只用于该沙箱在127.0.0.1上的容器。它们不是你的凭据。在 Odoo 14.0 及更高版本上,它还会让沙箱的 Odoo 为其管理员用户创建一个 API key(在沙箱的一次性容器中运行odoo shell,使用 Odoo 自己的res.users.apikeys),检查这个 key 能否登录,并把它以ODOO_API_KEY保存在同一个.env中,从不打印。由你把它复制到插件的 Odoo API key 选项中;Claude 从不读取它。- 测试(
server/tests/):使用伪造的金丝雀凭据,证明任何日志行、审计行、诊断记录、支持报告或 hook 输出都从不包含凭据。插件从不运行这些测试。 - Uses hooks. 即 凭据 中所述的两个
SessionStarthook:hooks/session_start.py只读取插件的数据文件夹和CLAUDE_CODE_ENTRYPOINT,从不读取插件选项或凭据(由server/tests/test_product_rules.py检查),并且总是以 0 退出;另一个只运行python3 --version。 - Runs a local program for an MCP server.
python3 ${CLAUDE_PLUGIN_ROOT}/server/run_stdio.py:只使用标准库,不安装也不下载任何东西(参见 供应链)。 - Contains a download-and-run command. 只有两个命令会在你的电脑上下载并运行代码,而且只在你调用并确认时才会这样做:
/odoo-tools:deploy-local会拉取并运行 Docker 镜像(Transgenia 按版本系列标签提供的官方 PostgreSQL 和 Odoo 镜像的原样副本,以及与插件版本相同的 MCP 服务器镜像),/odoo-tools:odoo-setup-cli会为可选的 CLI 运行npm install。MCP 服务器和 hooks 不下载也不安装任何东西。
自动检查(CI)¶
.github/workflows/security.yml 流程会在每次 push/PR 时运行:
- gitleaks:扫描历史记录和 diff,查找被提交的密钥。它通过
go install在固定标签(v8.18.4,每个模块都经 Go 校验和数据库验证)上构建,不会用curl下载并运行二进制文件。 - leak-guard:查找被禁止的内部/租户标记(客户数据、基础设施主机、税务标识符、硬编码的凭据),如果它们出现则让构建失败。该仓库是 vendor-neutral(供应商中立)的:只包含通用工具,外加公开的品牌/署名以及服务报价。组织特定的标记从仓库机密(secret)中读取,而不是写在流程文件里(插件文件夹就是仓库根目录,因此流程文件会随插件分发给每个安装者),并且对这些标记的匹配只按文件名报告。
.gitignore:会排除.env、*.env、虚拟环境和构建输出,以免本地凭据被意外提交。
传输¶
- 使用带 HTTPS 的 Odoo URL。JSON-2、JSON-RPC 和 XML-RPC 都运行在你所提供的 URL 上;
http://URL 会以不加密的方式发送。 - JSON-2(Odoo saas~18.4 / 19.0+)在每次请求中以
Authorization: bearer发送 API key,绝不会在重定向中发送(重定向会被拒绝),不发送 cookies,也绝不会在工具结果中显示 Odoo 服务器的堆栈跟踪(错误中的debug字段)。它只接受 API keys,因此使用密码的配置会停留在经典端点上。 - 所有传输方式都会使用系统信任存储验证 TLS 证书(包括主机名)(遵循
SSL_CERT_FILE;如果安装了可选的certifi包,也会使用其 CA 列表)。没有关闭验证的选项。所有传输方式都应用ODOO_TIMEOUT(默认 120 秒;插件选项 Request timeout)。 - JSON-RPC 不跟随 HTTP 重定向(被重定向的 POST 会变成没有请求体的 GET);请配置你的 Odoo 的最终 URL。
- Odoo 前面的基本认证:
ODOO_URL可以带有user:pass@;JSON-RPC 会将其作为Authorization请求头发送,任何错误、日志或工具结果都不会复述该密码(自 v1.5.0 起,odoo_connections会从它报告的 URL 中去掉user:pass@)。JSON-2 需要同一个请求头来传递它的 API key,因此这样的 URL 会停留在经典端点上。Odoo URL 不是sensitive选项:该密码保存在插件配置中,而不是安全凭据存储中。 - 服务器仅在 stderr 上记录日志;stdout 保留给 MCP 通道。
审计日志¶
自 v1.5.0 起,每次工具调用(包括被拒绝的调用)都会留下一行 JSON:工具、它触及的模型和操作名称(op:模型[:方法])、拒绝发生的层和规则、计数、耗时、错误的异常类名以及策略摘要。它从不保存参数值、域、字段值、id、文件内容、URL、令牌、密钥、密码或错误消息文本;没有通过策略名称检查的名称会写成 ?。Odoo 的数据库和登录名只出现在服务器启动时写入的 start 行中。
- 插件把每日文件(
odoo-tools-audit-YYYY-MM-DD.jsonl)写入它的数据文件夹(ODOO_AUDIT_DIR),只有你的用户可以访问(文件夹0700,文件0600,从不经过符号链接),并保留 90 天(ODOO_AUDIT_RETENTION_DAYS;0表示一直保留)。 - 没有文件夹时,日志行会写到 stderr 上的
odoo_mcp.audit日志记录器;ODOO_AUDIT_DIR=off不写入任何内容。 - 每一行都带有上一行的哈希,
python3 server/run_stdio.py --verify-audit 文件会点名第一处被编辑、被调换顺序或被插入的行,或不完整的最后一行。仅凭一个文件,它无法发现从某天末尾删掉的整行,也无法发现被删除的某天文件(幸存的前缀仍然是连续的链)。在本地,这是证据而不是防范:能写入该文件夹的人就能改写这些文件。 - 写入失败从不会中断工具调用。
详细说明和格式见 审计日志。
权限策略¶
在本地(stdio)模式下,权限策略限制的是 Claude 通过 odoo-tools 的 MCP 工具 能做什么。它防范的是错误,以及隐藏在 Claude 所读数据中的指令。它不限制持有 API key 的人,也不是一个沙箱,无法约束在同一台机器上拥有不受限 shell 访问权限的智能体。对所有人都有效的边界是 Odoo 用户本身:为智能体提供一个专用的、最小权限的 Odoo 用户(不是管理员),以及一个带过期时间的 API key。在未来的远程模式中,服务器会保管 Odoo 凭据,届时同一套策略将成为真正的权限强制。
- 它是什么。 位于 Odoo 访问权限、记录规则和字段组之上的一个上限,由插件选项 Permission profile(
readonly、默认的standard、builder、full)和一个可选的 Permission policy file 设定;后者在启动时从一个显式的绝对路径读取一次(从不从默认位置读取)。它从不授予 Odoo 拒绝的权限,也从不弱于 Read-only mode。full保留 1.4 的行为。参考:权限策略。 standard关闭了什么。 已保存的密钥(系统参数、邮件服务器、LDAP 和支付服务商的凭据、IAP 和银行同步令牌、CFDI 证书和私钥、外发邮件队列)无法读取;用户、组、访问规则、模块、银行账户、联系人合并、chatter 历史和财务序列只读;发送邮件、短信、信件或分享链接的模型,以及外发方法(内部备注以外的message_post、message_subscribe、send_mail等)被关闭;服务器动作、自动化、视图和其他ir.*记录只读;删除被拒绝;类凭据字段会从读取结果中移除,并在参数中被拒绝。full仍然拒绝创建、更改或撤销 API keys 以及卸载模块,并且为了执行这一点,也会拒绝策略无法检查其参数或关联模型的调用。- 两层。 每个工具声明的访问,会在会话存在之前根据其参数做出决定(A 层:没有任何内容到达 Odoo)。然后,工具发出的每个 Odoo 调用都要经过一个受守卫的会话(B 层),在发送之前连同其值和路径再检查一次(x2many 命令、域和导出路径、附件的父模型、用户的联系邮箱),并把工具没有声明的任何调用当作缺陷拦下。没有权限映射的工具无法被注册、列出或调用。
- 无效的策略会锁定服务器:只有
odoo_version可用,它会在不联系 Odoo 的情况下解释错误。 - 是安全带,不是沙箱。 在 stdio 下,拥有不受限 shell 的智能体可以读取服务器的环境、编辑策略文件或插件选项,或者直接调用 Odoo。针对策略文件夹的 Claude Code
permissions.deny规则、Bash 的确认提示以及指令行(策略的拒绝是最终结果)会让这更困难,但并非不可能。当 Odoo 用户是管理员时,odoo_version会发出警告,因为此时 Odoo 不做任何限制。 - 已知限制(详见 已知限制):业务方法内部的副作用(一封确认邮件、已有的自动化)属于 Odoo;方法返回的字符串(带令牌的门户 URL)不会被隐去;
ir.attachment以外的指针模型只有在文件拒绝业务读取时才重要;名称无害的关联字段可能携带被屏蔽的数据;many2one 链接不作为触及项检查;已发布的内容在效果上是一条外发渠道;计数和聚合可以用来推断;在机密性方面builder等同于full,因为自动化会在服务器上运行代码;在 Odoo 10 至 12 上,按位置传递给策略对照表之外的业务方法的对象中,与模型某个字段同名的键会被当作值来读取,因此它不受上下文允许清单的约束。
只读模式¶
- 开启 Read-only mode(
ODOO_READONLY=1)时,服务器不再列出非只读工具(odoo_create、odoo_write、odoo_unlink、odoo_execute、odoo_translate_set、两个 Studio 风格工具和两个导入工具:也包括odoo_import_preview,因为它回滚的预演可能会消耗序列号并触发 webhooks),并且在按名称调用它们时,会在访问 Odoo 之前以清晰的错误拒绝它们。读取、导出、odoo_telemetry_preview以及只读的 Online 工具仍然可用。只要它处于开启状态,任何配置档或策略文件都无法恢复写入。 - 备用 CLI 没有对应的功能,也没有权限策略:它的
write命令总会尝试执行调用。在那里请使用最小权限的 Odoo 用户。 odoo_online_profile只按名称、范围和日期列出用户的 API keys:Odoo 不会把 key 本身保存在可读的字段中,插件也从不创建、轮换或撤销 key。odoo_record_documents只有当文件是所请求记录的附件且大小不超过max_bytes(最大 5 MiB)时才返回该文件。
本地基础设施(/odoo-tools:deploy-local)¶
- 只有在你调用并确认后才会运行;没有任何 hook 会运行 Docker。
- Odoo 仅发布在
127.0.0.1,不会暴露给你的网络。 - 密码使用 Python 的
secrets在本地生成,保存在~/.odoo-tools/sandbox/.env(0600,文件夹0700),并且从不打印;Claude 不会读取该文件。 - 在 Odoo 14.0 及更高版本上,Odoo 为沙箱的
admin创建的 API key 以ODOO_API_KEY保存在同一个文件中,同样从不打印,即使在错误信息中也不会出现。由你把它复制到插件的 Odoo API key 选项中;切勿把它粘贴到聊天中。Odoo 在 Preferences > Account Security 中把它列为 "odoo-tools sandbox";如果你在那里删除它,下一次sandbox up会再创建一个。 - 镜像来自
ghcr.io/transgenia(官方镜像的原样副本,每周更新),并可回退到 Docker Hub。 ghcr.io/transgenia/odoo-mcp-tools镜像以非特权用户运行,通过--env-file接收凭据,从不出现在命令行中。
报告漏洞¶
请写信至 dev@transgenia.org,附上详情和复现步骤。请 不要 为安全报告开启公开 issue。我们力求在 5 个工作日内确认收悉。