# 面向代理操作的 VoiceOver 审批卡片

审批卡片是一次安全决策，不是装饰性的打断。如果 VoiceOver 用户在按钮出现之前，无法确认发起请求的进程、实际目标、操作和范围，那么这张卡片要求用户同意，却没有提供做出决定所需的事实。

我见过一些团队因为 VoiceOver 能读到「批准」按钮，就把提示称为「无障碍」。这个标准太低，也很危险。一个人可能会激活自己并不了解的控件。对于代理操作，这种失败会带来实际后果：一次批准可能让请求使用已保存的凭据发送出去，也可能打开 SSH 会话，连接到一台连代理自己都无法可靠描述的机器。

## 卡片必须先说明决定，再提供控件

第一句有用的播报必须说明这是什么决定，做出决定所需的每项事实都必须出现在「批准」和「拒绝」之前。一张好的卡片会先让 VoiceOver 读出简短摘要，再按稳定顺序提供详情。它不会让用户在视觉布局中到处寻找信息，因为那种布局只有在用户能看见间距、徽章或颜色时才有意义。

请按以下顺序展示：

1. 卡片为什么出现：新的代理进程请求授权，或受保护的凭据需要为这次调用确认。
2. 谁在请求：进程名称、必要时提供可执行文件路径，以及代码签名机构。
3. 它要在哪里操作：协议和具体目标。
4. 它要做什么：HTTP 方法和请求结构，或 SSH 命令及其目标。
5. 这项决定授予什么权限：仅本次调用、当前运行的进程直到退出，或单次使用受保护的凭据。

这个顺序符合谨慎操作人员做决定时的思路。来源信息回答谁在请求。目标回答后果会落在哪里。操作回答会发生什么变化。范围回答权限会持续多久。如果在这条信息链之前放置按钮，卡片就会变成一次凭反射操作的测试。

开头的摘要应短到可以一次听完。例如：

```
Authorization requested. Signed process: Acme Development, agent-helper.
HTTPS request to api.example.net. POST /v1/releases.
This approval allows this process until it exits.
```

展开的详情应紧接在摘要之后，并位于控件之前。不要将它隐藏在没有标签的展开三角形后面，也不要让用户进入另一个视图才能知道主机。紧凑的卡片可以折叠后果较小的字段，但不能折叠目标本身或具体操作。

W3C 的 WAI-ARIA Authoring Practices 将模态对话框描述为一种封闭式交互，它应当有标签、适当的焦点处理方式，以及明确的关闭方法。即使应用使用的是原生 macOS 控件而不是网页 ARIA，这条建议仍然适用。只播报标题的模态提示，最多满足了这一模式中最狭窄的部分，却没有真正支持用户做决定。

## 进程身份是证据，不是许可凭证

进程签名会告诉用户请求访问权限的代码由谁构建或签署，但不会告诉用户这个请求是否合适。应将它作为卡片开头的证据，然后让请求的其余部分与这个身份清楚区分，避免混淆。

身份字段需要一种稳定、适合播报的形式。先说签名机构，因为当多个 helper 进程使用相似的文件名时，这能给操作人员一个有意义的判断依据。接着说进程名称。只有在路径能消除歧义时才加入路径，例如本地开发构建和已安装构建使用同一个名称。较长的路径应放在详情区域，让 VoiceOver 逐行读出。

避免使用「受信任的代理」或绿色「已验证」徽章这类含糊的说法。这些词会把多个独立的判断压缩成一个令人安心的视觉提示。进程可以拥有有效签名，同时仍然携带格式错误的提示、遭到入侵的指令来源，或面向错误账户的请求。用户需要的是来源信息，而不是由界面替他们做出的结论。

请求摘要可以这样写：

```
Requesting process
Signing authority: Example Software LLC
Process: deploy-helper
Details: /Applications/Deploy Helper.app/Contents/MacOS/deploy-helper
```

