# AI 代理的代码签名：用证据审批会话

如果审批提示只写着“AI 代理请求访问”，它要求人类为一个身份不明的对象背书。在共享开发机器上，这可能意味着有人批准了复制来的脚本、遗留的终端，或由错误用户启动的进程。更有用的问题应该具体得多：哪个可执行进程正在请求访问？谁为它签名？它是如何进入系统的？这次运行期间它能做什么？

代码签名可以为这个决定提供证据。它能把正在运行的程序与签名机构关联起来，并显示签名内容是否发生变化。但它无法告诉你代理是否收到了恶意提示，无法证明可信发布者没有发布有问题的版本，也无法判断请求的生产操作是否合理。把签名误当成行为保证，团队就容易陷入麻烦。

在共享 Mac 上，可以利用签名机构识别已知代理客户端、拒绝异常请求，并让每次审批随获得授权的进程结束而失效。同时还要配合清晰的账户边界、范围受限的凭据，以及能够还原审批和后续调用的记录。

## 签名能识别代码，不能识别代码背后的意图

代码签名可以确认某段具体代码由谁签署，以及 macOS 是否仍能验证签名内容；它不能证明这段代码应该获得系统访问权限。每个审批流程都必须清楚保留这一区别。

Apple 的代码签名文档 Technical Note TN2206 将签名描述为验证代码并表达 designated requirement 的方式，macOS 可以据此在之后识别同一份代码。designated requirement 很重要，因为它比文件名更精确。名为 `agent` 的可执行文件可以被复制到任意位置并改名。如果签名链和要求仍能通过验证，签名身份会提供更强的连续性。

这种连续性可以回答一个实际问题：“这是我们同意允许的客户端吗？”但它无法回答以下同样实际的问题：

- 代理是否收到了不该进入生产环境的指令？
- 用户是否有意从预期项目目录启动了这个进程？
- 进程是否加载了会改变行为的扩展、插件或配置？
- 请求的 API 调用是否属于当前任务？
- 调用背后的凭据是否拥有超出任务所需范围的权限？

人们常把发布者名称当成结论，但它不是。签名机构只能告诉你，谁控制了用于签署该构建产物的签名凭据。大型发布者可以签署许多程序，小型内部团队也可以签署完全合适的构建。审批时应将观察到的身份与团队能够解释的允许列表比较，而不是因为颁发者听起来熟悉就批准。

还有一个容易被忽略的限制：签名验证的是被签署的代码，不会自动覆盖进程之后读取的一切。配置文件、提示文件、环境变量、仓库中的数据、下载的扩展和远程响应，都可能改变一个正确签名的程序的行为。即使代理的二进制文件看起来熟悉，也必须控制它可以执行的操作。

## 把审批卡当作归属记录来阅读

审批卡应在操作员批准前提供足够信息，让他把请求归属到真实进程。签名机构应放在顶部，因为它比进程标签更难伪造，但必须和进程路径、会话详情一起显示。

对于在共享机器上请求访问的进程，我希望在同一处看到：

- 可执行文件或应用身份，以及完整的本地路径。
- 用于识别它的签名机构或 designated requirement。
- 进程 ID 和父进程，以便知道它由什么启动。
- 拥有该进程的 macOS 用户账户。
- 操作通道和目标，例如 API 主机或 SSH 主机。

前三项事实可以捕捉不同类型的问题。熟悉的显示名称配上异常路径，通常意味着二进制文件被复制，或有人使用了包装器。熟悉的路径配上陌生的签名机构，可能意味着有人重新构建或替换了文件，也可能是符号链接指向了其他位置。熟悉的可执行文件由异常父进程启动，则可能说明是另一款自动化工具启动了它，而不是正在查看提示的开发者。

在共享 Mac 上，用户账户不是装饰。如果两位工程师共用一个登录账户，审批几乎无法说明是哪位用户启动了代理。机器仍能告诉你哪个进程发起了调用，但团队在审批流程开始之前，就已经失去了清晰的人类责任边界。为每个人设置独立的 macOS 账户，比事件发生后争论终端历史记录便宜得多。

不要训练人们只根据徽标、简短命令名或单独的机构字符串来批准。应让他们识别完整的预期组合：已批准的代理客户端、预期签名机构、预期本地位置、自己的账户，以及与任务相关的目标。缺少大部分证据的审批卡，会把一次点击变成猜测。

## 把可执行文件检查清楚，再将其设为已批准身份

在团队依赖某个客户端的签名机构开展实际工作前，应检查计划批准的确切应用或可执行文件。在初次设置时完成检查，将预期结果记录到内部操作手册中，并在有意升级客户端时重复检查。

在 macOS 上，`codesign` 可以显示签名详情。详细输出会写入标准错误，因此如果要保存审查材料，需要进行重定向：

```sh
codesign -dv --verbose=4 /Applications/ApprovedAgent.app 2\u003e\u00261
```

输出通常会包含类似字段：

```text
Executable=/Applications/ApprovedAgent.app/Contents/MacOS/ApprovedAgent
Identifier=com.example.approved-agent
Format=app bundle with Mach-O thin (arm64)
CodeDirectory v=20500 size=...
Authority=Developer ID Application: Example Developer (ABCDE12345)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
TeamIdentifier=ABCDE12345
Sealed Resources version=2 rules=13 files=...
```

应将 `Identifier`、叶节点 `Authority` 和 `TeamIdentifier` 放在一起看。单独使用 Team ID 作为审批规则并不可靠，因为一个组织可以为多个应用签名。单独使用 identifier 也更弱，因为任何人都可以创建一个具有相同 bundle identifier 的未签名程序。签名链共同构成了有用的身份声明。

然后验证签名内容：

```sh
codesign --verify --deep --strict --verbose=2 /Applications/ApprovedAgent.app
```

成功时通常不会输出内容。失败则表示嵌套组件被修改，或存在其他签名问题。`--deep` 会要求 `codesign` 递归检查嵌套代码，适合用作检查辅助，但不要把它当成所有内置组件都符合安全策略的证明。Apple 说明，深度验证会递归执行，可能掩盖应用包内部签名设计需要直接审查这一事实。

如果应用来自受管理软件渠道之外，还应让 Gatekeeper 评估它：

```sh
spctl --assess --type execute --verbose=4 /Applications/ApprovedAgent.app
```

`spctl` 和 `codesign` 回答的是相关但不同的问题。`codesign` 针对构建产物验证签名，`spctl` 则询问系统评估策略是否接受它。通过评估是有用的来源信号，但不代表应用的命令、脚本或远程行为适合生产环境。

记录检查过的路径。如果有人后来因为 `/Users/alex/bin/agent` 的标签看起来像在 `/Applications` 中检查过的应用，就批准了它，那么他并没有重复这项检查。路径本身就是证据的一部分。

## 解释器的签名不会为它运行的脚本背书

已签名的终端、运行时或 Shell，不能为启动时交给它的任意脚本担保。许多听起来合理、实际却失效的审批，都源于这个缺口。

例如，开发者可能用下面的命令启动代理：

```sh
/usr/bin/python3 /Users/dev/work/demo/tools/agent_runner.py
```

系统 Python 可能有已知签名，但这只能说明解释器二进制文件的情况。它无法说明 `agent_runner.py`、仓库中导入的文件、读取的 `.env` 文件，或通过标准输入传入的指令。如果审批界面只显示 `python3`，攻击者无需修改解释器，只要修改脚本或项目状态即可。

`node`、Shell、编辑器扩展和通用自动化运行器也有同样问题。像“批准由该运行时发布者签名的进程”这样的宽泛规则，会让团队轻易批准大量没人审查过的行为。它之所以受欢迎，是因为能减少审批阻力，但对于能够接触重要凭据的通道，这种做法并不安全。

选择下面两种模型之一，并明确告诉团队采用哪一种。更强的模型是批准一个专门构建、已签名的代理客户端，由该客户端的会话进程直接请求访问。更灵活的模型允许解释器，但把每次脚本启动都视为独立会话，并在解释器签名机构旁显示脚本路径、项目目录、参数和父进程。

可以用标准工具检查运行中进程的基本上下文：

```sh
ps -p 4821 -o pid=,ppid=,user=,command=
ps -p 4812 -o pid=,ppid=,user=,command=
```

第一条命令可能显示代理进程，第二条显示其父进程。将命令行与开发者正在进行的工作对照。如果子进程来自预期项目中的终端，证据是一致的。如果它来自无人值守的调度器、浏览器辅助进程或另一款代理，就不要再把请求视为普通的开发者审批。

对于基于脚本的客户端，应将脚本摘要纳入审批记录。一个简单的本地检查就能让变化变得可见：

```sh
shasum -a 256 /Users/dev/work/demo/tools/agent_runner.py
```

摘要不会让脚本自动可信，但当有人询问批准的脚本在两次运行之间是否发生变化时，它能提供具体答案。只应为经过审查的版本或受控项目状态保存预期摘要，不要养成从聊天消息复制哈希值的形式主义。

