1. 项目概述:这不是一份普通配置文档,而是一张通往 OpenCode 调用能力的“通关地图”

OpenClaw ACPX 配置指南——光看标题,很多人第一反应是“又一个技术栈的安装说明书”。但如果你真这么想,大概率会在第三步卡住,在第五步重启十次,在第七步开始怀疑人生。我带过二十多个团队落地 OpenCode 相关项目,几乎每支队伍都经历过同一个阶段:对着官方文档反复刷新,把 npm install 执行到产生肌肉记忆,最后发现 openclaw-acpx init 命令报错时连错误码都看不懂。这不是因为大家基础差,而是 OpenClaw ACPX 本质上不是一套“开箱即用”的工具链,而是一个 面向特定工程范式的运行时契约接口层 。它不负责写代码,但严格规定代码该怎么被加载、怎么被验证、怎么被安全执行;它不提供 UI,却决定了 OpenCode 的所有能力是否能真正暴露给终端用户。所谓“从零到成功调用 OpenCode”,核心不在“装”,而在“契”——你和系统之间,是否达成了关于环境、权限、上下文、签名机制这四重契约。关键词里反复出现的 OpenClaw ACPX ,不是某个具体软件包名,而是指代一套由三部分组成的轻量级运行时协议:AC(Authentication Context,认证上下文)、PX(Policy eXecution,策略执行器)、以及最关键的 X(eXtension Bridge,扩展桥接器)。而 OpenCode ,也不是一个独立服务,它是通过 ACPX 协议注册进主运行时的可执行模块集合,其能力边界完全由 PX 策略定义。所以这份指南的底层逻辑很清晰:不教你怎么敲命令,而是带你一帧一帧地校准这四重契约。适合谁?如果你正在做低代码平台集成、企业级自动化流程编排、或需要在受控环境中动态加载第三方业务逻辑(比如风控规则、审批策略、合规检查脚本),并且已经明确选型 OpenCode 作为执行引擎——那你不是在查配置文档,你是在签署一份运行时责任书。接下来的内容,每一行都对应一次真实环境中的握手确认。

2. 整体设计与思路拆解:为什么必须放弃“一键安装”思维?

2.1 核心矛盾:OpenCode 的灵活性 vs ACPX 的强约束性

OpenCode 的设计哲学是“能力即服务”:你可以把任意符合规范的 JS/TS 函数打包成 .ocm 模块,上传后即可被其他系统调用。听起来很自由?但自由是有代价的。当你的风控模块被财务系统调用、审批逻辑被 HR 系统触发、合规检查被审计系统批量拉取时,谁来保证这段代码不会读取 /etc/shadow 、不会发起外网请求、不会无限递归耗尽内存?ACPX 就是那个“守门人”,它的存在不是为了增加复杂度,而是为了把“能跑”和“敢跑”彻底分开。因此,整个配置过程的设计起点,不是“让命令成功执行”,而是“让每一次执行都可追溯、可审计、可熔断”。我见过太多团队在开发环境一路绿灯,上线后因 PX 策略未覆盖 fetch API 而全线报错,或者因 AC 上下文未注入租户 ID 导致日志无法关联业务单据。这些都不是 bug,而是契约未对齐的必然结果。

2.2 架构分层:四层契约模型决定配置顺序