将这些内容作为一个带标签的组展示，组内子项遵循相同顺序。不要在副标题、徽章和无障碍提示中重复签名机构。重复内容会让 VoiceOver 的输出变得疲劳，用户最终会开始跳过你最希望他们注意的那句话。

这里还有一个陷阱：进程名称可以更改。攻击者可以把可执行文件命名为「release-agent」，希望人们不再继续阅读。签名机构更难伪造，但它仍然不能为目标背书。卡片绝不能把身份说得像是请求的保证。可以说「请求者是」或「签名者是」，然后用单独的句子描述操作。

对于未签名的本地开发进程，应明确说明这一点。不要用「个人构建」之类的委婉说法替代。如果产品允许请求继续，用户可以自行判断一个未签名进程是否值得获得会话授权。隐瞒这一条件并不能消除麻烦，只会把麻烦推迟到错误发生之后。

## 目标需要一个用户可以检查的名称

卡片必须播报具体的端点或主机作为目标，因为类别和图标无法告诉用户凭据将被发送到哪里。HTTP 和 SSH 需要不同的摘要方式，但在任何批准控件出现前，两者都需要说明明确的远端目标。

对于 HTTP，应读出协议、主机、方法，以及能够保留后果信息的路径。`POST api.example.net/v1/releases` 远比「发布服务」提供的信息多。如果端口不是常用端口，也要包含端口。如果请求发生重定向，不要只描述原始主机，把最终主机留给用户稍后意外发现。应在请求同意前解析目标，或者在解析结果改变远端主机时再次请求同意。

对于 SSH，应读出主机或别名、会影响权限的用户名，以及命令。`ops@build-03.example.net: systemctl restart worker` 能提供足够的信息，让人做出决定。「SSH 命令」则不能。如果别名会通过配置文件展开，应在主要摘要中提供解析后的主机，并在详情中保留别名。人们往往先认出昵称，之后才认出实际机器，两者都应提供。

DNS 也需要同样克制。主机名是面向用户的合适身份。在可疑或异常请求中，可以补充 IP 地址，但不应以 IP 地址替代主机名，把每次审批变成记忆力测试。如果系统无法解析目标，提示应明确说明无法解析，而不是用旧的缓存标签制造虚假的确定感。

不要依赖红色的生产环境标签、警告三角形或左侧栏来区分目标。这些元素可以帮助能看见屏幕的用户快速浏览，但不能承载实际含义。如果你有可靠来源能确认环境名称，可以将它放入文本中，同时也要播报主机。环境标签可能发生变化，主机会告诉用户连接将前往哪里。

简洁的无障碍表示可以这样写：

```
Destination
Protocol: HTTPS
Host: billing.example.net
Request: PATCH /v2/subscriptions/4821
Credential: billing-service token
```

凭据标签应放在目标和操作之后，而不是之前。用户应先知道凭据将做什么，再决定是否允许使用该凭据。绝不要播报令牌值，即使它出现在只供无障碍使用的标签中。辅助技术也是用户界面的一部分，不是存放秘密的私人旁路。

## 编辑必须保留批准或拒绝的理由

编辑应移除秘密材料，同时保留会改变决定的请求部分。团队经常把这件事做反。他们隐藏所有参数，然后声称保护了隐私，但审批卡片却变得过于含糊，无法阻止错误操作。

始终隐藏 bearer token、密码、私钥材料、Cookie、授权标头、签名 URL，以及可能包含个人或财务数据的字段的完整值。只遮住令牌中间部分通常只是做做样子。部分秘密仍可能帮助攻击者关联或重建凭据，也会诱使设计人员在日志中暴露过多内容。

保留结构。对于 HTTP 请求，应显示方法、主机、路径、内容类型，以及能提供有用规模信息的正文大小，同时显示敏感字段的名称。对于命令，应显示可执行文件、参数、目标文件或服务，并明确标出哪些参数已被编辑。用户在决定是否发送发票时，应能听到请求包含 `recipient`、`amount` 和 `currency`，但如果策略将收件人地址或金额视为敏感信息，他们不需要听到具体地址或金额。