## 审批规则之前，共享机器需要账户边界

只要每个人使用独立账户、每次代理运行都有明确负责人，共享开发 Mac 就可以管理。如果所有人共用一个登录账户，签名机构无法弥补缺失的归属信息。

为每位开发者创建独立的 macOS 账户，日常工作不要使用公共管理员账户。代理进程应在启动它的用户下运行。这样，项目文件、终端历史、环境变量和审批决定才有明确的负责人。共享仓库不等于必须共享操作系统账户。

可行的设置还应区分敏感目标。为开发、预发布和生产环境使用不同的凭据条目，并采用能让操作员看出用途的标签。对 `inventory-staging` 的审批不应因为两个环境指向同一个 API 客户端，就悄悄选择生产凭据。如果凭据名称掩盖了环境，人类就无法在关键时刻做出可靠选择。

物理访问同样重要。有人坐在一台未锁定的共享 Mac 前，就可以在当前账户下启动进程，然后等账户所有者点击审批提示。离开时锁定屏幕，设备休眠后要求重新登录，也不要在公共区域留下正在运行的高权限终端。这些控制措施看似普通，所以团队往往会跳过它们，直到不得不收拾一场本可避免的混乱。

不要用发布一份庞大的已批准签名机构列表来解决共享机器风险。列表会不断加入编辑器、语言运行时、包管理器、构建工具和辅助应用，最后只能说明这台机器用于开发。对于能够请求外部操作的代理，应保持允许集合足够小，并记录每个身份为何属于其中。

## 会话审批应绑定一个进程，而不是永久绑定一个人

会话审批应在该进程运行期间授权一个已观察到的代理进程，并在进程退出时失效。这是在每次无害请求都要求决定，与授予会持续存在的长期权限之间的实用折中。

进程边界比日历时间限制更重要。代理退出并重启后，就是新的执行上下文。它可能拥有不同的工作目录、被修改的二进制文件、不同的扩展、不同的父进程，或键盘前的不同用户。重启时要求重新授权，能让操作员再次看到这些变化。

这正是代码签名发挥作用的地方。审批流程可以先显示签名机构，帮助操作员识别预期客户端；会话绑定则防止这种识别变成无限期授权。代理进程退出后，审批也应随之消失。如果操作员发现可疑行为，应能在详细调查代码签名前立即撤销会话。

要把授权与身份验证分开。Touch ID 或密码可以证明当前的 macOS 用户批准了请求，但无法识别进程。代码签名可以帮助识别进程，但两者都无法决定目标和操作是否合适。优秀的审批卡会展示这三类证据，而不是假装它们可以相互替代。

对于普通仓库工作，一次会话决定可以覆盖针对开发 API 的重复读取操作。若操作会修改部署配置、轮换凭据、删除记录，或打开到生产主机的 SSH 连接，就应对该调用要求新的决定。阻力应放在后果发生变化的地方，而不是在计时器到期后随机出现。

## 签名机构无法阻止人们以为它能阻止的失败

有效的签名链无法阻止可信客户端收到有害输入、被攻破的开发者账户签署恶意代码，或已批准进程发出不明智的请求。应把它们视为不同的失败路径，并为每条路径设置正确的控制措施。

第一条路径是提示或仓库指令攻击。代码助手读取恶意注释，被要求通过 HTTP 请求窃取配置文件。可执行文件可能正是已批准且正确签名的客户端。审批需要显示目标和请求通道，因为签名无法说明它遵循了什么指令。

第二条路径是团队尚未审查的合法更新。发布者可以使用同一签名机构为新版本签名。如果不检查身份和发布来源，就批准该机构未来发布的任何版本，那么整个策略就变成了“相信发布者所有权”。对于低风险本地工具，这或许可以接受；对于能够使用生产凭据的代理，这远远不够。

第三条路径是本地进程操控。已签名应用可能加载插件、继承环境变量，或由提供异常参数的父进程启动。macOS 的保护措施可以减少某些篡改方式，但审批系统仍需显示真实的进程上下文。证据相互冲突时，应拒绝请求并检查机器，不要因为机构字符串熟悉就自行编造一个良性解释。

第四条路径是权限过大。身份确认无误的客户端，仍可能使用一个允许它执行远超任务所需操作的凭据。应把凭据权限限制到代理需要访问的 API、主机、仓库和环境。代码签名可以告诉你哪个客户端使用了凭据，却无法事后缩小凭据权限。