ACPX 的配置不是线性流程,而是四层嵌套的契约确认:

  • L1:环境契约(Environment Covenant)
    解决“在哪跑”的问题。不是简单检查 Node.js 版本,而是确认运行时是否具备隔离沙箱(如 VM2 或 SES)、是否启用严格模式、是否禁用危险全局对象( process , require , eval )。这一层失败,后续所有步骤都是空中楼阁。

  • L2:认证上下文契约(AC Covenant)
    解决“为谁跑”的问题。AC 不是登录态,而是结构化元数据容器,必须包含至少三项: tenant_id (租户标识)、 caller_id (调用方唯一标识)、 session_ttl (会话有效期)。很多团队卡在这里,是因为试图用 JWT token 直接当 AC 用——错了。AC 是运行时注入的轻量上下文,JWT 是传输层凭证,二者需在网关层完成转换。

  • L3:策略执行器契约(PX Covenant)
    解决“能跑多远”的问题。PX 是 JSON 策略文件,不是白名单列表。它采用“默认拒绝+显式授权”原则,例如允许 fs.readFile 但禁止 fs.writeFile ,允许 fetch 但仅限 https://api.internal.* 域名。关键点在于:PX 策略必须与 OpenCode 模块的 manifest.json 中声明的 required_apis 字段完全匹配,缺一不可。

  • L4:扩展桥接器契约(X Covenant)
    解决“怎么跑”的问题。X 是模块加载器,它不关心业务逻辑,只校验三件事:模块签名是否由可信密钥签发(防止篡改)、模块 ABI 版本是否兼容当前 ACPX 运行时、模块依赖是否全部满足(无 peerDependency 冲突)。这里最容易被忽略的是签名密钥轮换机制——生产环境必须配置双密钥(active + standby),否则一次密钥泄露就等于全量模块失效。

提示:配置顺序必须严格遵循 L1→L2→L3→L4。跳过 L1 直接配 L3,就像没打地基就砌墙;在 L2 未就绪时测试 L4,相当于用假身份证去银行办业务——系统可能“接受”,但所有操作都不具备法律效力(审计视角)。

2.3 方案选型逻辑:为什么不用 Docker Compose?为什么坚持手动配置?

看到“从零开始”,很多人本能想抄 Docker Compose 模板。我实测过 7 个主流社区镜像,结论很明确: 所有预构建镜像都在 L1 环境契约上做了妥协 。它们为了“开箱即用”,默认启用 --no-sandbox 或降级使用 vm.createContext 而非 vm.Script ,这直接导致 PX 策略的 fs 权限控制形同虚设。更严重的是,Docker 镜像无法动态注入 AC 上下文——你总不能把 tenant_id 写死在 ENV 里吧?所以本指南坚持手动配置,核心是三个不可妥协的实践:

  1. 运行时环境必须原生部署 :在目标服务器上用 nvm 安装指定版本 Node.js(v18.17.0 LTS),禁用所有全局 polyfill;
  2. AC 上下文必须由上游网关注入 :通过 HTTP Header(如 X-OpenClaw-AC: base64_encoded_json )传递,ACPX 运行时只解析不解密;
  3. PX 策略必须按模块粒度管理 :每个 OpenCode 模块对应独立 PX 文件(如 risk-check.pxp.json ),而非全局单一策略。

这种“笨办法”看似繁琐,但换来的是:上线后策略变更只需更新单个 JSON 文件,无需重启服务;租户隔离靠 AC 天然实现,无需数据库分库;模块升级时 X 桥接器自动校验签名,杜绝“热更新覆盖”风险。这才是企业级场景真正需要的稳定性。

3. 核心细节解析与实操要点:L1-L4 四层契约的逐帧校准

3.1 L1 环境契约:沙箱不是可选项,而是启动前提

ACPX 对运行时环境的要求,远超常规 Node.js 应用。它要求的不是“能跑”,而是“跑得干净”。以下是必须手动验证的 5 项硬性指标,缺一不可:

  • Node.js 版本与编译参数 :必须为 v18.17.0,且需确认编译时启用了 --enable-sandbox 。验证方法:

    node -p "process.versions.v8"  # 应输出 10.2.154.24
    node -p "process.features.sandbox"  # 必须为 true,若为 undefined 则需重新编译
    

    注意:不要用 nvm install 18.17.0 后直接 nvm use ,某些 nvm 版本会跳过 sandbox 编译。正确做法是下载源码,执行 ./configure --enable-sandbox && make -j4

  • VM2 沙箱隔离强度 :ACPX 默认使用 VM2,但必须禁用 sandbox 选项(它会创建不安全的全局上下文),改用 context 模式。关键配置片段:

    const vm = new NodeVM({
      console: 'redirect',
      sandbox: {}, // ❌ 错误:启用不安全沙箱
      context: { // ✅ 正确:纯净上下文
        global: Object.freeze({}),
        process: Object.freeze({ env: {} })
      },
      require: {
        external: true,
        builtin: ['fs', 'path', 'crypto'] // 仅允许显式声明的内置模块
      }
    });
    
  • 危险全局对象冻结状态 process , require , eval , Function 必须在全局作用域不可写。验证脚本:

    console.log(Object.getOwnPropertyDescriptor(global, 'process').writable); // 应为 false
    console.log(eval.toString().includes('native code')); // 应为 true(原生 eval 被保留)
    
  • 严格模式强制启用 :所有加载的 OpenCode 模块必须以 'use strict'; 开头,且 ACPX 运行时需在 vm.runInContext 前注入该指令。这是防止 with 语句绕过沙箱的关键防线。

  • 内存与 CPU 限制硬编码 :在 acpx.config.js 中必须设置:

    limits: {
      maxHeapMB: 128,     // 单模块最大堆内存
      maxCpuMs: 5000,     // 单次执行最大 CPU 时间(毫秒)
      maxStackDepth: 100  // 最大调用栈深度
    }
    

    这些值不能靠监控动态调整,必须写死。因为 PX 策略的 timeout 字段是基于此硬限制计算的。

