阅读需 8 分钟

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

VoiceOver 审批卡片必须按固定且可测试的顺序展示进程身份、目标、操作和已编辑详情,让用户在充分了解信息后做出同意决定。

面向代理操作的 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,应读出主机或别名、会影响权限的用户名,以及命令。[email protected]: 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 请求,应显示方法、主机、路径、内容类型,以及能提供有用规模信息的正文大小,同时显示敏感字段的名称。对于命令,应显示可执行文件、参数、目标文件或服务,并明确标出哪些参数已被编辑。用户在决定是否发送发票时,应能听到请求包含 recipientamountcurrency,但如果策略将收件人地址或金额视为敏感信息,他们不需要听到具体地址或金额。

比较下面两种播报摘要:

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 就会变成数据外泄通道。请测试该字段的每一种表现形式:可见文本、无障碍标签、无障碍值、帮助文本、复制到剪贴板的行为,以及日志条目。

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

查看每次代理运行
在 Sessions 日志中查看代理运行的授权决定,并在需要时立即撤销某次运行。

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 语义,但经常会触发不同的布局代码。如果紧凑布局将范围文本移到按钮下方,或为了节省空间而删除标签,用户就会在最需要界面更加宽容时失去关键信息。

会话批准需要清晰的边界

确认敏感密钥的使用
将单个密钥设置为每次使用都需确认,只需点击一次或使用 Touch ID。

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

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

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

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

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

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

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

追踪每个已批准的操作
每次 HTTP 和 SSH 调用都会保存在 Activity 日志中,与会话授权分开记录。

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

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

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

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

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

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

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

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

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

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

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

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

常见问题

代理审批提示中,VoiceOver 用户首先应该听到什么?

先显示进程身份,然后依次显示目标、操作和已编辑的详情。VoiceOver 用户无需在装饰性容器、警告图标或重复的按钮标签之间移动,就应能听到这套顺序。

代码签名身份足以让人批准 AI 代理吗?

不能。签名身份只能告诉用户可执行文件由谁制作,有一定帮助,但无法说明请求将发往哪里或会执行什么操作。应将签名机构作为来源信息展示,再配合实际目标和请求的操作。

审批卡片应如何播报 HTTP 或 SSH 目标?

读出实际的主机名或 SSH 目标、协议,以及 HTTP 方法或命令类型。不要用「生产 API」这样的类别代替目标,除非用户能在批准前将其展开为具体端点。

审批卡片应该编辑哪些请求详情?

编辑凭据、bearer token、私钥材料、Cookie 值和敏感的请求正文域。保留主机、路径结构、方法、命令动词、仓库或账户范围,以及会改变请求后果的字段名。

如何在不看屏幕的情况下测试审批对话框?

打开 VoiceOver,并让测试人员无法接触指针来测试卡片。测试人员必须能够自行发现提示,按顺序读取其中的信息,查看已编辑的详情,批准或拒绝请求,并确认结果,整个过程不能依赖颜色、位置或图标。

一次授权应该覆盖整个代理会话吗?

会话授权应在代理进程退出时结束,界面也应明确说明这一点。跨不相关的进程复用授权,会把一次决定变成用户无法在点击当下判断的开放式授权。

什么时候代理操作应当每次都要求批准?

对于每次使用都值得由人重新判断的凭据或操作,应使用逐次调用确认。这种方式会刻意降低速度,因此适合后果会随每次请求明显变化的操作。

审批对话框最重要的可访问性语义有哪些?

对话框需要可访问名称、合理的初始焦点目标、清晰的遍历顺序、含义明确的控件名称,以及播报结果。原生控件可以处理很多交互机制,但无法修复含糊的标签或混乱的内容顺序。

为什么要离线验证加密审计日志?

它能让审核人员确认记录仍保留原始的顺序和内容,而无需打开存放秘密的保险库。这在审批发生争议后很有用,因为审计证据不应依赖任何人对实时界面的信任。

审批卡片能同时做到易访问和快速吗?

可以,只要审批界面按易读的顺序提供具体的身份、目标、操作、范围和持续时间。用户应能快速做出知情决定,而不是在代理等待时解读一大段密集的技术信息。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov