# 本地代理操作网关健康检查

本地操作网关只有在能够拒绝错误操作、执行正确的受控操作，并留下经得起审查的证据时，才算健康。绿色的进程监控几乎无法证明这些事情。它只能告诉你某个东西拥有一个 PID，却不能告诉你保险库是否锁定、代理是否能够连接网关、注入的凭据是否仍然有效，或者审计链是否记录了这次调用。

Sallyport 中的情况尤其需要区分，因为应用、保险库闸门、授权决定、外部操作和加密审计日志都是独立的故障点。把它们当成一个笼统的“健康”信号，会造成最糟糕的监控：控制措施已经失效，监控却仍然显示绿色。

把它做成一个小型验收套件，而不是一堆端口检查。套件应使用无害的金丝雀目标，返回非秘密证据，并正确识别保险库锁定的状态。如果必须由人员通过 Touch ID 解锁保险库，监控就应明确报告这一状态，而不是试图绕过它。

## 应用正在运行只是第一个条件

可用性探针应该只回答一个狭窄的问题：本地 Mac 能否启动并让网关应用持续运行？它不应假装运行中的进程就能证明外部操作有效。

对于菜单栏应用，先从监督程序可以在不接触凭据的情况下运行的本地检查开始。确切的进程名称和安装位置可能因版本而变化，因此应将这些值放在一个本地配置文件中，而不是散落在各个脚本里。基础 shell 探针可以这样写：

```sh
#!/bin/sh
set -eu

APP_NAME="Sallyport"

if ! pgrep -x "$APP_NAME" >/dev/null 2>&1; then
  open -a "$APP_NAME"
  sleep 2
fi

if pgrep -x "$APP_NAME" >/dev/null 2>&1; then
  printf 'app=ready\n'
  exit 0
fi

printf 'app=unavailable\n' >&2
exit 2
```

这个检查有意只做出有限的声明。`open -a` 请求 macOS 启动应用，`pgrep` 则在请求后观察进程。它不能证明应用已经完成初始化，不能证明保险库能够响应请求，也不能证明 MCP shim 可以接受客户端连接。Apple 将 Launch Services 说明为用于启动和激活应用的系统接口，因此通过操作系统启动应用，比把应用包路径硬编码进脚本更好。

让输出保持结构化且简单。对调度器来说，`app=ready` 已经足够。不要把 `ps` 输出写入集中式日志，否则命令参数、用户名或无关的进程细节会永久堆积。

进程可能存在，但已经卡死。它也可能因为 Mac 处于睡眠、用户已退出登录，或更新后正在重启而暂时不存在。告警规则需要为这些预期情况设置宽限期。每次笔记本合上盖子都触发告警的健康检查很快就会被禁用，到真正发生故障时也就帮不上忙了。

因此，有用的可用性信号应该是本地且有边界的：应用已启动，在短暂的稳定等待期后仍然存在，并且 MCP 客户端可以开始会话。最后一项应放在单独的探针中，因为它测试的是另一条边界。

## 保险库状态必须是明确的结果

保险库锁定时拒绝操作，这本身是健康的。把这个状态称为服务中断，会混淆安全行为与服务故障。

Sallyport 的保险库闸门规则是绝对的：锁定期间，所有操作都会被拒绝。在受支持的 macOS 硬件上，保险库闸门使用 Secure Enclave 和 Touch ID。检查必须保留这条规则。不要编写向界面输入密码的脚本，不要保存绕过生物识别的令牌，也不要把已解锁的桌面会话当成应该解锁保险库的证明。

不要只使用布尔值，而应使用四种结果状态：

- `ready`：应用可用，保险库已解锁，可以运行受控检查。
- `locked`：应用可用，但保险库正确地拒绝了操作。
- `denied-unexpectedly`：保险库已解锁，但预期的金丝雀操作却被拒绝。
- `unavailable`：应用或其本地 MCP 路径无法响应。