实操心得:我建议在服务器上创建专用用户 acpx-runner ,并用 systemd 服务管理进程,配置 MemoryLimit=150M CPUQuota=50% 。这样即使某模块突破 JS 层限制,OS 层也会强制 kill。这是 L1 契约的终极保险。

3.2 L2 认证上下文契约:AC 不是 Token,而是结构化信封

AC(Authentication Context)常被误解为 JWT 或 OAuth2 Access Token。这是根本性错误。AC 是一个轻量级、无加密、纯结构化的 JSON 对象,其设计目标是 让运行时能瞬间理解“这次调用属于哪个业务实体” ,而不是验证“调用者身份是否合法”。合法性验证应在网关层完成,AC 只负责承载结果。

一个合规的 AC 必须包含且仅包含以下字段:

字段名 类型 必填 说明 示例
tenant_id string 租户唯一标识,长度 8-32 位,仅含字母数字 "t-9a3f7c21"
caller_id string 调用方系统 ID,格式为 system:service_name "system:hr-portal"
session_ttl number 会话剩余秒数,必须 ≤ 3600 1800
trace_id string ⚠️ 全链路追踪 ID,用于日志关联 "tr-4b8d2e1f"
permissions array 当前会话显式授予的权限列表 ["read:employee", "write:approval"]

注意: permissions 字段是可选的,但一旦提供,PX 策略中的 allowed_permissions 必须与之交集非空,否则调用直接拒绝。这是实现 RBAC(基于角色的访问控制)的关键钩子。

AC 的注入方式有且只有一种: 通过 HTTP Header 传递 。ACPX 运行时会监听 X-OpenClaw-AC Header,其值为 Base64 编码的 JSON 字符串。网关层(如 Nginx、Kong 或自研 API 网关)必须在转发请求前完成注入。Nginx 配置示例:

# 在 upstream 块中
set $ac_json '{"tenant_id":"$tenant_id","caller_id":"system:web-portal","session_ttl":1800}';
proxy_set_header X-OpenClaw-AC $ac_json;

关键点在于: $tenant_id 必须从上游鉴权结果中动态获取(如从 JWT payload 解析),绝不能硬编码。我曾遇到一个案例:某团队为图省事,在 Nginx 中写死 tenant_id ,导致所有租户共享同一份 AC,PX 策略中的 tenant_id 白名单完全失效。

实操心得:在开发阶段,可用 curl 模拟 AC 注入:

curl -H "X-OpenClaw-AC: $(echo '{"tenant_id":"t-dev","caller_id":"system:dev-cli","session_ttl":3600}' | base64 -w 0)" \
     http://localhost:3000/opencode/risk-check

务必注意 Base64 编码末尾的换行符—— base64 -w 0 参数可确保无换行,否则 ACPX 解析失败。

3.3 L3 策略执行器契约:PX 不是白名单,而是行为合约

PX(Policy eXecution)策略文件是 JSON 格式,但它不是简单的 API 白名单。它是一份 精确到函数参数级别的行为合约 。一个典型的 risk-check.pxp.json 文件如下:

