# 面向 AI 代理选择 HTTP 还是 SSH：限制影响范围

AI 代理应在服务能够将目标动作表达为一个范围明确、经过身份验证的操作时使用 HTTP。只有当任务需要 API 无法提供的主机级能力时，才应使用 SSH，而且必须通过专为该任务设计的账户和命令接口完成。

常见错误是把两种传输方式拿来比较，仿佛一种更现代，另一种更老旧。这样会错过真正的决策点。HTTP 和 SSH 都只是传递手段。真正决定代理能否完成受控修改，还是会带着管理员权限在生产主机上到处活动的，是你赋予它们的权限、接受的输入，以及保留的证据。

我见过团队因为代理只需要一个运行事实，就发放一把所谓临时的 SSH 密钥。一个月后，这把密钥已经能读取部署密钥、访问内部服务并运行交互式 Shell。没有人做出过什么戏剧性的安全决定，他们只是接受了一个方便的默认配置。小任务正是这样获得巨大影响范围的。

## 接口决定代理得到的权限

面向 AI 代理选择 HTTP 还是 SSH，讨论的应是能力形状，而不是协议偏好。HTTP 调用可能很宽泛且危险，SSH 连接也可以严格限制。实践中，API 更常提供约束权限的合适位置，因为端点、方法、请求结构和令牌权限可以共同描述一个操作。

考虑这样一条指令：重启一个失败的工作进程。`POST /workers/worker-17/restart` 这样的 HTTP 端点明确写出了目标和允许使用的方法。服务可以拒绝未知工作进程，要求拥有重启权限的角色，并将记录与令牌关联起来。`ssh host sudo systemctl restart worker` 这样的 Shell 指令则暗含更宽泛的权限。它依赖账户、sudo 配置、单元命名规则、Shell 解析和主机状态全部正确。

这并不意味着 API 自动安全。一个能够调用所有端点、创建其他令牌或导出所有记录的令牌，虽然藏在整洁的 URL 后面，影响范围仍然很大。同样，一个只接受允许列表中固定工作进程标识符的强制 SSH 命令，可能比设计糟糕的管理 API 更安全。

连接任一工具前，先做一个测试：用一句话写出最小的成功操作，然后列出代理产生意外输入时，同一凭据还能做什么。如果你无法解释后半部分，就还没有真正测量权限范围。

一个范围明确的接口有四个特征：

- 指定少量目标，而不是整个环境。
- 接受结构化输入，并使用可以校验的语法。
- 拒绝当前任务不需要的相邻操作。
- 创建记录，让其他人之后能够解释结果。

任务还应决定凭据可以存活多久。一次运行使用的凭据，不应因为没人记得删除，就悄悄变成长期访问权限。以进程生命周期为边界，比依靠日历提醒更可靠。

## 只有 API 真正执行边界时，HTTP 才能缩小影响范围

当服务在资源和操作层面检查授权时，HTTP 才能为代理提供更小的影响范围。Bearer 令牌只是传递凭据的载体。它是否安全，取决于服务器收到令牌后验证了什么。

RFC 9110 按语义描述 HTTP 方法，包括安全方法与幂等方法之间的区别。这些概念有助于处理重试和意图，但不会授予权限。`GET` 也可能泄露敏感材料。`PUT` 可能是幂等的，却仍然会覆盖生产配置。应把方法名当作客户端行为的线索，而不是权限模型。

在把令牌交给代理前，向服务负责人询问：

- 这个令牌具体可以调用哪些路径和方法？
- 服务会对每个资源检查访问权限，还是只检查宽泛的集合？
- 令牌能否创建凭据、修改权限或触发导出？
- 请求是否可能通过某个标识符跨入另一个租户、项目或环境？
- 服务是否记录凭据身份和请求结果？

还要问一个不太舒服的问题：读取权限是否会泄露超出任务需要的信息。仓库 API 可能允许读取源代码、合并请求评论、构建日志和配置。一个保存不当的配置值，就可能让读取权限等同于访问密钥。如果代理只需要某次部署的状态，就给它一个返回该状态的端点。不要交给它通用仓库令牌，然后把结果称为最小权限。

请求结构与权限范围同样重要。比较下面两个请求：

```http
POST /v1/releases/release-42/promote HTTP/1.1
Authorization: Bearer token-value
Content-Type: application/json

{"environment":"staging"}
```

```http
POST /v1/admin/execute HTTP/1.1
Authorization: Bearer token-value
Content-Type: application/json

{"operation":"promote","arguments":{"environment":"staging"}}
```

两者都可能提升一个版本。第一个请求几乎不给服务器留下解释操作的空间。第二个请求却创建了一个管理调度器。调度器会吸引例外，接着出现任意操作名，最后形成一个真实权限很难描述的令牌。除非服务器使用严格的操作允许列表，并分别校验每种操作的参数结构，否则我不会把这类调度器用于代理。