这套词汇可以避免一种常见的运维错误。团队经常在夜间安排需要凭据的探针，屏幕锁定后看到检查失败，于是不断削弱系统，直到系统能够自行解锁。他们并没有修好监控，而是移除了保险库原本要求人工做出的决定。

实际做法应分两部分运行。无人值守的任务记录应用可用性和锁定保险库的拒绝结果。人员，或已经得到人工批准的受控工作站会话，在解锁后启动需要凭据的检查。将原因写入运行输出：

```json
{
  "run_id": "hc-2026-07-22T141501Z-8f29",
  "app": "ready",
  "vault": "locked",
  "http": "skipped",
  "ssh": "skipped",
  "audit": "verified",
  "reason": "credentialed checks require an unlocked vault"
}
```

运行 ID 不是秘密。它让操作人员之后可以用一个稳定的值与活动记录进行比对。不要使用用户名、机器序列号、端点 URL 或凭据标签作为运行 ID。

每次会话授权和每次调用审批也需要单独处理。新的代理进程可能需要会话审批，某个凭据也可能要求每次使用都获得审批。这是预期行为，不是不稳定的测试。运行器应说明它是为已批准的会话设计的，还是每次调用都由人员审批。无声超时不会给下一位调查人员留下任何有用信息。

## 使用能够证明凭据注入的金丝雀目标

HTTP 检查应调用专门用于验证某个金丝雀凭据的端点，并返回固定的非秘密结果。测试公共 URL 只能证明网络可用，无法说明网关是否选中了预期凭据、是否将凭据插入正确的标头，或是否将凭据与代理隔离。

建立一个小型服务，只接受一个路径、一种方法和一种凭据形式。在服务端保存预期的金丝雀秘密，在网关保险库中保存同一个金丝雀秘密。触发操作的客户端不应收到该秘密，服务也不应回显它。

响应契约可以简单到这样：

```json
{
  "check": "agent-gateway-http",
  "result": "ok",
  "request_id": "7d7a0f3c"
}
```

凭据缺失或错误时，服务应返回 `401`；方法错误时返回 `405`；只有在收到预期凭据时才返回 `200`。在服务端生成 `request_id`，并保持其不透明。不要根据授权标头或传入请求的任何部分生成它。

使用专用路由，例如 `/agent-gateway-canary`。不要把检查附加到现有生产端点上。生产端点会随着时间积累速率限制、重定向、内容协商、缓存规则、计费副作用和权限变更。金丝雀路由则可以始终保持有意的简单。

RFC 9110 定义了请求方法的语义，并将 GET、HEAD、OPTIONS 和 TRACE 归为安全方法。但 HTTP 中的“安全”意味着请求的操作不应改变资源的预期状态，并不意味着它不会影响账户、日志、配额或下游行为。API 可以记录 GET 请求、收取请求费用，或者因为实现不佳而触发副作用。应建立一个能够检查服务器端行为的路由，而不是仅仅相信熟悉的方法名。

一个常见但糟糕的建议是使用带生产 API 令牌的 `curl` 作为健康检查。它之所以流行，是因为只需一行命令。但这会出错，因为 shell 历史、进程检查、CI 日志和错误输出都会给 bearer token 提供太多可能出现的位置。如果网关通常会自己注入凭据，这种做法还绕过了你真正需要测试的行为。

运行器应通过代理使用的同一 MCP 路径调用网关。不要为检查另造一个后门 HTTP 客户端。将具体传输方式的调用封装在本地适配器后面，因为工具名称和请求格式可能变化。适配器接收一个逻辑操作，请求 MCP 连接的网关执行它，然后只输出标准化结果。

```json
{
  "action": "http_canary",
  "target": "canary-api",
  "method": "POST",
  "path": "/agent-gateway-canary",
  "expected_status": 200,
  "expected_check": "agent-gateway-http"
}
```

适配器必须在失败时进行脱敏。它可以报告 `http_status=401`、`transport_error=timeout` 或 `response_schema=invalid`，但不能打印传出标头、请求正文、带查询参数的完整 URL 或原始响应，除非你已经确认这些数据不会泄露敏感信息。