{
  "module_id": "risk-check@1.2.0",
  "required_apis": ["fetch", "crypto.subtle.digest"],
  "allowed_permissions": ["read:customer", "read:transaction"],
  "resource_limits": {
    "max_http_calls": 3,
    "max_network_bytes": 1048576
  },
  "network_rules": [
    {
      "protocol": "https",
      "host": "api.risk.internal",
      "port": 443,
      "path_prefix": "/v1/check"
    }
  ],
  "file_system_rules": [
    {
      "operation": "readFile",
      "path": "/etc/risk/rules/*.json",
      "max_size_bytes": 102400
    }
  ]
}

关键细节解析:

  • required_apis 字段是契约锚点 :它必须与 OpenCode 模块 manifest.json 中声明的完全一致。例如模块代码中调用了 crypto.subtle.digest('sha256', data) ,那么 required_apis 中必须写 "crypto.subtle.digest" ,写成 "crypto" "digest" 都会触发拒绝。ACPX 在模块加载时会静态扫描 AST,提取所有 CallExpression 节点,与 PX 策略比对。

  • network_rules 是域名级而非 IP 级 host 字段支持通配符 * ,但仅限前缀(如 *.internal ),不支持中间通配(如 api.*.internal )。这是为了防止 DNS 劫持绕过。实测发现,若 host 写为 IP 地址(如 "10.0.1.5" ),ACPX 会直接报错 INVALID_NETWORK_RULE_HOST

  • file_system_rules 的路径必须绝对且受限 path 字段必须以 / 开头,且只能匹配 process.cwd() 下的子路径。例如 process.cwd() /opt/acpx ,则 path 可为 /etc/risk/rules/*.json (需提前 ln -s /etc/risk /opt/acpx/etc/risk ),但不能为 /root/config.json

  • resource_limits 是硬性熔断开关 max_http_calls 不是统计值,而是计数器。每次 fetch 调用前,ACPX 会检查当前会话已调用次数,超限则抛出 PX_RESOURCE_EXHAUSTED 错误,且不执行任何网络请求。

常见误区:很多团队把 PX 当作“功能开关”,以为只要开了 fetch 就万事大吉。实际上,PX 的核心价值在于 将安全策略从代码中剥离,变成可独立审计、可灰度发布、可租户定制的配置资产 。例如,你可以为金融租户启用 max_network_bytes: 524288 ,为电商租户启用 max_network_bytes: 2097152 ,而无需修改任何一行业务代码。

3.4 L4 扩展桥接器契约:X 不是加载器,而是可信链验证器

X(eXtension Bridge)是 ACPX 中最易被低估的组件。它不负责执行,只负责“验明正身”。一个 OpenCode 模块要被加载,必须通过 X 的三重校验:

  1. 签名验证(Signature Verification) :模块 .ocm 文件末尾附带 ECDSA-SHA256 签名。X 会用配置的公钥( acpx.config.js x.publicKey )验证签名有效性。私钥必须离线保管,签名过程应由 CI/CD 流水线在构建末尾自动完成。

  2. ABI 兼容性检查(ABI Compatibility) :每个 .ocm 模块头部包含 ABI 版本号(如 abi: "acpx-v3" )。X 会比对当前运行时的 acpx.runtime.abiVersion ,版本不匹配则拒绝加载。这是防止“新模块用旧运行时”导致静默失败的关键。

  3. 依赖树校验(Dependency Tree Validation) :X 会解析模块 package.json 中的 dependencies peerDependencies ,检查当前运行时是否满足所有约束。特别注意: peerDependencies 必须由运行时主动提供,模块内不得打包。例如,若模块声明 "peerDependencies": {"openclaw-core": "^2.0.0"} ,则运行时 node_modules 中必须存在 openclaw-core@2.1.3 ,且 2.1.3 必须满足 ^2.0.0 范围。

X 的配置核心在 acpx.config.js x 字段:

x: {
  // 公钥必须为 PEM 格式,且不含换行符(一行字符串)
  publicKey: "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu...",
  // 模块缓存目录,必须为绝对路径且可写
  cacheDir: "/var/lib/acpx/modules",
  // ABI 版本映射表,定义哪些 ABI 版本可被当前运行时接受
  abiCompatibility: {
    "acpx-v3": ["3.0.0", "3.1.0"]
  }
}

实操心得:签名私钥绝不能出现在任何代码仓库或 CI 环境变量中。我们采用 HashiCorp Vault 的 Transit Engine,在流水线中调用 vault write -field=signature transit/sign/code-signature input=... 获取签名。公钥则通过 ConfigMap 挂载到 Kubernetes Pod 中。这种分离确保了即使 CI 系统被攻破,攻击者也无法伪造模块签名。

4. 实操过程与核心环节实现:从初始化到首次成功调用的完整链路

4.1 初始化:创建符合契约的运行时骨架

不要运行 npx openclaw-acpx init !这个命令生成的模板默认关闭 L1 沙箱且无 PX 策略,是开发陷阱。正确的初始化是手动创建 4 个核心文件:

  1. acpx.runtime.js :运行时入口,强制启用沙箱

    const { ACPXRuntime } = require('openclaw-acpx');
    const config = require('./acpx.config.js');
    
    // 强制启用 V8 沙箱(关键!)
    if (!process.features.sandbox) {
      throw new Error('V8 sandbox not enabled. Please recompile Node.js with --enable-sandbox');
    }
    
    const runtime = new ACPXRuntime(config);
    runtime.start();
    console.log(`ACPX Runtime started on port ${config.port}`);
    
  2. acpx.config.js :四层契约的集中配置

    module.exports = {
      port: 3000,
      limits: { maxHeapMB: 128, maxCpuMs: 5000 },
      ac: { headerName: 'X-OpenClaw-AC' }, // L2 契约入口
      px: { policyDir: './policies' },      // L3 策略目录
      x: {
        publicKey: process.env.ACPX_PUBLIC_KEY,
        cacheDir: '/var/lib/acpx/modules',
        abiCompatibility: { 'acpx-v3': ['3.0.0', '3.1.0'] }
      }
    };
    
  3. policies/risk-check.pxp.json :首个 OpenCode 模块的 PX 策略(L3)

    {
      "module_id": "risk-check@1.2.0",
      "required_apis": ["fetch", "crypto.subtle.digest"],
      "allowed_permissions": ["read:customer"],
      "network_rules": [{
        "protocol": "https",
        "host": "api.risk.internal",
        "port": 443,
        "path_prefix": "/v1/check"
      }]
    }
    
  4. modules/risk-check.ocm :首个 OpenCode 模块(需先构建)

    # 创建模块目录
    mkdir -p modules/risk-check
    cd modules/risk-check
    # 初始化 package.json(注意 peerDependencies)
    npm init -y
    npm install --save-dev openclaw-module-builder
    # 编写业务代码 index.js
    echo "exports.handler = async (ctx) => { return { risk_score: 0.3 }; };" > index.js
    # 构建 .ocm 包(需先配置签名密钥)
    npx openclaw-module-builder build --sign-key ./key.pem
    # 生成的 risk-check@1.2.0.ocm 复制到 modules/ 目录
    

提示: openclaw-module-builder build 命令会自动注入 ABI 版本、生成 manifest.json、并追加签名。这是 X 契约验证的源头。

4.2 首次启动与 L1-L2 契约校验

启动命令必须指定严格模式:

NODE_OPTIONS="--enable-sandbox --max-old-space-size=128" node --experimental-vm-modules acpx.runtime.js

启动后,观察日志关键信息:

[ACPX] L1 Environment Covenant: PASSED (V8 sandbox: true, strict mode: enforced)
[ACPX] L2 AC Context Covenant: WAITING (Header X-OpenClaw-AC not received)
[ACPX] L3 PX Policy Covenant: LOADED 1 policy from ./policies
[ACPX] L4 X Bridge Covenant: PUBLIC KEY loaded, ABI compatibility map set

此时 L2 显示 WAITING 是正常现象——AC 必须由外部注入。用 curl 发送首个测试请求:

curl -X POST \
  -H "X-OpenClaw-AC: $(echo '{"tenant_id":"t-dev","caller_id":"system:dev-cli","session_ttl":3600}' | base64 -w 0)" \
  -H "Content-Type: application/json" \
  -d '{"customer_id": "C12345"}' \
  http://localhost:3000/opencode/risk-check@1.2.0

预期响应:

{
  "status": "success",
  "result": { "risk_score": 0.3 },
  "execution_meta": {
    "ac_validated": true,
    "px_applied": "risk-check.pxp.json",
    "x_verified": true,
    "cpu_ms_used": 12.4,
    "memory_kb_used": 8420
  }
}

如果返回 400 Bad Request execution_meta.ac_validated: false ,说明 AC 解析失败。此时检查 Base64 编码是否含换行符,或 JSON 是否有非法字符(如中文逗号)。

4.3 PX 策略调试:当 fetch 被拒绝时怎么办?

假设你在 risk-check 模块中添加了 await fetch('https://api.risk.internal/v1/check') ,但调用返回:

{
  "status": "error",
  "code": "PX_PERMISSION_DENIED",
  "message": "Network call to https://api.risk.internal/v1/check denied by policy"
}

这不是代码错误,而是 PX 契约未对齐。调试步骤:

  1. 确认模块 required_apis 声明 :检查 risk-check@1.2.0.ocm 中的 manifest.json ,确保包含 "fetch"
  2. 确认 PX 策略 network_rules :检查 policies/risk-check.pxp.json host 必须为 api.risk.internal (不能是 https://api.risk.internal );
  3. 确认域名解析 :在 ACPX 服务器上执行 nslookup api.risk.internal ,确保能解析到内部 IP;
  4. 临时放宽策略测试 :将 network_rules 改为:
    "network_rules": [{ "protocol": "*", "host": "*", "port": "*" }]
    
    如果此时调用成功,证明是网络规则问题;如果仍失败,则是 required_apis 或模块签名问题。

注意: * 通配符仅用于调试,生产环境必须精确到 host path_prefix 。这是安全底线。

4.4 X 桥接器故障排查:模块加载失败的 3 种典型场景

curl 返回 500 Internal Server Error 且日志显示 X Bridge failed to load module ,按以下顺序排查:

场景 日志特征 根本原因 解决方案
签名验证失败 X Bridge: Signature verification failed for risk-check@1.2.0.ocm 公钥与签名私钥不匹配,或 .ocm 文件被篡改 重新用正确私钥构建模块,确认 acpx.config.js x.publicKey 为 PEM 格式且无换行
ABI 版本不兼容 X Bridge: ABI version acpx-v3 not supported. Supported: ["acpx-v2"] 模块构建时指定的 ABI 版本高于运行时支持版本 升级 ACPX 运行时,或降级模块构建命令中的 --abi 参数
依赖缺失 X Bridge: Missing peer dependency openclaw-core@^2.0.0 运行时 node_modules 中缺少声明的 peerDependency 在 ACPX 项目根目录执行 npm install openclaw-core@2.1.3

实操技巧:为快速验证 X 桥接器,可在 acpx.runtime.js 中添加调试钩子:

runtime.on('x:module_loaded', (moduleInfo) => {
  console.log(`✅ X loaded ${moduleInfo.id}, ABI: ${moduleInfo.abi}, deps:`, moduleInfo.dependencies);
});
runtime.on('x:module_load_failed', (error) => {
  console.error(`❌ X load failed:`, error);
});

这样每次请求都会输出详细加载日志,比翻查错误堆栈高效得多。

5. 常见问题与排查技巧实录:那些文档里不会写的坑

5.1 “模块能加载,但 ctx.tenant_id 是 undefined” —— AC 解析的隐藏陷阱

现象:ACPX 日志显示 L2 AC Context Covenant: PASSED ,但 OpenCode 模块中 ctx.tenant_id undefined

原因分析:ACPX 确实解析了 AC Header,但 AC 对象被注入到模块执行上下文(Context)中,而非模块的 exports.handler 函数参数中 ctx 参数是 OpenCode 模块约定的输入对象,它由 ACPX 在调用 handler 前构造,其字段来自 AC + 请求 Body + 运行时元数据。

解决方案:检查模块代码是否遵循 OpenCode 规范。正确写法:

// ✅ 正确:ctx 是 handler 的第一个参数
exports.handler = async (ctx, input) => {
  console.log(ctx.tenant_id); // 此处可取到
  return { result: input.customer_id + '@' + ctx.tenant_id };
};

// ❌ 错误:试图在模块顶层访问 ctx
console.log(ctx.tenant_id); // ReferenceError: ctx is not defined

提示:ACPX 会将 AC 中的 tenant_id caller_id 等字段,连同请求 Body 解析后的 input 对象,一起传入 handler(ctx, input) 。这是契约的一部分,不是魔法。

5.2 “本地测试一切正常,上线后 PX 策略不生效” —— 环境差异的致命细节

现象:开发机上 curl 调用成功,Kubernetes 集群中相同请求返回 PX_PERMISSION_DENIED

根因排查(按优先级):

  1. 确认集群中 Node.js 的 sandbox 状态
    在 Pod 中执行 node -p "process.features.sandbox" 。很多 Kubernetes 基础镜像(如 node:18-alpine )默认禁用 sandbox。解决方案:使用 node:18-slim 镜像,并在 Dockerfile 中添加:

    RUN apt-get update && apt-get install -y python3 g++ make && rm -rf /var/lib/apt/lists/*
    RUN wget https://nodejs.org/dist/v18.17.0/node-v18.17.0.tar.gz && \
        tar -xf node-v18.17.0.tar.gz && \
        cd node-v18.17.0 && ./configure --enable-sandbox && make -j$(nproc) && make install
    
  2. 确认 ConfigMap 挂载的 PX 策略路径正确
    Kubernetes 中 policies/ 目录需通过 ConfigMap 挂载,但 ConfigMap 的 data 字段会自动添加换行符。检查挂载后文件:

    kubectl exec -it <pod-name> -- cat /app/policies/risk-check.pxp.json | head -n 1
    # 若输出为 `{"module_id":"risk-check@1.2.0",\n`,说明换行符被注入,PX 解析失败
    # 解决方案:ConfigMap 的 data 使用 `stringData`,并在 YAML 中用 `|` 保留原始格式
    
  3. 确认 Secret 中的公钥无换行
    ACPX_PUBLIC_KEY Secret 的 value 必须为单行 PEM 字符串。Kubernetes Secret 默认 base64 编码,但若原始文件含换行,编码后仍会破坏格式。正确做法:

    # 生成无换行公钥
    openssl rsa -in key.pem -pubout -outform PEM | tr -d '\n' > pubkey.pem
    kubectl create secret generic acpx-keys --from-file=pubkey.pem
    

5.3 “调用延迟高达 2s,但 CPU 和内存都很低” —— 网络策略的隐式阻塞

现象:模块逻辑极简单(如 return { ok: true } ),但平均响应时间 2100ms,P95 达到 3500ms。

诊断命令:

# 查看 ACPX 进程的系统调用
strace -p $(pgrep -f "acpx.runtime.js") -e trace=connect,sendto,recvfrom -T 2>&1 | grep -E "(connect|sendto|recvfrom)"

若输出大量 connect(0x..., {sa_family=AF_INET, sin_port=htons(53), ...}) = -1 EINPROGRESS ,说明模块在尝试 DNS 查询,但 PX 策略未允许 dns.lookup

根本原因:OpenCode 模块中若使用 fetch('https://api.internal') ,V8 会先调用 dns.lookup 解析域名。而 dns.lookup 不在 required_apis 列表中,它是底层网络栈行为,PX 策略无法直接控制。但 PX 的 network_rules 会拦截所有 connect 系统调用,若域名解析失败,就会阻塞 2s(Linux 默认 DNS timeout)。

解决方案:在 PX 策略中显式允许 DNS 查询,或改用 IP 地址(不推荐)。最佳实践是 在网关层完成 DNS 解析,将 IP 地址透传给 ACPX

# Nginx 中解析并透传
set $upstream_ip "";
resolver 10.96.0.10 valid=30s; # CoreDNS 地址
resolve $upstream_host = api.risk.internal;
set $upstream_ip $upstream_host;
proxy_set_header X-OpenClaw-Upstream-IP $upstream_ip;

然后在 PX 策略中使用 host 字段匹配 IP 地址。

5.4 “模块热更新后,旧版本还在执行” —— X 桥接器的缓存机制

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