---
name: adom-desktop-permissions
description: >
  How the Adom Bridge human-approval permission gate works for an AI caller.
  Read this when a verb refuses with errorCode needs_approval or reason_required,
  when you need shell approval / bypass permissions on a machine, when you must
  call request_shell_approval to surface the human approval dialog, when a call
  demands a reason arg, or before an unattended run that touches shell_execute,
  run_script, write_file, or any other gated verb. Covers the human approval
  gate, the Approve 1 hr / 24 hr / Bypass Permissions buttons, why grants are
  human-only, and the macOS TCC permissions (Screen Recording, Accessibility)
  that sit underneath Bridge's own gate.
---

# Adom Bridge permissions: the human approval gate for AI callers

## The trust model in one paragraph

Bridge binds `127.0.0.1` only, and installing Bridge (or a bridge plugin) is
the user's consent to be driven, so every LOCAL caller (the CLI on the same
box, Adom Hydrogen, and bridges on the loopback direct API) is trusted by
transport and never gated. The approval gate applies to exactly one origin:
the remote relay / cloud-AI path (`via_relay`), a thread driving the machine
from off-box that the user may not be watching. If you are calling over the
relay, the gated verbs below require a human grant. Read-only verbs
(screenshots, status, window and UI reads, discovery) are never gated.

## Which verbs gate

The gated class is "runs code / writes / deletes / kills processes /
synthesizes input":

- **Arbitrary code execution:** `shell_execute`, `run_script` (bash / zsh /
  sh), `launch_process`, `wiki_exec`
- **Filesystem:** `write_file`, `delete_file`
- **Process control:** `process_kill`
- **Synthetic input** (drive the machine as the user): `desktop_click` /
  `desktop_type` / `desktop_press_key` and the Accessibility (AX) action
  verbs; these ride the SAME grant window as shell

One human grant covers the whole class. A bridge's own exec verbs
(`browser_evaluate`, `nbrowser_eval`, `blender_run_python`,
`fusion_run_modeling_script`, ...) gate per the bridge's declared `risk`
field, independently of this class; check that bridge's describe output, not
this doc.

## The `reason` requirement: every gated call must explain itself

Every permission-class verb sent over the relay MUST carry a `reason` arg:
one plain sentence saying WHY you need this. The human sees it verbatim in
the approval prompt, attributed as "The AI says: ..." (omitting it shows
"(no reason given)", which a cautious user should decline). It also lands in
Bridge's activity log.

```bash
adom-desktop shell_execute '{"command":"brew install kicad",
  "reason":"Installing KiCad so I can export your gerbers"}'
```

Write the reason for the human, not for the log: say what you are trying to
accomplish, not what the command does. Good: "Installing the KiCad CLI so I
can export your gerbers". Weak: "running npm install" (that is just the
command again). The reason is capped at 300 chars and never replaces the
command preview; the human always sees both.

A permission-class call with NO reason is refused before any approval path:

```json
{
  "success": false,
  "errorCode": "reason_required",
  "error": "'write_file' needs human permission, so it REQUIRES a `reason`. Retry the SAME call with reason:\"...\".",
  "retryWith": { "reason": "<why you need this, in one plain sentence>" },
  "appliesTo": ["shell_execute", "run_script", "launch_process", "wiki_exec", "write_file", "delete_file", "process_kill"]
}
```

The rule is class-based and flat: the verbs in `appliesTo` ALWAYS carry a
reason (whether or not a grant is currently active); verbs not in that list
never need one. Learn it once: do not pay the round-trip again, and do not
add `reason` to ungated verbs.

## What a refusal looks like

When no grant is active, a gated relay call refuses with
`errorCode: "needs_approval"` plus a structured `approval` block; you never
need to read `~/.adom/config.json`:

```json
{
  "success": false,
  "errorCode": "needs_approval",
  "error": "'write_file' needs shell approval - it runs code / writes / deletes / kills a process (a Tier-2 verb, as dangerous as shell_execute).",
  "approval": {
    "required": true,
    "granted": false,
    "class": "shell",
    "verb": "write_file",
    "grantedBy": "human-only",
    "grantVerb": "request_shell_approval",
    "howToGrant": ["Ask the user to click Approve (1 hr / 24 hr) or Bypass Permissions ...",
                   "Call request_shell_approval to re-surface the prompt ...",
                   "The user can tick Settings > Shell Commands > Bypass Permissions ..."]
  }
}
```

The refusal itself already surfaced a human-facing prompt on the machine: the
Bridge window SUMMONS ITSELF to show the approval dialog (Bridge is normally a
menu-bar agent with no window; an approval flips it from Accessory to Regular,
shows the window, and activates it), and a Notification Center notification
fires as well. If the human actively clicks Deny (embedded mode), you get
`errorCode: "denied_by_user"` instead; do NOT auto-retry that one, ask the
user what they want.

## The correct AI flow on a gate hit

1. **Surface the prompt** (the gate already did once; re-surface on demand):

   ```bash
   adom-desktop request_shell_approval
   ```

   This is ungated; it only SURFACES, never grants. It re-shows the approval
   dialog and brings the Bridge window to the front, so "there is nothing to
   click" is never true.

