跳到主要内容
Safe 与 Web3 运维

AI Agent 处理数字资产时的权限矩阵

权限矩阵逐项写明 AI Agent 可以准备什么、由人批准什么,以及由哪个渠道执行。

发布 更新 10 分钟阅读
permissionsSafeAI agentsWeb3 opsmultisig

本文仅用于教育目的,不能替代正式的智能合约审计、法律建议、金融建议或针对具体协议的安全审查。

处理数字资产的 AI Agent 需要的不只是一份工具清单。它需要一份权限矩阵,逐项写明 Agent 可以读取和准备什么、由人批准什么,以及由哪个授权渠道执行。

一只手在一列琥珀色的锁上转动钥匙,控制台上还有蓝色文件图标、开关网格和用户头像。
每把锁都是一项独立的权限,各有负责人。概念示意图。

有用的单位是能力,而不是工具。签名、访问密钥、启用模块、更改 fallback handler 或批准一笔 DELEGATECALL 操作,每一项都会打开对账户的控制权,所以每一项都需要自己的一行、自己的审批和自己的负责人。

本文把这一思路落实为一份矩阵,面向运行基于 Safe 的运维流程的小团队,内容包括:各行的设置、每项权限背后的资源、托管服务商的策略引擎覆盖哪些内容、哪些仍由团队负责,以及在 Agent 接触生产环境之前用于检验矩阵的反例。

关于生产环境中 Agent 权限的一般原则,请看下面链接的文章。本矩阵聚焦于密钥、签名、模块、guard 以及绕过它们的路径。

从打开控制权的能力入手

工具名称很少能说明风险。一个叫 propose 的工具可能只是起草 payload,也可能持有为它签名的密钥。因此,矩阵先列出能力,再把工具对应到能力上。

在 Safe 智能账户中,以下能力会改变谁能转移资金或账户如何运作:

  • 签名或提交交易,或者持有执行这些操作的密钥。
  • 添加或移除 owner,以及更改阈值。
  • 启用或禁用模块。根据 Safe 文档,模块可以执行任意交易,恶意模块可以接管一个 Safe。
  • 设置 transaction guard,或者从 Safe 1.5.0 起设置 module guard。根据文档,有缺陷的 guard 可能导致 Safe 拒绝服务。
  • 更改 fallback handler。文档提醒,它允许在账户内执行任意外部函数。
  • 批准 operation 字段为 DELEGATECALL 的交易。Safe 支持 CALL 和 DELEGATECALL;使用 DELEGATECALL 时,目标代码在 Safe 的上下文中运行,使用 Safe 的存储和余额。
  • 升级、暂停协议自身的合约,或更改其参数。

有一处版本差异会影响矩阵。从引入 guard 的 Safe 1.3.0 到 1.4.1,transaction guard 只检查通过 execTransaction 执行的交易,不覆盖由模块执行的交易。Safe 1.5.0 为这条路径新增了独立的 module guard,但只有在设置之后才会生效:仅升级到 1.5.0,或仅设置 transaction guard,模块执行的交易依然不受检查。我们在本地分叉上验证了这两点:在设置了拒绝一切调用的 guard 的 Safe 1.4.1 上,owner 签名的交易被回滚,而模块的交易照常执行;在 Safe 1.5.0 上,直到设置了 module guard,模块的交易才被回滚。

逐行看矩阵

简而言之:Agent 读取、解码、模拟并起草;人批准确切的 payload 或具体的变更;Agent 之外的授权渠道负责签名和执行。下面每一行分三部分:Agent 可以做什么、谁来批准、由什么执行。

  • 查询余额和配置。Agent:限定在清单所列网络和 Safe 内的类型化读取。审批:事先商定的读取策略。执行:只读工具。
  • 解码和模拟。Agent:准备固定在某个区块上的证据。审批:由人审查覆盖范围和结果。执行:经授权的模拟器。
  • 准备一笔转账。Agent:未签名的草稿。审批:签名者审查确切的 payload。执行:Agent 之外的授权渠道。
  • 签名或提交。Agent:不在其范围内。审批:人工签名流程。执行:钱包或授权渠道。
  • 轮换密钥、更改 owner 或更改阈值。Agent:有记录的提案。审批:该变更的负责人和签名者。执行:管理渠道。
  • 更改模块、guard、fallback handler 或进行升级,或签署任何 DELEGATECALL。Agent:分析和草稿。审批:针对该变更的专项审查。执行:管理渠道。
  • 暂停合约或更改参数。Agent:诊断和提案。审批:流程中指定的负责人。执行:受限的外部工具。