比较下面两种播报摘要：

```
POST request with protected details.
```

```
POST api.example.net/v1/payouts.
JSON fields: destination account redacted, amount redacted, currency visible.
This call uses the finance credential.
```

第二个版本没有泄露秘密值，但告诉用户代理正在尝试发起一笔付款，而不是执行无害的状态检查。这种区别正是同意界面的作用。

不要在审批提示中创建「显示秘密」按钮。用户可能需要更多上下文，但答案应是更安全的呈现方式，而不是随意绕过保险库边界的出口。如果操作人员确实需要检查受保护的值，应将其引导至另一个经过明确身份验证的流程，并清楚说明访问规则。审批卡片应当是做决定的界面，而不是浏览秘密的工具。

编辑也必须在复制、无障碍输出和审计投影中保持有效。如果可见标签写着「令牌已编辑」，但元素的无障碍值包含原始标头，VoiceOver 就会变成数据外泄通道。请测试该字段的每一种表现形式：可见文本、无障碍标签、无障碍值、帮助文本、复制到剪贴板的行为，以及日志条目。

## 对话框的无障碍树应反映决策过程

VoiceOver 遵循无障碍语义，而不是设计师眼中的视觉路径。应按相同的决策顺序构建无障碍树，然后用屏幕阅读器验证，不要假定视觉堆叠会决定遍历顺序。

使用真正的对话框或警报容器，并提供简洁的无障碍标题，例如「需要批准」。对话框打开时，将焦点移入对话框。初始焦点应落在决策摘要或第一个身份字段上，而不是「批准」按钮。如果系统直接将焦点移到默认按钮，就会为未经了解的点击创造捷径，并迫使用户反向遍历对话框才能理解内容。

原生 macOS 控件已经提供了许多自定义视图必须重新实现的行为。尽可能保留原生界面：用静态文本展示事实，用标准展开控件展示可选详情，只有在用户能够更改某个解释清楚的选项时才使用复选框，并使用名称明确的普通按钮。这样，无障碍检查器才有机会展示出与界面相符的树结构。

预期顺序应能读成一份简单的大纲：

```
Dialog: Approval required
  Summary: New agent process requests authorization
  Group: Requesting process
    Static text: Signing authority, Example Software LLC
    Static text: Process, deploy-helper
  Group: Destination
    Static text: HTTPS, api.example.net
  Group: Requested action
    Static text: POST /v1/releases
  Group: Scope
    Static text: Approval lasts until this process exits
  Disclosure button: More redacted details, collapsed
  Button: Deny request
  Button: Approve this process
```

这是一份验收依据，不是建议把每句话都塞进一个无障碍标签。分组能为用户提供定位点。每个组中的静态文本则提供返回具体信息的位置。无论详情控件折叠还是展开，顺序都必须保持稳定。

为按钮提供完整名称。当多个审批提示可能打断工作时，「批准」这个名称太弱。「批准此进程」和「拒绝请求」即使被 VoiceOver 单独读出，也仍然清楚。如果卡片提供单次调用批准和会话批准，应在按钮文本或紧接在前的无障碍描述中说明两者的区别。两个视觉标题不同、但都叫「允许」的按钮，是设计错误。

Apple 针对 macOS 的无障碍指南要求应用提供能传达控件用途的标签、角色、值和描述。这里的关键是「用途」。一个名为「目标」的字段可能拥有标签、角色和值，却仍然迫使用户猜测它指的是主机、账户、文件还是命令。应优先使用包含缺失名词的标签，例如「SSH 主机」「HTTP 请求」「使用的凭据」和「审批时长」。