## HTTP 成功必须证明预期的操作

单独一个 `200` 证据很弱。检查必须验证服务响应、请求方法和目标身份，避免重定向、代理页面或过期测试夹具造成误通过。

让金丝雀响应能够识别这次测试，但不要识别凭据。比较少量精确字段：

```sh
status=200
check=agent-gateway-http
result=ok
request_id=7d7a0f3c
```

检查器应接受语法有效的 `request_id`，然后将其与运行 ID 一起保存。对于字段缺失、正文声称成功但状态码不是 2xx，以及内容类型不符合预期的情况，都应判定失败。强制门户、企业代理错误页面或错误的 DNS 记录都可能返回有效的 HTTP 响应。这只是传输成功，不是操作成功。

curl 文档也指出了相关问题：如果没有使用 `--fail` 或 `--fail-with-body`，curl 不会将 404 或 401 这样的 HTTP 状态视为命令失败。对于通用传输客户端来说，这种行为是合理的，但它会让只看 curl 退出状态来编写监控的人掉入陷阱。如果适配器内部使用 curl，应同时捕获进程结果和 HTTP 状态，再根据明确的契约判断成功。

除非重定向属于端点设计的一部分，否则不要让检查自动跟随重定向。重定向可能将金丝雀调用送到返回 `200` 的登录页面，也可能送到你本来不想联系的其他主机。在配置中固定 HTTPS 来源，通过正常的客户端栈验证证书。只有在不会暴露敏感网络细节的前提下，才记录最终对端身份。

超时需要单独标记。DNS 失败、TCP 拒绝、TLS 验证失败、网关拒绝、上游 `401`、上游 `500` 和响应不匹配，分别对应不同的责任方。如果运行器把它们全部称为 `http=failed`，每次事故开始后的前十分钟都会浪费在确认请求究竟停在哪里。

有用的失败记录可以这样写：

```json
{
  "run_id": "hc-2026-07-22T141501Z-8f29",
  "check": "http_canary",
  "outcome": "failed",
  "stage": "upstream_response",
  "http_status": 401,
  "request_id": null,
  "secret_material": "redacted"
}
```

`secret_material` 这一行只是提醒阅读记录的人，并不能证明脱敏真的成功。应通过测试证明脱敏有效：故意让金丝雀拒绝凭据，捕获运行器的标准输出和标准错误，然后在这些文件中搜索测试秘密。秘密不应出现。还要对格式错误的 JSON、超时、TLS 失败和代理侧工具错误重复测试。秘密最容易从错误路径中泄露。

## SSH 需要隔离目标，而不是登录 shell

SSH 健康检查应证明对专用账户的身份验证和命令执行，而服务器应拒绝运行任意命令。连接到普通管理主机虽然能够证明连接成功，却朝错误的方向提供了过多权限，让健康检查凭据拥有可用的 shell。

在受控测试主机上创建单独的账户，例如 `gateway-health`。在 `authorized_keys` 中配置强制命令，或使用等效的服务器端限制。强制命令应忽略原始命令，将时间戳和不透明的运行 ID 写入本地审计文件，然后返回固定响应。

概念性的 `authorized_keys` 条目如下：

```text
command="/usr/local/libexec/gateway-health",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAA... gateway-health
```

公钥属于网关的金丝雀身份，私钥保存在保险库中。这里的 `AAAA...` 文本有意不完整，因为你必须生成自己的密钥材料，不能把示例直接复制到生产文件中。

服务器命令应避免回显 `$SSH_ORIGINAL_COMMAND`、环境变量或认证细节。它可以返回固定格式：

```json
{"check":"agent-gateway-ssh","result":"ok","receipt":"c2b91a"}
```

如果需要运行 ID 进行关联，可以在提交的命令中传入不透明令牌，并让强制命令验证固定长度的十六进制字符等严格格式。绝不要接受任意字符串，再把它写入 shell 命令、文件名或日志行。更好的做法是让服务器生成收据，在调查时按时间窗口关联记录。