有两行看起来相似,其实不同。准备一笔转账和批准一笔 DELEGATECALL 最后都可能是一次 execTransaction 调用,但后者可能改变账户本身。所以矩阵把它和模块或 guard 变更放进同一类审查。

写明每项权限背后的资源

只有写明了资源,一行才能真正执行。对每项权限,团队记下网络、Safe、代币合约、已批准的收款地址、Agent 可以起草的函数,以及适用的限额。

例外情况也一样。如果某个流程需要 DELEGATECALL,例如用于批量调用,矩阵应写明经过审查的目标合约及其版本,其他任何 DELEGATECALL 目标都不进入 Agent 的草稿。如果某个集成需要模块,矩阵要写明模块地址、负责人和计划移除的日期。

托管服务商的策略引擎覆盖什么

有些团队把 Agent 连接到由托管或钱包基础设施服务商持有的密钥上,这些服务商有自己的策略引擎,例如 Turnkey 或 Fireblocks。这些引擎对到达它们的请求执行规则。矩阵需要记录每个引擎执行了哪些行,哪些行依赖其他组件。

根据 Turnkey 的文档,组织内的每条策略都会在每次请求时被评估:任何匹配的 deny 优先;否则看是否有匹配的 allow;其余情况一律隐式拒绝。以太坊规则可以读取已解析的交易字段,例如收款地址、金额和 calldata;只有上传了合约接口,函数名和参数才可用;也可以评估类型化数据。同一份文档说明,由 root quorum 批准的操作不经过策略引擎,任何策略都无法限制它们。

根据 Fireblocks 的文档,其 Policy Engine 会把策略应用于从 Fireblocks Console 或其 API 创建的所有交易。规则按首次匹配原则自上而下应用,因此顺序很重要。合约调用规则可以按方法过滤。只有 workspace 的 Owner 或 Admin 级用户可以管理策略,提议的变更必须经审批组批准。

Safe 还带来了由合约定义、而非由服务商引擎定义的控制路径:

  • 受某个引擎管理的密钥可能只是多个 owner 之一。其他 owner 的密钥在别处、按别的规则签名。
  • 已启用的模块通过 execTransactionFromModule 执行交易,在这条路径上 Safe 不会校验 owner 签名;如果有校验,也由模块自身完成。只有当服务商的引擎管理着调用该模块或为其授权的密钥时,它才能看到这次执行;即便如此,引擎收到的也是发给模块的请求,而不是由 owner 签名的 Safe 交易,因此针对 Safe 交易编写的规则对它并不适用。
  • guard 和 fallback handler 都是在 Safe 合约本身中配置的。设置了 transaction guard 后,无论由哪把密钥签名,它都会检查每一次 execTransaction,但不检查模块执行的内容;在 Safe 1.5.0 上,这条路径需要单独的 module guard。fallback handler 负责响应对 Safe 未实现函数的调用。此外,已启用的模块无需 owner 签名即可更改上述全部配置,因为这些都是 Safe 对自身的调用。
  • DELEGATECALL 体现在 operation 字段中:这个字段既出现在 owner 签名的 SafeTx 里,通常是 EIP-712 类型化数据,也出现在提交该交易的 execTransaction 调用中。只有当规则按引擎实际收到的格式读取这个字段时,才能拦截它;如果密钥只对 SafeTx 的哈希签名,引擎就没有任何字段可读。

服务商的引擎是矩阵中的一个执行点,而不是矩阵本身。对 Safe 的 owner 来说,引擎读到的收款地址通常就是 Safe 本身;真正的目标地址、金额和 gas 退款字段都在 calldata 或类型化数据内部,只有规则对其解码时才会被检查。团队仍需决定写哪些规则、谁可以修改它们、哪些路径会绕过它们,例如 root quorum、模块或其他 owner,以及如何用反例测试这些路径。

需要单独决策的新模块

示例场景。一个支付集成请团队启用一个模块,用于执行定期转账。Agent 可以阅读模块的代码和文档,列出它在 Safe 上调用的函数,并起草 enableModule 交易。它不能决定是否启用。