对话框打开时不要播报一整面文字。VoiceOver 用户需要一个清晰的起点，而不是一段让他们无法暂停并检查某项内容的长文。将简短摘要放在标题区域或标题之后，然后让普通的阅读命令遍历各项事实。对于被拒绝的请求，或随进程退出而失效的批准等重要变化，应使用简单的语言播报一次。

## 颜色和布局不能承担警告的含义

只有当用户通过播报顺序和键盘控件做出的决定，与他们通过视觉观察做出的决定相同时，审批卡片才算通过基本的无障碍测试。这个测试能发现的不只是颜色对比度问题，也包括关于列布局、图标位置、默认焦点和视觉分组的隐藏假设。

请让经常使用 VoiceOver 的测试人员参与这项练习，不要只让熟悉每个元素位置的开发人员来测试。启动一个新的代理进程。打开 VoiceOver。让指针远离控件。在测试人员批准或拒绝之前，请他们回答四个问题：谁发起了请求、请求将在何处执行、它会做什么、批准会持续多久。

然后让他们完成以下流程：

1. 不借助能看见屏幕的人，找到传入的审批请求。
2. 按顺序读取进程身份、目标、操作和范围。
3. 展开详情，并确认界面编辑了哪些值。
4. 拒绝请求，然后触发新的请求并批准它。
5. 确认每次选择之后发生了什么，以及在哪里能找到记录的操作。

记录实际播报的顺序，包括重复的标签、被跳过的字段和焦点跳转。视觉演示可能会漏掉错误的无障碍顺序，因为审核人员看到了预期布局，会下意识地填补空缺。语音录音和书面转录能让问题变得难以辩解。

请分别测试以下情况：

- 已签名的代理进程请求首次会话批准。
- 未签名的本地进程提出相同请求。
- 在已经存在会话批准后，受保护的凭据仍需为某次调用确认。
- 请求包含已编辑的标头或正文域。
- 两个请求在短时间内到达，第一个卡片在拒绝后消失。

最后一种情况会暴露一个常见故障。开发人员使用一个可复用视图，并直接更新其中的标签。能看见屏幕的人会注意到卡片发生了变化。VoiceOver 可能仍然将焦点保留在某个按钮上，但按钮背后的进程、主机和操作已经改变。将实质不同的请求视为新的对话框，并触发新的摘要和新的焦点事件。绝不能让现有的「批准」按钮在没有提示的情况下获得不同含义。

还要测试减少动态效果和大号文本设置。这些设置不会直接改变 VoiceOver 语义，但经常会触发不同的布局代码。如果紧凑布局将范围文本移到按钮下方，或为了节省空间而删除标签，用户就会在最需要界面更加宽容时失去关键信息。

## 会话批准需要清晰的边界

只有当用户能够确切知道哪个正在运行的进程获得了授权，以及授权何时结束，会话授权才是安全的。「记住我的选择」不适合代理，因为它听起来像一种持久偏好，而实际决定应绑定到一个进程的生命周期。

请在卡片中明确边界：「此批准允许此进程发起调用，直到该进程退出。」将它放在操作描述之后，因为那里正好回答了用户自然产生的后续问题。如果进程退出后又启动了替代进程，应再次显示卡片。新进程可能拥有相同的显示名称，但它并没有获得旧决定的授权。

Sallyport 默认采用按会话授权的方式，并在审批卡片开头显示进程的代码签名机构。这是一个有用的起点，但卡片仍必须让 VoiceOver 用户同样清楚地听到目标、操作和持续时间。

逐次调用批准承担着不同的任务。对于每次使用都有独立后果的凭据，应逐次确认，例如支付操作、生产环境删除操作，或会改变服务状态的 SSH 命令。确认时必须明确说明：「此凭据每次使用都需要批准。」不要把这个条件藏在设置界面中，再用额外的提示让用户感到意外。

