# VoiceOver approval cards for agent actions

An approval card is a security decision, not a decorative interruption. If a VoiceOver user cannot identify the requesting process, the real destination, the action, and the scope before the buttons, the card asks for consent without supplying the facts needed to give it.

I have seen teams call a prompt "accessible" because VoiceOver reached an Approve button. That is a low bar and a dangerous one. A person can activate a control they do not understand. For agent actions, that failure has teeth: one approval may send a request under a stored credential or open an SSH session to a machine the agent itself cannot be trusted to describe.

## The card must state the decision before it offers controls

The first useful utterance must identify the decision, and every fact needed to make it must come before Approve and Deny. A good card gives VoiceOver a short summary first, then exposes the details in a stable order. It does not make the person hunt through a visual arrangement that only makes sense when they can see its spacing, badges, or color.

Use this order:

1. Why the card appeared: a new agent process requests authorization, or a protected credential requires confirmation for this call.
2. Who asks: the process name, executable path when useful, and its code-signing authority.
3. Where it wants to act: protocol and concrete destination.
4. What it wants to do: the HTTP method and request shape, or the SSH command and target.
5. What the decision grants: this call, this running process until it exits, or a single protected credential use.

That sequence matches the decision a careful operator makes. Provenance answers who is asking. Destination answers where the consequence lands. Action answers what changes. Scope answers how long the permission survives. Putting the buttons before that chain turns the card into a reflex test.

Keep the opening summary short enough to hear in one pass. For example:

```
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.
```

The expanded detail should follow immediately after the summary, before the controls. Do not hide it behind an unlabeled disclosure triangle or make the user enter a separate view to learn the host. A compact card can collapse low-consequence fields, but it cannot collapse the destination or the operation itself.

W3C's WAI-ARIA Authoring Practices describes a modal dialog as a contained interaction with a label, appropriate focus handling, and an explicit way to dismiss it. That advice holds even when the application uses native macOS controls rather than web ARIA. A modal prompt that announces only its title has met the narrowest part of the pattern while failing the actual decision.

## Process identity is evidence, not a permission slip

A process signature tells a user who built or signed the code that asks for access. It does not tell them whether the request is appropriate. Treat it as evidence that belongs at the start of the card, then make the rest of the request impossible to confuse with that identity.

The identity field needs a stable, spoken form. Lead with the signing authority because that gives an operator a meaningful anchor when several helper processes have similar file names. Follow with a process name. Include a path only when it resolves ambiguity, such as a local development build and an installed build sharing a name. A long path should live in the details region, where VoiceOver can read it line by line.

Avoid vague language such as "trusted agent" or a green "verified" badge. Those phrases compress several separate claims into a reassuring visual cue. A process can have a valid signature and still carry a malformed prompt, a compromised instruction source, or a request aimed at the wrong account. Users need provenance, not a verdict the interface made for them.

A request summary can say:

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

Expose that content as one labeled group whose children follow the same order. Do not repeat the signing authority in a subtitle, a badge, and an accessibility hint. Repetition makes VoiceOver output tiring, and the user starts skipping precisely the phrase you wanted them to notice.

There is another trap here: a process name is mutable. An attacker can name an executable "release-agent" and hope the human reads no further. The signing authority is harder to imitate, but it still cannot authorize the destination. The card should never phrase the identity as if it guarantees the request. Say "requested by" or "signed by," then describe the action in separate sentences.

For unsigned local development processes, state that condition plainly. Do not substitute a euphemism such as "personal build." If the product permits the request, the user can decide whether an unsigned process deserves a session grant. Hiding the condition saves no one from friction; it postpones the friction until after a mistake.

## A destination needs a name users can inspect

A card must speak the destination as a specific endpoint or host, because categories and icons cannot tell a user where credentials will travel. HTTP and SSH need different summaries, but both need a concrete remote side before any approval control.

For HTTP, read protocol, host, method, and a path that retains the consequence. `POST api.example.net/v1/releases` says far more than "Release service." If a port differs from the usual one, include it. If the request follows a redirect, do not describe only the original host and leave the final host for a later surprise. Resolve the destination before asking for consent, or ask again when resolution changes the remote host.

For SSH, read the host or alias, the user when it affects privilege, and the command. `ops@build-03.example.net: systemctl restart worker` supplies a decision a person can make. "SSH command" does not. If an alias expands through a configuration file, provide its resolved host in the primary summary and retain the alias in details. People often recognize the nickname first and the actual machine second; they deserve both.

DNS deserves the same restraint. A hostname is the right human-facing identity. An IP address can supplement it, especially in a suspicious or unusual request, but it should not replace the hostname and turn every approval into a memory contest. If a system cannot resolve a target, the prompt should say that it cannot resolve it. It should not invent confidence with an old cached label.

Do not rely on a red production label, a warning triangle, or a left-hand column to distinguish destinations. Those devices can support sighted scanning, but they cannot carry the meaning. Put the environment name in text if you have a reliable source for it, then speak the host as well. Environment labels drift. Hosts tell the user where the connection goes.

A concise accessible representation looks like this:

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