启用和禁用模块都是 Safe 对自身的调用:要么经过达到阈值签名的交易,要么经过一个已启用的模块。模块一旦启用,就可以一直操作,直到有人执行 disableModule;把集成从团队的工具里移除,并不会把它从 Safe 中移除。到 Safe 1.4.1 为止,没有任何 guard 检查模块执行的内容;在 1.5.0 上,只有 module guard 会检查,而且前提是已经设置。因此,矩阵应把启用、之后的移除和 guard 问题当作三个独立的决定,各有指定的负责人。

用反例检验矩阵

只有 Agent 周围的工具真正执行矩阵,它才有用。第一项检查在任何人签名之前,把每份草稿归入某一行。这个示例使用 bash 和 Foundry 的 cast;规则刻意写得很短,应按你自己的矩阵调整。该函数接收 Safe 交易的全部九个字段,而不只是目标地址和 calldata,因为 gas 退款字段同样可能转移资金。

bash
# 在任何人签名之前,把 Agent 准备的草稿归入矩阵中的某一行。
# 需要 bash 和 Foundry 的 cast。规则只是示例:请按你自己的矩阵调整。
SAFE=0x...                        # Agent 为其准备草稿的 Safe
TOKEN=0x...                       # 此流程唯一可以转出的代币合约
RECIPIENT=0x...                   # 此流程唯一批准的收款地址
TRANSFER=$(cast sig "transfer(address,uint256)")
ZERO=0x0000000000000000000000000000000000000000

classify() {                      # 参数:SafeTx 的九个字段,按顺序
  [ "$#" -eq 9 ] || { echo "STOP: pass all nine SafeTx fields"; return; }
  local to value=$2 data=${3,,} op=$4 selector=${3:0:10}
  to=$(cast to-check-sum-address "$1")
  if [ "$op" != "0" ]; then
    echo "ADMIN REVIEW: operation $op is not CALL; with 1, DELEGATECALL, the target code runs with the Safe's storage"
  elif [ "$to" = "$(cast to-check-sum-address "$SAFE")" ]; then
    echo "ADMIN REVIEW: call to the Safe itself, which can change owners, threshold, modules, guard or fallback handler"
  elif [ "$5 $6 $7" != "0 0 0" ] || [ "$(cast to-check-sum-address "$8")" != "$ZERO" ] || [ "$(cast to-check-sum-address "$9")" != "$ZERO" ]; then
    echo "STOP: gas refund fields are set; the Safe would pay a refund to refundReceiver"
  elif [ "$value" = "0" ] && [ "$to" = "$(cast to-check-sum-address "$TOKEN")" ] && [ "${selector,,}" = "$TRANSFER" ] && [ "${#data}" -eq 138 ] \
    && [ "$(cast decode-calldata "transfer(address,uint256)" "$data" | head -n 1)" = "$(cast to-check-sum-address "$RECIPIENT")" ]; then
    echo "SIGNERS: token transfer to the approved recipient; review the exact payload and amount"
  else
    echo "STOP: target, function, recipient or value outside the matrix"
  fi
}

最重要的是那些不能被当作常规转账放行的情况:以 DELEGATECALL 发送的同一 payload、一次启用模块或更改 fallback handler 的 Safe 自调用、用 approve 代替转账、指向清单之外的合约或收款地址的转账,以及设置了 gas 退款字段的草稿,这会让 Safe 在转账之外再向第三方付款。

bash
MODULE=0x...                      # 正在审查的模块地址
NOREFUND="0 0 0 $ZERO $ZERO"      # safeTxGas baseGas gasPrice gasToken refundReceiver
PAY=$(cast calldata "transfer(address,uint256)" $RECIPIENT 5000000000)
classify $TOKEN 0 $PAY 0 $NOREFUND                                                   # SIGNERS
classify $TOKEN 0 $PAY 1 $NOREFUND                                                   # ADMIN REVIEW
classify $SAFE 0 $(cast calldata "enableModule(address)" $MODULE) 0 $NOREFUND        # ADMIN REVIEW
classify $SAFE 0 $(cast calldata "setFallbackHandler(address)" $MODULE) 0 $NOREFUND  # ADMIN REVIEW
classify $TOKEN 0 $(cast calldata "approve(address,uint256)" $RECIPIENT 5000000000) 0 $NOREFUND  # STOP
classify $MODULE 0 $PAY 0 $NOREFUND                                                  # STOP
classify $TOKEN 0 $(cast calldata "transfer(address,uint256)" $MODULE 5000000000) 0 $NOREFUND    # STOP
classify $TOKEN 0 $PAY 0 0 100000 5000 $TOKEN $MODULE                                # STOP