不要采纳把所有决定都放在一个宽泛的「允许此代理」开关后的常见建议。人们喜欢这种方式，因为它能让自主工作持续运行，但它也把身份、目标和持续时间压缩成一个关于代理会如何行动的承诺。一旦代理遵循了不同的指令或访问了不同的服务，这个承诺就不再具有有用的安全含义。有限的会话授权加上有选择的逐次调用确认，会让人判断一个实际请求。

保险库的门禁应独立于这个选择。当保险库锁定时，系统应直接拒绝操作，而不是显示一张暗示点击就能绕过锁定的审批卡片。如果产品支持，可以通过 Touch ID 解锁或确认，但语音界面必须说明发生的是哪一种事件。「已批准」和「保险库已解锁」是不同的状态变化，绝不能共用一个含糊的播报。

## 审计记录必须帮助用户事后还原决定

如果操作人员事后无法确认自己批准了哪个请求、随后执行了什么操作，以及记录是否发生变化，审批流程就并不完整。即时卡片负责征得同意，审计轨迹则负责应对之后出现的争议，而那时通常已经没人记得屏幕上当时闪过什么。

请将会话事件和单个操作事件分开记录。会话记录应标识代理运行、授权决定，以及撤销或退出。活动记录应标识每次 HTTP 或 SSH 调用、其目标、操作、结果，并遵循与审批视图相同的编辑规则。把这些内容混成一条含糊的「已允许代理」记录，会丢失同意与后果之间的联系。

对于哈希链式加密日志，验证应当是一个独立且可检查的操作。预期形式很简单：

```
$ sp audit verify
Verifying encrypted audit log...
Chain verified: 184 records
Result: valid
```

确切的记录数量会变化，但命令应告诉操作人员验证成功还是失败，并在发现链断裂时明确报错。在密文上进行验证很重要，因为审核人员可以检查日志完整性，无需先解锁保险库来读取审计证据。它不能证明获准的操作是明智的，只能证明保留下来的顺序没有被悄悄改动。

请使用同一套事件词汇构建审批卡片和日志。如果卡片称之为「发往 billing.example.net 的 HTTPS 请求」，而日志称之为「远程操作 12」，用户就无法将决定与记录对应起来。应复用目标、操作、进程身份、范围和编辑类别，即使每个界面呈现的详细程度不同。

也要为 VoiceOver 用户提供同样直接的记录入口。在批准或拒绝之后播报结果，并提供带标签的路径，通往相关的会话或活动记录。不要让彩色状态点承担证明操作已经发生的作用。用户听到「请求已拒绝。活动记录可用」后，就能在之后检查结果，无需凭记忆重新构建界面。

## 无障碍回归应属于安全测试套件

把审批流程的语音顺序当作安全契约，在卡片、操作通道或身份模型发生变化时都进行测试。确认标题和两个按钮存在的快照，无法发现目标被移到控件下方，也无法发现编辑字段通过无障碍值泄露。

保留一小组刻意设计得不理想的测试请求：很长的签名机构、未签名进程、国际化主机名、非标准端口、重定向到不同主机的请求、SSH 别名，以及包含敏感字段名的正文。每个测试样例都应生成预期的无障碍大纲和预期的日志表示。设计人员更改视觉布局时，应先比较这些输出，再宣布工作完成。

实际评审可以直截了当地问四个问题。VoiceOver 用户能否识别可执行文件和签名者？能否识别最终的远程目标？能否分清这次调用与这个进程的生命周期？能否在听到清晰结果的情况下批准或拒绝？如果任何一个答案依赖颜色、间距或图标，提示仍然存在安全缺陷。

首先测试产品允许的最重要凭据，以及最难处理的请求。向熟悉的主机发起友好的 GET 请求，会让每张审批卡片看起来都很好。真正能检验设计的，是一个陌生的已签名进程请求修改真实服务，同时请求正文的一部分已被编辑。正是在这种情况下，措辞、顺序和焦点行为会决定用户能否真正行使控制权，还是只能一路点击通过一道门槛。
