# Capabilities

> Guarded OS services for notifications, clipboard, credentials, dialogs, file drops, and effect-channel access boundaries.

- Repository: vercel-labs/native
- GitHub: https://github.com/vercel-labs/native
- Human docs: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a
- Complete Markdown: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a/llms-full.txt

## Source Files

- `examples/capabilities/README.md`
- `examples/capabilities/build.zig`
- `skill-data/core/references/bridge-security-native-capabilities.md`
- `src/runtime/effects.zig`
- `examples/effects-probe/README.md`

---

---
title: "Capabilities"
description: "Guarded OS services for notifications, clipboard, credentials, dialogs, file drops, and effect-channel access boundaries."
---

Native OS services are exposed through `PlatformServices`, `Runtime` methods, host-emitted events, and — for WebView apps — default-deny builtin bridge commands under `native-sdk.*`. Trusted Zig code calls the runtime directly. Web content never receives capability access unless each command is listed in `RuntimeOptions.builtin_bridge` with matching `security.permissions` and origin policy. In a `UiApp`, clipboard side effects go through the effects channel (`fx.writeClipboard` / `fx.readClipboard`) so `update` never holds a runtime handle.

## Access model

```text
┌─────────────────────┐     ┌──────────────────────────┐
│  Trusted Zig        │     │  WebView / JS            │
│  runtime.*          │     │  window.zero.invoke /    │
│  Event.files_dropped│     │  window.zero.os|clipboard│
└──────────┬──────────┘     └────────────┬─────────────┘
           │                             │
           │                             │ builtin_bridge Policy
           │                             │ + security.permissions
           │                             │ + origin allowlist
           ▼                             ▼
        ┌────────────────────────────────────────┐
        │  RuntimeSystemServices / PlatformServices │
        │  showNotification, clipboard, credentials │
        │  dialogs, openUrl, revealPath, …          │
        └────────────────────────────────────────┘
```

| Path | Who uses it | Gate |
| --- | --- | --- |
| `Runtime` methods (`showNotification`, `writeClipboard`, …) | Zig app / loop code | Platform service availability (`supports`) |
| Builtin bridge (`native-sdk.os.*`, `native-sdk.clipboard.*`, …) | Trusted WebView JS | `builtin_bridge.enabled` + per-command policy; dialog/OS/clipboard/credentials are always default-deny |
| Host events (`files_dropped`, lifecycle activate/deactivate) | Zig `event_fn` and optional WebView listeners | Host emission only; no permission grant required |
| Effects (`fx.writeClipboard` / `fx.readClipboard`) | `UiApp` `update` | Effect slots/keys; no bridge permission (loop-owned) |

`window.zero.os.*`, `window.zero.clipboard.*`, and `window.zero.credentials.*` exist when the host injects the JS surface; they still route through the same builtin command names and policy checks. `js_window_api` can open a limited permission fallback for window/view/command/platform helpers only — it does **not** unlock dialog, OS, clipboard, or credential commands.

## Capability inventory

| Capability | Zig surface | Bridge command | Permission | Host notes |
| --- | --- | --- | --- | --- |
| Feature probe | `runtime.supports(feature)` | `native-sdk.platform.supports` | `window` | Returns host + engine truth for `PlatformFeature` |
| Open URL | `runtime.openExternalUrl(url)` | `native-sdk.os.openUrl` | `network` | Also requires `security.navigation.external_links` allowlist |
| Notification | `runtime.showNotification(options)` | `native-sdk.os.showNotification` | `notifications` | Title required; subtitle/body optional |
| Message / file dialogs | `showMessageDialog` / `showOpenDialog` / `showSaveDialog` | `native-sdk.dialog.showMessage` / `openFile` / `saveFile` | `dialog` | Default-deny on the bridge |
| Reveal path | `runtime.revealPath(path)` | `native-sdk.os.revealPath` | `filesystem` | File manager reveal |
| Recent documents | `addRecentDocument` / `clearRecentDocuments` | matching `native-sdk.os.*` | `filesystem` | Platform recent-document list |
| Clipboard text / rich | `readClipboard` / `writeClipboard` / `*Data` | `native-sdk.clipboard.readText` / `writeText` / `read` / `write` | `clipboard` | Text path always; rich MIME on system hosts |
| Clipboard (UiApp) | `fx.writeClipboard` / `fx.readClipboard` | — | — | Effect channel; terminal Msg outcomes |
| Credentials | `setCredential` / `getCredential` / `deleteCredential` | `native-sdk.credentials.set` / `get` / `delete` | `credentials` | Keychain / libsecret / Credential Manager |
| File drops | `Event.files_dropped` | `window.zero.on("drop:files")` / `native-sdk:drop:files` | — | Host-emitted; system WebView hosts |
| App activation | `LifecycleEvent.activate` / `deactivate` | `app:activate` / `app:deactivate` | — | Host-emitted lifecycle |