如果服务器允许交互式 shell、端口转发、分配 TTY，或执行强制命令之外的命令，检查都应失败。这些属于配置回归。它们改变了金丝雀凭据的暴露范围，因此严重程度应不同于普通网络超时。

不要复用部署所用的 SSH 身份。复用身份起初更容易设置，最终却会把一次监控故障变成广泛的访问问题。健康凭据只应有一个用途、一个目标账户，并且只能用于生成收据。

Sallyport 通过内置的无状态 `sp-ssh` 辅助程序发送 SSH。你的测试因此应经过标准网关操作路径，而不是用 OpenSSH 调用本地私钥。否则你检查到的只是主机和账户，跳过了真正需要证明的凭据边界。

## 审计验证是另一项独立断言

金丝雀操作成功和审计链有效，是两项不同的声明。两者都要验证。

活动记录告诉你网关记录了一次具体操作。会话记录告诉你代理运行的情况，并提供撤销该运行的方法。两者都不能替代对远程结果的检查，因为请求可能在上游失败之前就已经被记录。反过来，远程系统可能收到请求，而本地日志路径恰好在此时失败。你需要的是关联，而不是想当然地认为两者总会一致。

对于每次成功的 HTTP 或 SSH 操作，收集三项非秘密证据：

- 运行器生成的健康运行 ID；
- 金丝雀服务生成的不透明收据或请求 ID；
- 以 UTC 记录的本地操作时间戳。

然后检查活动日志中可以安全保留的操作元数据，例如通道、操作结果、目标别名和时间。不要期待日志中出现明文凭据，它不应该出现。也不要要求日志重现远程响应正文。收据属于远程金丝雀系统，不属于网关的秘密存储。

Sallyport 从一个只写不可读、采用哈希链的加密审计日志中生成 Sessions 和 Activity 日志。将其离线完整性检查作为套件中的独立部分运行：

```sh
sp audit verify
```

成功的命令应记录为 `audit=verified`；非零结果在证明并非如此之前都应视为完整性事件。这项验证不需要保险库密钥，因此即使保险库锁定，也适合无人值守的本地检查。

不要把审计验证简化为“日志文件存在”。文件存在几乎不能说明什么。文件截断、记录被替换、链断裂，或写入程序启动后就停止，都可能与 Finder 中看起来正常的路径同时存在。

还有一个容易犯的错误：只检查审计链是否有效。即使链有效，也可能缺少你期待的事件，因为网关从未尝试执行该操作。每次金丝雀操作后，都应将链验证与事件存在性断言结合起来。为本地投影设置合理的到达窗口，并将延迟与事件缺失分开报告。日志更新延迟可能需要调查，但它与写入失败不是一回事。

## 让运行器位于所检查的信任边界之外

健康检查运行器应该编排操作并判断证据。它不应持有凭据、解析保险库文件或读取包含秘密的应用状态。

一种实用布局包含四个组件：

1. 本地调度器在专用 macOS 账户下启动小型运行器。
2. 运行器检查应用是否存在，并调用本地 MCP 适配器。
3. 适配器通过网关请求命名的金丝雀操作，并返回经过脱敏的结构化结果。
4. 运行器验证审计完整性，并将一份简短报告写入受保护的本地目录。

专用账户不应有权限访问保险库数据文件。Apple 将非沙盒 macOS 应用支持数据放在当前用户的 `~/Library/Application Support` 目录下，但监控程序不应假设自己可以或应该检查这些文件。检查需要验证行为，而不是复制保险库内容。

让配置保持声明式且不含秘密。下面的示例为运行器提供了验证结果所需的信息，但不会暴露凭据：

```yaml
checks:
  http_canary:
    target_alias: canary-api
    expected_status: 200
    expected_check: agent-gateway-http
    timeout_seconds: 10
  ssh_canary:
    target_alias: canary-ssh
    expected_check: agent-gateway-ssh
    timeout_seconds: 10
  audit:
    command: sp audit verify
    timeout_seconds: 15
```