因此，“已签名就等于安全”是一条有害建议。它听起来简单，也能减少提示，但会鼓励人们在没有目标、操作摘要或会话边界的情况下批准请求。签名信号能帮助人类区分已知代码和未知代码，却绝不能抹去决策的其他部分。

## 不可逆或高影响凭据应逐次调用审批

当一次请求可能造成会话审批不应默默允许的后果时，每次使用凭据都应要求人工决定。判断标准是操作影响，而不是代理可执行文件看起来是否可信。

对能够写入或删除生产数据、改变身份或权限、建立外部承诺、发布软件，或进入敏感 SSH 环境的凭据，应启用逐次调用审批。操作员在使用时应看到凭据标签和目标。像“使用密钥”这样的通用提示，会迫使人们在压力下记住过多信息。

低影响工作仍应保持易用。如果开发者每次读取测试 API 都必须审批，就会学会不阅读提示直接点击。这会削弱控制，也让真正重要的提示更容易被忽略。当代理针对开发目标执行重复且范围明确的工作，且进程身份符合预期时，会话审批很合适。

决策阶梯应简单到让人经历忙碌的一周后仍能解释清楚。首先，锁定的凭据保管库拒绝所有操作。接着，新代理进程需要会话授权。最后，选定的凭据每次使用都需要审批。不要把这些决定埋进只有一个人能解释的自定义策略语言中，隐藏例外正是共享机器规则逐渐失效的地方。

Sallyport 直接采用这三种控制：锁定保管库会拒绝操作，新代理进程默认请求会话授权，选定凭据可以要求每次使用都审批。其审批卡首先显示进程的代码签名机构，这对于多人共用一台 Mac 的情况是正确的起点。

## 将审批和调用作为两个独立事件审计

代理会话和单次操作需要分别记录，因为单独任何一种记录都无法回答所有事件问题。会话记录说明哪个进程何时获得授权，以及授权何时结束。操作记录说明它在批准后尝试了什么、针对哪个通道或目标，以及返回了什么结果。

实用的调查顺序如下：

1. 找到请求访问的进程对应的会话记录。
2. 检查用户账户、可执行文件路径、签名机构、父进程和审批时间。
3. 找出该会话期间发起的调用，并将目标与分配的任务比较。
4. 如果会话仍处于活动状态，先撤销它；如果调用显示出滥用迹象，再停用或轮换受影响的凭据。
5. 在修改项目文件或重新安装客户端前，保留相关记录。

顺序很重要。团队常常一开始就阅读代码，结果丢失了实际发生过什么的证据。应先确认授权与操作的先后关系，再检查可执行文件、仓库状态、Shell 历史和相关配置。

防篡改证据在这里很有价值。如果本地进程可以重写审计记录，而该进程本身又属于正在调查的事件，那么这类记录提供的保障很少。哈希链可以让验证者发现记录序列中的修改或删除，但无法证明所有可能发生的事件都被捕获。必须准确说明这个限制，防篡改证据不是全知全能。

Sallyport 将会话和活动日志记录在同一份加密、哈希链式审计日志中，`sp audit verify` 可以离线检查链条，无需保管库凭据。这让事件后例行验证，或将记录交给其他审查者，都更加实际。

## 让审批决定可复现，而不是依赖个人判断

团队应该能够解释某个代理会话为何获得批准，而不依赖点击审批者的记忆。为每个允许的代理客户端写一份简短的审批配置，并将它放在仓库操作说明附近。

配置应列出预期应用或可执行文件路径、标识符、签名机构、正常父进程、允许的用户账户、可用环境，以及每次调用都需要决定的凭据。这不是为了形式主义，而是为新工程师提供可观察的标准，也让值班人员能够拒绝异常请求，而不用争论个人偏好。

出现以下变化时应审查配置：代理客户端更新、团队采用新的运行时包装器、凭据获得更强权限，或原本本地的流程开始访问共享服务。如果签名身份发生变化，应通过团队信任的软件来源验证这一变化。不要先点击一次、打算以后再调查，从而让异常机构变得“正常”。

在共享 Mac 上进行一次有意设计的失败演练。从预期账户启动已批准客户端，确认显示的签名机构和会话行为。然后通过已签名解释器启动未批准脚本，从不同账户启动客户端，并请求一个标记为逐次调用审批的凭据。每种情况下，正确行为都应让操作员一眼看懂。如果提示看起来过于相似，就在真正犯错前改进显示的信息。

代码签名之所以有用，是因为它把模糊的进程名称替换成了可以检查的证据。让它的职责保持在这里。为眼前的工作批准已知进程，限制凭据权限，并让可疑会话在证据仍然完整时即可撤销。