第二项检查看链上状态。集成被移除后,它的模块不应再处于启用状态,也不应出现矩阵未列明的模块、guard 或 fallback handler。下面的读取先列出 Safe 当前的配置,然后从被移除模块的地址模拟一次调用,不发送任何交易。

bash
export ETH_RPC_URL=...            # 该 Safe 所在网络的 RPC
cast call $SAFE "VERSION()(string)"
# 所有已启用的模块,而不只是你预期的那个 (第一页最多 10 个;如果第二个值不是 0x...01,继续查询)。
cast call $SAFE "getModulesPaginated(address,uint256)(address[],address)" 0x0000000000000000000000000000000000000001 10
# 从存储槽读取 transaction guard 和 fallback handler (为零表示未设置)。
cast storage $SAFE 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8
cast storage $SAFE 0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5
# 仅限 Safe 1.5.0:module guard (为零表示没有 guard 检查模块执行的内容)。
cast storage $SAFE 0xb104e0b93118902c651344349b610029d694cfdec91c589c91ebafbcd0289947
cast call $SAFE "isModuleEnabled(address)(bool)" $MODULE
# 反例:不是已启用模块的地址无法通过该 Safe 执行。
# 模块处于禁用状态时的预期结果:以 GS104 回滚。如果返回了结果,说明它可以执行。
cast call $SAFE "execTransactionFromModule(address,uint256,bytes,uint8)(bool)" \
  $SAFE 0 0x 0 --from $MODULE

我们按原样运行了这三段代码,对象是两个公开的主网 Safe,版本分别为 1.3.0 和 1.4.1,另外还在一个本地分叉上先启用、再禁用了该模块。分类结果与注释一致,模拟调用只在模块启用期间返回 true。在 Safe 1.4.1 上,模块列表显示了一个已启用的模块,而只检查单个地址的方法没有发现它。在分叉上,设置了 gas 退款字段的草稿被归为 STOP;我们仍然执行了这份草稿,结果 Safe 除完成转账外,还用自身余额向退款接收方付了款。这些检查覆盖的是你写下的规则;它们不能替代对模块代码或服务商策略的审查。

每项变更都有负责人

每一行都需要一名负责人,矩阵的每次变更都应像账户变更一样对待。有三类事件应触发复审:Safe 新版本,因为 guard 对模块的覆盖范围在 1.4.1 和 1.5.0 之间不同,而且 module guard 需要明确设置;服务商策略的变更,或可编辑策略的人员变更;以及任何新的模块、guard、fallback handler 或 DELEGATECALL 目标。

记录应显示谁批准了每项变更、运行了哪些反例以及结果如何。有了这份记录,团队之后才能解释 Agent 为什么能做某件事,或者为什么不能。

常见问题

在这份权限矩阵中,AI Agent 永远不做什么?AI Agent 不签名、不提交交易、不持有密钥,也不对 Safe 执行管理类变更。它只读取、解码、模拟并起草。

谁来为 Safe 启用模块?Safe 的 owner,通过达到阈值签名的交易,经由管理渠道完成。已启用的模块也可以启用模块,部署该 Safe 时的 setup 调用同样可以,这也是在给 Agent 授予访问权限之前列出所有已启用模块的又一个理由。Agent 可以分析模块并起草交易。

把矩阵变成交付物

矩阵是一份工作文档:每项能力一行,写明资源、负责人、每条规则的执行点,以及检验它的反例。在 AI Agent 接触 Safe 之前,团队应能说明每项操作需要哪种审批,以及 Agent 试图越界时是哪项测试失败了。

如果你想讨论自己 Safe 的权限设置,可以通过联系页面与我们联系;留言时请注明本文以及你考虑的 Safe 或流程,不要附上密钥或机密的运营资料。

参考来源

资料查阅于 2026 年 10 月 1 日。这些来源描述的是机制和配置;矩阵、模块场景和分类规则是本文提出的方案。已执行并保存输出:三段代码,使用 Foundry 的 cast 1.8.3,在两个公开主网 Safe 上和一个本地分叉上运行,包括 gas 退款的情况;在一个 Safe 1.4.1 本地分叉上进行的 guard 与模块测试;以及在一个新的 Safe 1.5.0 本地分叉上进行的 module guard 测试。已对照文档核实但未执行:对 Turnkey 和 Fireblocks 策略引擎的描述。

相关文章

需要把这些方法应用到你的上线或工作流吗?

申请技术审查,我们会一起梳理最安全的第一步。

相关文章