目标别名很重要。别名比完整 URL 或主机名更不敏感，还会迫使你在网关配置中明确选择目标。如果操作人员修改了别名，操作检查应表明在悄悄开始测试另一个位置之前，需要进行配置审查。

不要给运行器审批新代理进程的权限。会话授权默认开启是有原因的。新代理进程的首次调用会在审批卡中显示其代码签名机构，审批只对本次运行有效。如果需要在人员批准后安排定期检查，就安排一个经过审查的长期健康客户端进程。进程退出后，应预期下一次运行会再次请求审批。

这比隐藏一个始终获批的自动化账户稍微不方便，却能避免新可执行文件仅仅因为复制了健康检查的命令行，就获得操作权限。

## 失败检查需要给出有用的诊断

大多数网关健康检查失败都源于普通的配置漂移。危险之处在于，人们常常通过削弱控制措施来回应，而不是定位发生故障的边界。

考虑一个看起来真实的过程：应用正在运行，保险库已解锁，HTTP 金丝雀返回 `401`。第一反应往往是把金丝雀令牌粘贴到 shell 中，直接调用端点。不要这样做。这只能证明令牌有效，却绕过了保险库，并创造了新的秘密泄露路径。

应按以下顺序处理证据：

1. 确认本地 MCP 适配器确实到达网关，并收到操作结果，而不是在提交之前就超时。
2. 确认目标别名仍然指向预期的已保存凭据和端点配置。
3. 检查金丝雀服务日志中的不透明请求 ID、方法、路由和拒绝原因。不要记录收到的授权值。
4. 检查活动日志中是否有匹配的操作尝试，并使用 `sp audit verify` 验证审计链。
5. 如果配置审查显示预期值发生变化，或秘密可能已经暴露，就轮换金丝雀凭据。

这个顺序可以区分四种看起来完全相同的故障：目标映射错误、秘密被撤销或轮换、网关注入失败，以及上游服务配置发生变化。它还可以避免一种常见的自我造成的事故：操作人员在终端中“测试”秘密，随后发现秘密被复制到 shell 历史、滚动缓冲区或支持包中。

根据失效的控制措施划分严重程度。应用不可用是可用性问题。锁定的保险库拒绝操作属于信息状态，除非原本预期会有一次已批准的定期检查。保险库报告锁定但金丝雀操作成功，是安全事件。审计链损坏同样是安全事件，即使两个金丝雀目标仍然返回成功。

不要把这些类别隐藏在一个红色或绿色徽章后面。看到 `vault=locked`、`audit=verified` 和 `http=skipped` 的操作人员知道该做什么；看到 `health=warning` 的人只能开始猜测。

## 最小的实用计划应分成两条通道

频繁运行非秘密检查，然后谨慎、有意地运行需要凭据的金丝雀操作。这两条通道可以减少噪声，并提供更好的证据。

无人值守通道可以在预期 Mac 处于唤醒状态时运行。它检查应用是否存在、保险库锁定时是否拒绝操作、MCP 路径能否返回分类结果，以及 `sp audit verify` 是否通过。这些检查都不应要求提取凭据或解锁保险库。

凭据通道在人员解锁保险库，并在需要时批准健康客户端会话后运行。它执行一次 HTTP 金丝雀操作和一次 SSH 金丝雀操作，确认远程收据，检查对应的本地活动记录，并再次验证审计链。保持适度的运行频率。每次凭据操作都会生成审计事件和远程服务事件，因此每分钟检查一次只会制造噪声，并不会带来多少额外信心。

在凭据配置、目标别名、代理工具、macOS 权限、网络控制或网关应用发生变更后运行凭据通道。这种时机能够捕捉最可能破坏操作执行的变化，也能为审查人员提供清晰的前后记录。

标准很简单：健康检查套件必须证明你打算依赖的控制措施。如果它只能证明应用图标还在菜单栏中，就把它称为可用性探针，到此为止。不要把它称为自主代理能够安全执行操作的证明。