2. **Ask the human** (in your conversation) to click **Approve 1 hr**,
   **Approve 24 hr**, or **Bypass Permissions** in the Adom Bridge window.

3. **RETRY the original verb unchanged** (with its `reason`). One grant
   covers the whole exec/write/kill/input class for the granted window.

## Grants are HUMAN-ONLY: you cannot self-grant

Granting is permanently human-only. The only grant paths are a human clicking
in Bridge: the approval dialog's Approve buttons and the Settings checkbox. A
GRANT attempt via `shell_auto_approve` / `set_shell_auto_approve` from a relay
(cloud-AI) caller is refused with `errorCode: "human_only"` and auto-surfaces
the human prompt instead. Your only lever is `request_shell_approval`; it
TRIGGERS the human decision, it does not perform it.

The one exception: a REVOKE is allowed for any caller (de-escalation is
always safe):

```bash
adom-desktop shell_auto_approve '{"duration_secs":0}'   # clears the checkbox AND the timer
```

Do not look for a workaround. `launch_process` is not a bypass of
`shell_execute` (it gates identically), and synthetic input (clicking the
Approve button yourself) rides the same grant. If the AI could grant, the
gate would be theater.

## The durable Settings checkbox

**Settings > Shell Commands > Bypass Permissions** (config
`shell_auto_approve_permanent`, default OFF) is the durable indefinite grant,
for machines the user fully trusts. The timed buttons (1 hr / 24 hr) set an
in-memory window instead. Unticking the checkbox, or
`shell_auto_approve {"duration_secs":0}`, returns to per-command approval
instantly.

## Embedded mode: Adom Hydrogen owns the decision

When Bridge runs embedded under Adom Hydrogen (Hydrogen passed `auto_approve`
in `POST /embedded/enter`), the human's app is Hydrogen and Bridge shows no UI
of its own. Each gated relay command is FORWARDED to Hydrogen's
`/permission/request`; Hydrogen renders the prompt (with your `reason` and a
decoded command preview) and returns the decision:

- `allow_once`: run this one command, no window
- `allow_1h` / `allow_24h`: set a timed grant window and run
- `bypass_while_running`: session-length grant (clears on restart)
- `deny`: you get `errorCode: "denied_by_user"`; do not auto-retry

While forwarding is active, a standing grant (flag / checkbox / timer) does
NOT suppress the forward; Hydrogen decides every time. If Hydrogen is
unreachable, Bridge falls back to its own dialog. `auto_approve: true` means
the user opted in to auto-approval.

## macOS TCC permissions (underneath Bridge's gate)

Bridge's approval gate governs WHO may drive the machine; macOS's own TCC
privacy system separately governs what the Bridge PROCESS may do, and no
Bridge grant can override it. In System Settings > Privacy & Security the app
appears as **Adom Bridge**:

- **Screen Recording**: required for window and screen screenshots. Without
  it, captures come back blank or fail.
- **Accessibility**: required for synthetic input (CGEvent mouse/keyboard)
  and the AX read/action verbs. Without it, gated input verbs fail with a
  clear error naming the missing grant (macOS silently ignores CGEvent
  posting otherwise, so Bridge checks `AXIsProcessTrusted` first and refuses
  loudly).

If a verb says the Accessibility or Screen Recording permission is missing,
that is a one-time human toggle in System Settings, not something
`request_shell_approval` can grant.

## Check state up front with `status`

Before an unattended run, read `status.permissions.shell` so you know whether
you will stall on a human gate:

```bash
adom-desktop status
```

```json
"permissions": {
  "shell": {
    "autoApproved": false,
    "willPrompt": true,
    "source": "none",              // "permanent" | "timed" | "none"
    "expiresInSec": null,
    "grantedBy": "human-only",
    "grantVerb": "request_shell_approval",
    "gatedFor": "relay (cloud-AI) callers only",
    "embeddedPolicy": "standalone" // "forward-to-hd" | "auto-approve" | "standalone"
  },
  "bridgeExec": { "note": "bridge exec verbs gate per the bridge's declared risk field" }
}
```

If `willPrompt` is true and no human is at the machine, get the grant FIRST
(`request_shell_approval` plus asking the user) before starting the run.

## Quick reference

| Situation | What you do |
|---|---|
| `errorCode: reason_required` | Resend the SAME call plus `reason:"<one plain sentence>"` |
| `errorCode: needs_approval` | `request_shell_approval`, ask the human to click Approve, retry |
| `errorCode: human_only` | You tried to self-grant. Ask the human instead |
| `errorCode: denied_by_user` | The human said no. Do not auto-retry; ask them |
| A capture/input verb reports a missing macOS permission | Ask the user to enable Adom Bridge under System Settings > Privacy & Security (Screen Recording or Accessibility) |
| Planning an unattended run | Check `status.permissions.shell.willPrompt` first |
| Need to drop a grant | `shell_auto_approve {"duration_secs":0}` (allowed for any caller) |