The credential label belongs after destination and action, not before them. The user should know what the credential will do before deciding whether that credential deserves use. Never speak the token value, even in an accessibility-only label. Assistive technology is part of the user interface, not a private side channel for secrets.

## Redaction must preserve the reason to approve or deny

Redaction should remove secret material while retaining the parts of a request that change the decision. Teams often get this backwards. They hide every parameter, then claim they protected privacy, while the approval card becomes too vague to prevent a bad action.

Always suppress bearer tokens, passwords, private key material, cookies, authorization headers, signed URLs, and full values from fields that may contain personal or financial data. Masking only the middle of a token is usually theatre. A partial secret can still help an attacker correlate or reconstruct a credential, and it tempts designers to expose too much in logs as well.

Keep structure. For an HTTP request, show the method, host, path, content type, body size when it gives useful scale, and the names of sensitive fields. For a command, show the executable, arguments, target files or services, and an explicit marker for redacted arguments. A user deciding whether to send an invoice should hear that the request contains `recipient`, `amount`, and `currency`; they do not need the recipient's address or the amount itself if policy treats those as sensitive.

Compare these two spoken summaries:

```
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.
```

The second version does not reveal the secret values, but it tells the user that the agent is attempting a payout rather than a harmless status check. That distinction is the purpose of a consent screen.

Do not create a "show secret" button inside an approval prompt. A user may need more context, but the answer is a safer representation, not a casual escape hatch from vault boundaries. If an operator truly needs to inspect a protected value, send them to a separate, deliberately authenticated workflow whose access rules are clear. The approval card should remain a decision surface, not a secret browser.

Redaction must also survive copy, accessibility output, and audit projection. If the visible label says "token redacted" but the element's accessibility value contains the original header, VoiceOver has become an exfiltration path. Test every representation of the field: visible text, accessibility label, accessibility value, help text, copy-to-clipboard behavior, and journal entry.

## The dialog's accessibility tree should mirror the decision

VoiceOver follows accessibility semantics, not the designer's eye path. Build the accessibility tree in the same order as the decision, then verify it with the screen reader instead of assuming the visual stack determines traversal.

Use a real dialog or alert container with a concise accessible title such as "Approval required." Move focus into the dialog when it opens. The initial focus should land on the decision summary or the first identity field, not on Approve. If a system moves focus directly to the default button, it creates a fast path to an uninformed click and forces the user to reverse through the dialog to understand it.

Native macOS controls already supply much of the behavior that custom views have to recreate. Keep the surface native where possible: static text for facts, a standard disclosure control for optional detail, a checkbox only when the user can change a clearly explained option, and ordinary buttons with distinct labels. An accessibility inspector can then expose a tree that has a chance of matching the interface.

The intended order should be readable as a simple outline:

```
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
```

This is an acceptance artifact, not a suggestion to stuff every sentence into one accessibility label. Groups give the user landmarks. Static text within each group gives them a place to return to. The order must remain stable whether the details control is collapsed or expanded.

Give buttons full names. "Approve" is weak when more than one approval prompt can interrupt work. "Approve this process" and "Deny request" remain clear when VoiceOver reads them in isolation. If the card offers a single-call approval and a session approval, say that difference in the button text or the immediately preceding accessible description. Two buttons labeled "Allow" with different visual captions are a design error.

Apple's accessibility guidance for macOS asks apps to provide labels, roles, values, and descriptions that convey a control's purpose. The word "purpose" matters here. A field called "Target" may have a label, role, and value, yet still force the user to infer whether it names a host, account, file, or command. Prefer labels that carry the missing noun: "SSH host," "HTTP request," "Credential used," and "Approval duration."

Do not announce a wall of text when the dialog opens. VoiceOver users need a clear starting point, not a paragraph that prevents them from interrupting and inspecting an item. Put the short summary in the title region or directly after it, then let normal reading commands traverse the facts. Announce changes that matter, such as a denied request or an approval that expires with process exit, once and in plain language.

## Color and layout cannot carry the warning

An approval card passes the basic accessibility test only when a person can make the same decision through spoken order and keyboard controls that they can make by looking at it. That test catches more than color contrast failures. It catches hidden assumptions about columns, icon placement, default focus, and visual grouping.

Run this exercise with a tester who uses VoiceOver regularly, not only a developer who knows where every element sits. Start a new agent process. Turn on VoiceOver. Keep the pointer away from the controls. Ask the tester to answer four questions before they approve or deny: who requested this, where will it act, what will it do, and how long will approval last.

Then ask them to complete this sequence:

1. Find the incoming approval without help from a sighted person.
2. Read process identity, destination, action, and scope in order.
3. Expand details and identify which values the interface redacted.
4. Deny the request, then trigger a fresh request and approve it.
5. Confirm what happened after each choice and where to find the recorded action.

Record the actual spoken sequence, including repeated labels, skipped fields, and focus jumps. A visual walkthrough can miss a bad accessibility order because the reviewer sees the intended arrangement and unconsciously fills in gaps. Speech recordings and written transcripts make the defect hard to argue away.