Unsupported hosts reject with the standard unsupported-service error rather than no-oping.

## Permissions and policy

Named grants live in `src/security/root.zig`:

| Constant | String |
| --- | --- |
| `permission_window` | `window` |
| `permission_command` | `command` |
| `permission_view` | `view` |
| `permission_dialog` | `dialog` |
| `permission_filesystem` | `filesystem` |
| `permission_clipboard` | `clipboard` |
| `permission_network` | `network` |
| `permission_notifications` | `notifications` |
| `permission_credentials` | `credentials` |

Bridge policy is `bridge.Policy` / `BridgeCommandPolicy`:

```zig
.builtin_bridge = .{
    .enabled = true,
    .commands = &.{
        .{ .name = "native-sdk.os.showNotification", .permissions = &.{ "notifications" }, .origins = &.{ "zero://inline", "zero://app" } },
        .{ .name = "native-sdk.clipboard.readText", .permissions = &.{ "clipboard" }, .origins = &.{ "zero://inline", "zero://app" } },
        .{ .name = "native-sdk.clipboard.writeText", .permissions = &.{ "clipboard" }, .origins = &.{ "zero://inline", "zero://app" } },
        .{ .name = "native-sdk.credentials.set", .permissions = &.{ "credentials" }, .origins = &.{ "zero://inline", "zero://app" } },
        // … one entry per command the WebView may call
    },
},
.security = .{
    .permissions = &.{
        "window", "network", "filesystem", "notifications",
        "dialog", "clipboard", "credentials",
    },
    .navigation = .{
        .allowed_origins = &.{ "zero://inline", "zero://app" },
        .external_links = .{
            .action = .open_system_browser,
            .allowed_urls = &.{ "https://example.com/docs/*" },
        },
    },
},
```

Policy evaluation (`bridge.Policy.allows`):

1. `enabled` must be true.
2. Command name must match a listed policy entry.
3. App must hold every permission required by that entry.
4. Origin must match the entry’s `origins` list (or `"*"`).

Denied calls return `permission_denied` (for example `"Clipboard API is not permitted"`). Prefer exact origins over `"*"`.

`app.zon` can declare the same permission and capability strings for packaging/docs; the runtime gate that matters at call time is `RuntimeOptions.security` + `builtin_bridge`.

## Platform support discovery

`PlatformFeature` includes capability-relevant values such as `dialogs`, `clipboard_text`, `clipboard_rich_data`, `open_url`, `reveal_path`, `notifications`, `recent_documents`, `credentials`, `file_drops`, and `app_activation_events`.

Probe from Zig with `runtime.supports(.notifications)` (and peers). From JS:

```javascript
const r = {};
for (const feature of [
  "open_url", "reveal_path", "recent_documents", "notifications",
  "dialogs", "clipboard_text", "clipboard_rich_data", "credentials",
  "file_drops", "app_activation_events",
]) {
  r[feature] = await window.zero.invoke("native-sdk.platform.supports", { feature });
}
```

Feature names accept snake_case enum tags and camelCase aliases (`clipboardRichData`, `fileDrops`, `recentDocuments`, …) via `platformFeatureFromString`.

### Host matrix (capability-related)

| Feature | macOS system | macOS Chromium | Linux system | null (tests) |
| --- | --- | --- | --- | --- |
| `clipboard_text` / `clipboard_rich_data` | yes | yes | yes | yes |
| `notifications` | yes | yes | yes | yes |
| `dialogs` | yes | yes | yes | yes |
| `open_url` / `reveal_path` / `recent_documents` | yes | yes | yes | yes |
| `credentials` | yes (Keychain) | yes | yes when libsecret available | yes (fake) |
| `file_drops` | system engine only | no | system engine only | yes |
| `app_activation_events` | yes | yes | system engine only | yes |

Linux credentials and some audio features are live-probed; a missing library reports unsupported instead of a half-implemented path.

## Notifications

**Zig**

```zig
try runtime.showNotification(.{
    .title = "Build finished",
    .subtitle = "native-sdk",
    .body = "All checks passed.",
});
```

