---
name: Capabilities and ACL
type: architecture-map-page
slice: 02-tauri-runtime
last_verified: 2026/05/09
---

# Capabilities and ACL

> **Where you are:** [Architecture map](../README.md) → [Tauri runtime](README.md) → Capabilities and ACL

**Plain English summary.** Tauri 2 ships a permission-and-capability ACL: every plugin and core API ships permissions, capabilities bind permissions to specific window labels. Lumotia ships two capability files. The main window gets the broad set (dialog, opener, autostart, global shortcuts, notifications, full window control). The three secondary windows (task float, transcript viewer, transcription preview) get a narrow set with no plugin permissions at all and no destructive window APIs. The split is the firewall that prevents a compromised secondary window from launching an installer or registering a global hotkey.

## At a glance

- Path: `src-tauri/capabilities/main.json`, `src-tauri/capabilities/secondary-windows.json`.
- Auto-generated schemas: `src-tauri/gen/schemas/` (`desktop-schema.json`, `linux-schema.json`, `macOS-schema.json`, `windows-schema.json`, `acl-manifests.json`, plus per-plugin globals). Do not edit by hand — they regenerate from the installed plugin set.
- Tauri commands exposed: none (declarative config).
- Events emitted: none.
- Depends on: the runtime plugin set declared in `src-tauri/Cargo.toml` and wired in `src-tauri/src/lib.rs::run`. If a capability references a permission whose plugin is not enabled, the build fails.
- Called from frontend at: every `invoke()` is run through this ACL — the capability for the calling window must include the permission for the command being called, otherwise Tauri rejects the IPC call before it reaches the Rust handler.

## What's in here

### `main.json` (`src-tauri/capabilities/main.json:1`)

```
identifier:  "main"
description: "Main window capability for user-initiated app control."
windows:     ["main"]
permissions:
  - core:default
  - core:window:allow-start-dragging
  - core:window:allow-set-always-on-top
  - core:window:allow-minimize
  - core:window:allow-toggle-maximize
  - core:window:allow-is-maximized
  - core:window:allow-close
  - core:window:allow-hide
  - core:window:allow-show
  - core:window:allow-set-focus
  - opener:default
  - dialog:default
  - global-shortcut:allow-register
  - global-shortcut:allow-unregister
  - autostart:allow-enable
  - autostart:allow-disable
  - autostart:allow-is-enabled
  - notification:allow-is-permission-granted
  - notification:allow-request-permission
  - notification:allow-notify
```

Notes:

- `core:default` brings in the entire `core:event` set, allowing the main window to listen and emit on all event channels.
- The window-control set is granular: there is no `core:window:default`, every action is opted in. `allow-start-dragging` is what the custom Titlebar uses to drag a frameless window.
- `opener:default` lets the frontend open external URLs via `tauri-plugin-opener`. This is how the Settings → About links route out.
- `dialog:default` enables the file/save dialogs used by the import-audio and save-diagnostic-report flows.
- `global-shortcut:allow-register` and `allow-unregister` let the main window manage the dictation hotkey via the cross-platform plugin path. On Linux Lumotia uses the bespoke evdev backend (see `commands::hotkey`), but Settings still talks to the global-shortcut plugin so the same UI works on macOS / Windows.
- `autostart:*` lets Settings toggle login-time autostart.
- `notification:*` is what the Phase 6 nudge bus uses; the Rust-side `commands::nudges::deliver_nudge` adds the main-window-only firewall.

### `secondary-windows.json` (`src-tauri/capabilities/secondary-windows.json:1`)

```
identifier:  "secondary-windows"
description: "Narrow capability for passive secondary windows."
windows:     ["tasks-float", "transcript-viewer", "transcription-preview"]
permissions:
  - core:event:default
  - core:window:allow-start-dragging
  - core:window:allow-close
  - core:window:allow-hide
  - core:window:allow-show
  - core:window:allow-set-focus
  - core:window:allow-set-always-on-top
```

Notes:

- No `core:default`. Only `core:event:default`. Secondary windows can listen to events emitted by the backend or other windows but can't reach the broader core surface (e.g. shell, fs).
- No plugin permissions at all. No `dialog`, no `opener`, no `global-shortcut`, no `autostart`, no `notification`, no `updater`. This is enforced as a regression test (see below).
- The window-control set is the bare minimum to drag (titleless overlay), close, hide, show, focus, and pin always-on-top. No maximise, no minimise, no toggle-fullscreen.

### Regression test

`src-tauri/tests/config_hardening.rs::secondary_windows_do_not_get_high_risk_plugin_permissions` (`src-tauri/tests/config_hardening.rs:50`) walks every JSON file in `capabilities/`, finds the ones whose `windows` array contains any of the three secondary labels, and asserts none of their permissions start with `dialog:`, `autostart:`, `global-shortcut:`, `opener:`, or `updater:`. The test fails the moment someone adds, for example, `dialog:default` to the secondary-windows file.

### `gen/schemas/`

Auto-generated by `tauri-build` from the plugin set. Files:

```
acl-manifests.json
capabilities.json
desktop-schema.json
linux-schema.json
macOS-schema.json
windows-schema.json
plugin-global-scope-schema.json
plugin-default-permissions.json
```

These are committed to source control so a fresh checkout has a working ACL even before the first build. Do not edit by hand. If you add a plugin, `cargo tauri build` regenerates them. If a capability file references a permission absent from these schemas, the IDE's JSON-schema validation will flag it before the build even runs.

## Data flow

The ACL is enforced in two places:

1. **Webview IPC layer.** Every `invoke()` call from a window is checked against the capability bound to that window's label. A permission miss returns an IPC error before the Rust handler runs.
2. **Tauri-side guards.** Several command handlers add their own `ensure_main_window` check on top of the ACL (see [Power assertions and security](commands/power-and-security.md)). This is defence in depth: even if the ACL ever drifted, a secondary window calling, say, `delete_implementation_rule` would still be rejected by the Rust guard.

## Watch-outs

- Adding a permission to either capability is a security decision. The "secondary windows" set in particular has been pruned deliberately — the in-repo tests and the brief item #B.2 review trace the path that closed each gap.
- The Phase 9 LLM-content-tags command (`commands::llm::extract_content_tags_cmd`) does NOT check the capability; the History page lives in the main window so the main capability already gates it. If you ever expose this command to a secondary window, add an `ensure_main_window` guard and an explicit ACL allowlist entry.
- `notification:allow-notify` is on the main capability. Secondary windows cannot fire notifications. The frontend's nudge bus already uses the main-window guard via `commands::nudges::deliver_nudge`, but if you ever invoke the plugin's JS-side `sendNotification` directly from a secondary window, it will fail with an ACL error.
- The schema files in `gen/schemas/` are updated when the dependency graph changes. If you bump a plugin version, expect a diff there too. Commit them in the same PR as the dependency bump.

## See also

- [App lifecycle](app-lifecycle.md) — plugins are wired in `lib.rs::run`, gated by the same `cfg(not(target_os = "android"))` block that gates the desktop-only permissions.
- [Cargo and features](cargo-and-features.md) — the dependency graph that `gen/schemas/` is generated from.
- [Tauri config](tauri-config.md) — the CSP works alongside the ACL: CSP guards the webview, ACL guards the IPC.
- [Tests](tests.md) — `secondary_windows_do_not_get_high_risk_plugin_permissions` is the regression net.
- [Power assertions and security](commands/power-and-security.md) — `ensure_main_window` is the defence-in-depth pair.