Test these cases separately:

- A signed agent process requests a first session approval.
- An unsigned local process makes the same request.
- A protected credential requires confirmation for a call after session approval already exists.
- A request contains redacted headers or body fields.
- Two requests arrive close together, and the first card disappears after denial.

The last case exposes a common failure. Developers use one reusable view and update its labels in place. A sighted person notices the card change. VoiceOver may retain focus on a button while the process, host, and action behind it change. Treat a materially different request as a new dialog with a fresh summary and a fresh focus event. Never let an existing Approve button silently acquire a different meaning.

Also test reduced-motion settings and large text. These settings do not directly change VoiceOver semantics, but they often trigger alternate layout code. If a compact layout moves scope text below the buttons or removes a label to save room, users lose information precisely when their setup needs the interface to be more forgiving.

## Session approval needs a visible boundary

A session grant is safe only when the person can tell exactly which running process received it and when that grant ends. "Remember my choice" is the wrong language for an agent because it sounds like a durable preference while the actual decision should bind to one process lifetime.

State the boundary in the card: "This approval allows calls from this process until it exits." Place it after the action description, where it answers the natural follow-up question. If the process exits and a replacement launches, show the card again. A new process may have the same display name, but it has not earned the old decision.

Sallyport's per-session authorization does this by default, and its approval card leads with the process's code-signing authority. That is a useful starting point, but the card still has to make the destination, action, and duration just as legible to a person using VoiceOver.

Per-call approval has a different job. Use it for individual credentials where each use has independent consequence, such as a payment action, a production deletion, or an SSH command that changes service state. The confirmation must name that fact plainly: "This credential requires approval for every use." Do not bury the condition in a settings screen and then surprise the user with an extra prompt.

Avoid the popular recommendation to put every decision behind one broad "allow this agent" switch. People like it because it keeps autonomous work moving. It also collapses identity, target, and duration into a promise that the agent will behave. That promise has no useful security meaning once the agent follows a different instruction or reaches a different service. A limited session grant plus selective per-call confirmation asks the human to judge an actual request instead.

The vault gate belongs outside this choice. When the vault is locked, the system should deny actions outright rather than presenting an approval card that suggests a click can override the lock. A Touch ID action can unlock or confirm where the product supports it, but the spoken interface must say which event occurred. "Approved" and "vault unlocked" are different state changes and should never share one ambiguous announcement.

## Audit records must let users reconstruct a decision later

An approval flow is incomplete if the operator cannot later tell which request they approved, what action followed, and whether the record changed. The immediate card handles consent. The audit trail handles the argument that arrives afterward, often when nobody remembers what flashed on screen.

Keep the session event and the individual action event distinct. A session record should identify the agent run, its authorization decision, and revocation or exit. An activity record should identify each HTTP or SSH call, its destination, operation, result, and the same redaction rules used in the approval view. Mixing both into one vague "agent allowed" entry loses the connection between consent and consequence.

For a hash-chained encrypted journal, verification should be a separate, inspectable act. The expected shape is simple:

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

The exact record count will vary, but the command should tell the operator whether verification succeeded or failed and should fail loudly if it finds a broken chain. Verification over ciphertext matters because a reviewer can check log integrity without first unlocking the vault merely to read audit evidence. It does not prove that an approved action was wise. It proves that the retained sequence has not quietly changed.

Build the approval card and journals from the same event vocabulary. If the card says "HTTPS request to billing.example.net" while the journal calls it "remote operation 12," the user cannot match the decision to its record. Reuse the destination, action, process identity, scope, and redaction categories, even if each surface presents them at a different level of detail.

Give VoiceOver users an equally direct path to the record. After approval or denial, announce the result and provide a labeled route to the relevant session or activity entry. Do not make a colored status dot carry the proof that an action happened. A user who hears "Request denied. Activity record available" can check the outcome later without reconstructing the interface from memory.

## Accessibility regressions belong in the security test suite

Treat the spoken approval sequence as a security contract and test it whenever the card, action channel, or identity model changes. A snapshot that confirms a title and two buttons exists will not catch a destination that moved below the controls or a redacted field that leaks through an accessibility value.

Keep a small fixture set with intentionally awkward requests: a long signing authority, an unsigned process, an internationalized hostname, a nonstandard port, a redirect to a different host, an SSH alias, and a body with sensitive field names. Each fixture should produce an expected accessibility outline and an expected journal representation. When a designer changes visual layout, compare those outputs before declaring the work done.

A practical review asks four blunt questions. Can a VoiceOver user identify the executable and signer? Can they identify the final remote destination? Can they tell the difference between this call and this process lifetime? Can they approve or deny while hearing a clear result afterward? If any answer depends on seeing color, spacing, or an icon, the prompt still has a security defect.

Do the first test on the most consequential credential and the least cooperative request your product permits. A friendly GET to a familiar host makes every approval card look good. The request that reveals the design is an unfamiliar signed process asking to modify a real service with parts of its body redacted. That is where the wording, order, and focus behavior either let a person exercise control or reduce them to clicking through a gate.