**Bridge**

```javascript
await window.zero.invoke("native-sdk.os.showNotification", {
  title: "Capabilities",
  subtitle: "native-sdk",
  body: "Notification bridge succeeded.",
});
// or: await window.zero.os.showNotification({ ... })
```

| Field | Constraint |
| --- | --- |
| `title` | Required, non-empty, ≤ `max_notification_title_bytes` (128) |
| `subtitle` | Optional, ≤ 128 |
| `body` / `message` | Optional, ≤ `max_notification_body_bytes` (1024) |

Empty title → `InvalidNotificationOptions`. NUL bytes rejected.

## Clipboard

### Runtime / bridge path

| Operation | Bridge | Payload / result |
| --- | --- | --- |
| Write text | `native-sdk.clipboard.writeText` | `{ text }` (aliases `data`, `value`) → `true` |
| Read text | `native-sdk.clipboard.readText` | `{}` → JSON string |
| Write MIME | `native-sdk.clipboard.write` | `{ mimeType, data }` → `true` |
| Read MIME | `native-sdk.clipboard.read` | `{ mimeType }` → `{ mimeType, data }` |

Defaults: missing MIME type is `text/plain`. Bounds: MIME ≤ 128 bytes, payload ≤ `max_clipboard_data_bytes` (65536). Supported rich types on system hosts include `text/html`, `text/rtf`, and `application/rtf`.

### Effects path (`UiApp`)

Clipboard effects share effect slots and keys with spawn/fetch/file. Writes and reads run **synchronously on the loop thread** (pasteboard is a main-thread service) but still deliver exactly one terminal `Msg` through the ordinary drain.

```zig
// in update / effect builders
fx.writeClipboard(.{
    .key = clipboard_key,
    .text = "Copied from Native SDK",
    .on_result = Effects.clipboardMsg(.clipboard_done),
});
fx.readClipboard(.{
    .key = clipboard_key,
    .on_result = Effects.clipboardMsg(.clipboard_done),
});
```

| Outcome (`EffectClipboardOutcome`) | Meaning |
| --- | --- |
| `ok` | Write landed / read text is in `EffectClipboardResult.text` (copy what the model keeps) |
| `failed` | Host has no clipboard arm, pasteboard error, or read over `max_effect_clipboard_bytes` |
| `rejected` | Slots busy, duplicate active key, write over bound, or no services bound |
| `cancelled` | `fx.cancel(key)` before delivery (op may already have completed) |

`max_effect_clipboard_bytes` equals `platform.max_clipboard_data_bytes` (65536). Oversize writes never silently truncate — they reject whole so a cut string cannot pass for the full clipboard.

## Credentials

Key shape: `{ service, account }` plus `secret` on set.

| Command | Result |
| --- | --- |
| `native-sdk.credentials.set` | stores secret → `true` |
| `native-sdk.credentials.get` | secret string or `null` if missing |
| `native-sdk.credentials.delete` | `true` if deleted, `false` if not found |

| Field | Max |
| --- | --- |
| `service` | 128 |
| `account` | 256 |
| `secret` | 4096 |

Empty service/account/secret → `InvalidCredentialOptions`. Hosts: macOS Keychain, Linux Secret Service/libsecret when available, Windows Credential Manager, null-platform fake store for tests.

## Dialogs

Bridge commands (always explicit policy):

| Command | Notable payload fields | Result |
| --- | --- | --- |
| `native-sdk.dialog.openFile` | `title`, `defaultPath`, `allowMultiple`, `allowDirectories` | path array or `null` |
| `native-sdk.dialog.saveFile` | `title`, `defaultPath`, `defaultName` | path string or `null` |
| `native-sdk.dialog.showMessage` | `style` (`info`/`warning`/`critical`), `title`, `message`, `informativeText`, buttons | result tag name string |

OS-owned dialogs coexist with engine-drawn widgets; for menu/tray placement see related pages. Do not expose open/save dialogs to untrusted origins.

## OS services (URL, reveal, recent)

| Command | Permission | Extra gate |
| --- | --- | --- |
| `native-sdk.os.openUrl` | `network` | URL must be `http://` or `https://`, ≤ 4096 bytes, and match `external_links` allowlist (`open_system_browser` action) |
| `native-sdk.os.revealPath` | `filesystem` | non-empty path ≤ 4096 |
| `native-sdk.os.addRecentDocument` | `filesystem` | path validation |
| `native-sdk.os.clearRecentDocuments` | `filesystem` | none |

Wildcard external URL patterns must be `http(s)://host/...*` with a path segment (for example `https://example.com/docs/*`). Patterns without a `/` after the host are rejected so `https://example.com*` cannot match `https://example.com.evil/...`.

## File drops and activation events

### File drops

Host → platform event `.files_dropped` → runtime:

1. Dispatches `Event.files_dropped` to the app `event_fn` with `FileDropEvent` (`window_id`, optional `view_label` / `point`, `paths`).
2. Emits WebView window event `drop:files` with JSON detail `{ windowId, paths, ... }`.

Listen from JS with `window.zero.on("drop:files", …)` or `window.addEventListener("native-sdk:drop:files", …)`. No bridge permission — the host owns the path list.

### App activation

Lifecycle activate/deactivate update Zig state and emit `app:activate` / `app:deactivate` to open windows (`detail` typically `{}`).

## Packaging metadata (associations and schemes)

`app.zon` may declare `file_associations` and `url_schemes` (example: extension `zncap`, scheme `native-sdk-capabilities`). These are packaging/registration metadata, not bridge commands. They are validated and emitted into package registration on macOS, Linux, and Windows.

## Size and bridge limits

| Limit | Value |
| --- | --- |
| Bridge request / response / result | `max_message_bytes` / `max_response_bytes` / `max_result_bytes` = 1 MiB each |
| Request id | 64 bytes |
| Command name | 128 bytes |
| Clipboard data | 65536 bytes |
| External URL / reveal / recent path | 4096 bytes each |

Oversized bridge requests return `payload_too_large`.

## Example: `examples/capabilities`

Reference app that wires full policy, inline WebView UI, file-drop status bar updates, and headless tests.

```sh
# system WebView (macOS example)
zig build run -Dplatform=macos -Dweb-engine=system

# headless / null platform tests
zig build test -Dplatform=null

# from repository root
zig build test-examples-native
```

The example’s `builtin_policies` list every capability command against `zero://inline` / `zero://app`, grants matching permissions, allows external docs URLs under `https://example.com/docs/*`, and asserts notification/clipboard/credential/file-drop/activation behavior through `TestHarness` + null platform counters.

## Errors

Bridge rejects use `error.code`:

| Code | Typical cause |
| --- | --- |
| `permission_denied` | Missing policy entry, origin, or grant |
| `unknown_command` | Not a registered builtin or app handler |
| `invalid_request` | Malformed payload, validation failure, unsupported service |
| `handler_failed` | Handler returned an error |
| `payload_too_large` | Request over bridge budget |
| `internal_error` | Unexpected runtime failure |

Zig validation errors include `InvalidNotificationOptions`, `InvalidClipboardOptions`, `ClipboardFieldTooLarge`, `InvalidCredentialOptions`, `CredentialFieldTooLarge`, `InvalidExternalUrl`, `NavigationDenied`, `InvalidRevealPath`, and peers mapped into bridge error responses.

## Verification

| Check | Signal |
| --- | --- |
| Policy denies by default | Bridge without `builtin_bridge` entry → `permission_denied` |
| Policy allows | Success JSON with `"ok":true` / `"result":…` |
| Notification | null-platform `notificationCount()` / last title in tests |
| Clipboard round-trip | write then read returns same text |
| Credentials | set → get → delete → get `null` |
| File drop | `drop_count` increments; last window event `drop:files` |
| Activation | `app:activate` / `app:deactivate` event names |
| Effects clipboard | fake executor records request; feed terminal outcome; assert Msg |

## Related pages

<CardGroup>
  <Card title="Runtime and effects" href="/runtime-effects">
    Effect kinds, clipboard effect outcomes, cancellation, and loop-thread delivery.
  </Card>
  <Card title="Bridge and builtin commands" href="/bridge">
    Host-to-content bridge payloads, builtin command surface, and permission boundaries.
  </Card>
  <Card title="Menus, dialogs, and tray" href="/menus-dialogs-tray">
    OS-owned dialogs and chrome next to engine-drawn widgets.
  </Card>
  <Card title="State and data flow" href="/state-data-flow">
    Effects as the only side-effect channel in the TEA update loop.
  </Card>
  <Card title="Platform support and security" href="/platform-security">
    Host capability matrix and security defaults across platforms.
  </Card>
  <Card title="Testing" href="/testing">
    Headless truth drivers, effect tests, and harness patterns used by the capabilities example.
  </Card>
</CardGroup>