如果服务支持，请为不同动词使用不同凭据。把观察与修改分开，也把日常修改与身份或账单变更分开。这样会增加一些配置工作，但授权失败会变得有意义。拒绝说明任务定义与凭据不匹配。宽泛令牌则会把每个错误都变成一次成功请求，等你事后调查。

不要把长期有效的 API 密钥放进代理提示词、环境文件、仓库设置或工具配置。问题不只是输出中可能意外泄露。代理会检查自己的环境，工具会收集诊断信息，而拥有明文访问权的进程可能把密钥传到另一个目的地。让密钥留在代理进程之外，只授权最终动作。

## SSH 会暴露主机，除非你有意移除 Shell

SSH 的默认影响范围很大，因为交互式账户可以检查文件、运行程序、修改配置、打开隧道，并使用账户能够访问的所有网络路径。你打算只运行一个命令，并不会限制一个获得普通 Shell 的账户。

RFC 4251 将 SSH 描述为用于安全远程登录及其他安全网络服务的协议。它有意支持会话、通道、端口转发以及多种身份验证方式。这些能力对管理员很有用，却不适合作为自主执行者的起点，尤其是当执行者只需要一项受限维护操作时。

命令本身也很重要。`systemctl restart service-name` 看起来有边界，但要继续追踪周围的权限：该账户可以重启哪些单元，单元文件是否能运行特权钩子，账户能否编辑这些文件，服务名称是否来自经过校验的输入。一个看似普通的操作，可能通过被重启的服务触达部署凭据、挂载卷或内部控制平面。

如果确实需要 SSH，就编写一个输入语法封闭的小型远程程序。程序应将已知请求字段映射到已知操作，而不是把请求拼接成 Shell 命令。除非能用严格的允许列表校验，否则不要接受文件路径、主机名、正则表达式、Shell 片段或环境变量赋值。

受限的 `authorized_keys` 条目可以清楚地呈现这条边界。下面的模式会强制使用单一接收程序，并移除代理很少需要的几项 SSH 功能：

```text
command="/usr/local/libexec/agent-maintenance",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample agent-runner
```

这行配置本身不能解决授权问题。`agent-maintenance` 程序必须拒绝未知子命令并校验参数。运行账户只能拥有该程序所需的文件、服务和网络权限。如果程序调用 `sudo`，sudo 规则必须指定固定的可执行文件，不能允许编辑器、解释器、通配符或 Shell 转义路径。

OpenSSH 的 `authorized_keys` 手册记录了 `command=`、`no-pty` 和转发限制。把这些选项看作安全带，而不是整辆车。它们能移除几条容易利用的逃逸路径，但以过高权限账户运行的强制命令，仍然拥有过高权限。

一个有用的远程输入协议可以是标准输入上的普通 JSON：

```json
{"action":"restart_worker","worker":"worker-17","request_id":"8b4f3c2a"}
```

接收程序只能接受 `restart_worker`，以及自身清单中的工作进程名称。它应在执行前写入事件，调用不经过 Shell 的固定程序，捕获退出状态，并写入完成事件。如果代理发送 `worker-17; cat /etc/shadow`，校验必须在任何操作系统命令运行前拒绝完整值。

不要仅仅因为人类已经用 SSH 做同样的工作，就给代理 SSH 访问权限。人类能够发现异常提示、注意主机名不匹配，并在结果出乎意料时停下来。代理需要把限制嵌入接口。

## 日志必须说明尝试的操作和最终状态

写着“请求失败”的日志行不是审计记录。它可能足以调试客户端库，却无法说明代理是否改变了什么、是否有人批准了操作，或事件发生后应该检查什么。

对于 HTTP，应记录代理运行身份、凭据身份或凭据标签、目标主机、方法、规范化路径、安全表示形式的请求体、响应状态、请求关联值、开始时间和完成结果。在事件离开操作边界前，先删去密钥和敏感字段。为了保留证据而记录 `Authorization` 请求头，等于主动制造一次泄露。

对于 SSH，应记录主机身份、远程账户、强制命令名称、经过校验的参数、源进程身份、退出状态、标准错误分类和远程操作标识符。单独记录原始命令字符串证据不足，因为它可能隐藏引号处理，也无法说明接收程序实际接受了哪些参数。

事件序列应区分意图和效果。下面这种形状适用于两种传输方式：

```json
{"event":"authorization_granted","run":"r-204","action":"restart_worker","target":"worker-17"}
{"event":"action_started","run":"r-204","transport":"ssh","operation":"restart_worker","request_id":"8b4f3c2a"}
{"event":"action_finished","run":"r-204","outcome":"success","remote_status":0,"request_id":"8b4f3c2a"}
```

如果连接在 `action_started` 之后中断，应写入 `outcome:"unknown"`，不要擅自写成失败。这个词会迫使下一步走正确流程：先查询远程状态，再决定是否重试。它也让后续调查保持诚实。

普通日志还有保管问题。拥有主机管理员权限的人，或获得足够权限的进程，都可能截断、改写或删除日志。集中收集有所帮助，但当收集器或网络路径发生故障时，仍可能留下缺口。如果日志必须用于解决有关代理行为的争议，就应保留带完整性检查的追加式记录，并在操作路径之外进行验证。

当每个事件都纳入前一事件的摘要时，哈希链可以让修改变得可检测。它不能证明记录器看到了所有事件，也不能让不可信的时钟变得可信。这些限制很重要。哈希链只能回答一个更窄但很有用的问题：有人是否在事后修改了这段保留的序列？

Sallyport 为代理运行保留 Sessions journal，为单次调用保留 Activity journal，两者都从加密哈希链审计日志中生成。它的 `sp audit verify` 命令可以在离线状态下对密文检查哈希链。当你需要检查证据，却不想先暴露密钥时，这正是正确的特性。

## 超时会产生未知结果，而不是失败操作

网络故障是最容易让原本谨慎的团队造成重复修改的地方。客户端发送请求，远程端执行了操作，但响应消失。代理看到超时，于是再次执行。SSH 也会发生同样的情况，例如远程命令已经启动，但客户端尚未收到退出状态时连接中断。

不要让代理把传输错误理解为可以重试修改操作。先判断操作类型。

只有在重复相同请求会得到相同目标状态且不会产生额外效果时，操作才是幂等的。把某个工作进程的期望状态设为 `running` 可能符合这个定义。创建付款、追加记录、轮换密钥或重启进程通常不符合。重启可能打断第一次尝试已经开始的恢复流程。

如果 API 支持幂等标识符，就使用它。服务必须将标识符与已完成的效果一起保存，并在收到重复请求时返回之前的结果。客户端提供的请求标识符如果只是由服务器记录，并不能防止重复执行。

对于远程命令，增加一个能够回答明确问题的状态操作。`restart_worker` 返回失败后，查询工作进程当前代次、最近一次重启请求标识符和健康状态。如果接收程序在执行前保存请求标识符，并在状态中返回它，就能告诉重试中的代理该请求是否已经执行。

下面的失败过程说明了这一点的重要性：

1. 代理使用请求标识符 `8b4f3c2a`，提交对 `worker-17` 的重启请求。
2. 接收程序记录标识符并重启工作进程。
3. SSH 连接在工作进程停止时中断。
4. 代理查询状态，而不是再次重启。
5. 状态响应显示同一标识符仍在执行，因此代理等待并检查健康状态。

重试次数上限无法解决结果不明确的问题。它只能在错误决定重试后限制损害。观察状态才能修正决定本身。

HTTP 还有另一个风险：服务有时会在异步工作完成前返回成功状态。`202 Accepted` 表示服务器接受了稍后处理的工作，并不表示请求的状态已经存在。应要求服务提供操作资源或状态端点，让代理等待与任务真正相关的终态。

SSH 也有对应陷阱，例如命令把工作放到后台后以零退出码结束。不要把启动器的零退出码当作维护操作已经完成的证明。让接收程序等待完成，或者返回一个代理可以查询的持久操作标识符。

## Shell 命令隐藏的权限比文字显示的更多

最短的远程命令，往往隐藏着最宽的权限。Shell 展开、环境继承、当前目录、配置文件和可执行文件搜索路径都会影响实际运行的内容。代理可能生成看似无害的文本，但主机会结合上下文进行解释，从而触发意外结果。

避免这种模式：

```sh
ssh ops@host "deploy $branch $environment"
```

即使调用方今天能够正确引用变量，远程 Shell 仍会解析一门命令语言。部署脚本可能进行自己的展开。分支名称可能选择源位置，环境名称可能选择凭据或目标集群。在声称输入受限之前，必须检查每一层。

应使用读取结构化输入、直接调用固定可执行文件的接收程序。在大多数语言中，这意味着使用参数数组，而不是把字符串传给 `sh -c`。接收程序应负责把面向用户的目标名称映射到主机特定的标识符。不要让代理自行发现文件系统路径或服务单元名称。

HTTP 参数也有同样的问题。`/files?path=...` 这样的路径看起来很结构化，但服务器可能会把这个值传给文件系统操作。只有当服务器校验了输入的含义，而不是把命令解析转移到 URL 后面时，API 才能降低风险。

凭据存放位置会改变代理进程遭到攻破后的后果。如果代理在本地保存 SSH 私钥或 API 令牌，任何能读取这些材料的进程都可以在之后继续行动。独立的操作边界可以持有密钥，只向远程服务传递请求和授权决定。两者差别很明确：不让密钥出现在模型输出中，不等于让密钥离开代理进程。

Sallyport 对 HTTP 凭据和 SSH 密钥采用后者的方式：应用将它们保存在加密保险库中，并执行请求的操作，而不是把凭据材料交给代理。这种安排不会让危险请求变得安全，因此仍然需要限制目标并检查批准。

## 用书面能力比较来选择传输方式

不必召开冗长的风险研讨会，也能做出有依据的选择。为拟议的 HTTP 调用和 SSH 命令各写一行，然后用相同事实填充。诸如“读取权限”或“维护权限”这样的模糊标签不算数。

使用以下五点进行比较：

1. 说明确切结果，例如“获取服务 A 的部署状态”或“健康检查失败后重启 worker-17”。
2. 列出所有可触达目标：API 集合、项目、主机、服务、文件和网络目的地。
3. 列出同一凭据或账户在预期结果之外仍能执行的所有修改。
4. 说明超时、拒绝或看似成功后可以获得哪些证据。
5. 定义批准边界：一次运行、一次调用，或使用固定身份的预批准例行操作。

如果 HTTP 行能够列出更小的目标集合、提供更窄的操作并留下更清晰的记录，就选择 HTTP。如果远程接收程序能够比 API 更好地做到这些，或所需主机操作根本没有 API，就选择 SSH。如果两行都不够窄，先不要连接代理。先构建缺少的端点或接收程序。

这个比较还能发现一个常见的糟糕建议：“读取用 SSH，写入用 API。”它听起来谨慎，因为 Shell 访问让人感觉偏运维，而 API 调用让人感觉偏事务。但它是错误的。读取主机可能暴露凭据、源代码、客户数据和网络拓扑，而范围明确的 API 修改可以只改变一个目标状态。读取和写入不是充分的风险分类。可触达的数据和可产生的副作用才是。

一个负责诊断构建失败的代理，可能需要用于查询作业状态的 HTTP 端点、用于获取有限日志窗口的 API 调用，以及只有在特定修复确实需要时才访问某一台主机的远程接收程序。拆分任务会增加接口，但也能避免日常诊断任务因为某次罕见修复需要 Shell，就长期携带 Shell 凭据。

## 批准应匹配错误操作的代价

当批准出现在人能够理解的边界上时，它最有价值。每次无害的状态读取都要求批准，会训练人们不阅读就点击。一次宽泛的批准覆盖未来所有可执行文件，则完全取消了有意义的审核。

当新的代理进程首次请求执行操作，并且能够清楚显示其身份时，使用按运行批准。人员可以将请求执行的程序与原本要启动的工作进行比较。若运行行为异常，就撤销该运行，然后通过操作记录调查之前的调用。

对于具有不可逆或高代价副作用的操作，使用逐次调用批准，例如轮换凭据、删除、提升到生产环境、账户变更，以及代理可能动态选择目标的任何操作。批准提示应以普通语言写明目的地和操作。“执行工具请求”几乎无法帮助审核者判断。

不要试图用一套庞大的例外规则语言取代判断。团队最后会维护第二个编程环境，其边界情况可能授权执行原本想阻止的操作。一组容易检查的固定控制更好：保险库锁定状态、运行批准，以及按需批准每次敏感凭据的使用。

批准不能弥补权限无限的凭据。它只是让人有机会在操作离开机器前阻止它。底层服务仍必须执行自己的授权，审计记录也必须保留批准后发生的事情。

## 在第一次事故前建立远程操作边界

最好的第一步通常不是编写更复杂的代理提示词，而是用一个操作边界替代宽泛凭据。这个边界应有明确的输入语法、有限的目标集合、超时处理方案，以及其他操作人员可以验证的证据。

对于 HTTP 任务，要求服务负责人提供一个端点和一份权限只匹配该操作的凭据。像测试成功调用一样，有意识地测试被拒绝的路径和方法。对于 SSH 任务，创建专用账户、关闭交互功能、强制使用接收程序，并用错误输入测试它。测试应从代理将使用的同一路径进行，因为网络访问和身份检查往往不同于管理员笔记本电脑上的情况。

然后测试没人愿意模拟的故障：远程工作完成后，在响应抵达调用方之前切断连接。如果代理不能根据保留记录和远程状态判断应该等待、查询还是重试，系统就会在压力下重复执行工作。

一旦坚持这些要求，协议选择就会变得简单。使用能够授予你可以明确描述的最小能力、在失败后给出可靠答案，并留下操作记录的接口，而不是依赖某个人对终端会话的记忆。
