Minibar Capabilities and Permissions
Status v0.3 · for plugin authors and reviewers · Chinese original: 06-capabilities.md · API details: plugin-guide.en.md
Minibar is a platform. Every capability the host can obtain from macOS is meant to be available to plugins in a declared, granted and revocable form; conversely, the host never asks the system for anything no plugin has declared. This document spells both sentences out: the capability catalog, the permission and OS authorization behind each entry, how the host enforces least privilege, what is deliberately out of scope, and what comes next.
1. Five principles
| # | Principle | How it is enforced |
|---|---|---|
| P1 | Declared is the boundary | Everything a plugin may use is in manifest.json: permissions, allowedHosts, allowedSchemes. Anything undeclared fails with permission_denied / host_not_allowed / invalid_argument, checked once in the runtime and once more in the app process. |
| P2 | Least privilege, lazily | Launch, installing a plugin and onboarding never ask macOS for a system permission. A system prompt appears only when a plugin is installed, the user granted it that permission, and it calls the API for the first time. If no installed plugin declares a permission, the corresponding prompt never appears on this Mac. Entitlements are a static minimum locked by a CI test (§5). |
| P3 | Side effects only on a tap | Opening links or apps, writing the clipboard, freeing memory, running a Shortcut, keeping the Mac awake: all of it is callable only from an action (the user tapped a button); from render it throws not_in_action. Nothing happens quietly on refresh. |
| P4 | Per item, revocable, immediate | The install prompt explains every permission; Settings turns any single one off; after a revocation the next call is refused at once and held resources (such as a wake assertion) are released. |
| P5 | Auditable | Source is viewable before installing; the install prompt lists hosts and URL schemes; every capability call is logged for the plugin; diagnostics exports never include plugin secrets. |
2. Capability matrix
Status: ✅ shipped (v0.3) · 🗓 planned (milestone) · ⛔ never (§4)
| Capability | ctx API | Permission | OS authorization / entitlement | Phase | Status |
|---|---|---|---|---|---|
| Time, locale, host version | ctx.now() ctx.locale ctx.host | none | — | any | ✅ |
| Logging | ctx.log() console.* | none | — | any | ✅ |
| Persistent storage (1 MB) | ctx.storage.* | none | sandbox container file | any | ✅ |
User settings (incl. secret) | ctx.settings.* | none | — | any | ✅ |
| System metrics: CPU, memory, disk, load, network rate, battery | ctx.system() | none | — | any | ✅ |
| Hardware: model, chip, core layout, memory | ctx.system().hardware | none | — | any | ✅ |
| Memory pressure, swap | ctx.system().memory.pressure .swapUsed | none | — | any | ✅ |
| Granted permissions | ctx.permissions.granted(name) .list() | none | — | any | ✅ |
| Hash, HMAC, Base64, UUID | ctx.crypto.* | none | — | any | ✅ |
| HTTPS requests (allowlisted hosts) | ctx.http.* | network | network.client | any | ✅ |
| Open http / https / mailto links | ctx.openURL(), button.url | none | — | action | ✅ |
Open another app's URL scheme (things:, obsidian:…) | ctx.openURL() | apps + allowedSchemes | — | action | ✅ |
| Open / activate an app | ctx.apps.open(bundleID) | apps | — | action | ✅ |
| Running apps, frontmost app | ctx.apps.running() | apps | — | any | ✅ |
| Notifications | ctx.notify() | notifications | UNUserNotificationCenter (on first post) | any (1 per minute) | ✅ |
| Read clipboard | ctx.clipboard() / .read() | clipboard | per-session confirmation on the card | any | ✅ |
| Write clipboard | ctx.clipboard.write(text) | clipboard | — | action | ✅ |
| Calendar (read + create events) | ctx.calendar.upcoming() ctx.calendar.add() | calendar | TCC Calendars (on first call) · personal-information.calendars | read: any; add: action | ✅ |
| Reminders (read + create + complete) | ctx.reminders.incomplete() .add() .complete() | reminders | TCC Reminders (on first call) · same entitlement | read: any; add / complete: action | ✅ |
| Location (~1 km) | ctx.location() | location | TCC Location (on first call) · personal-information.location | any | ✅ |
| Free up memory | ctx.freeMemory() | system | — | action | ✅ |
| Run one of the user's Shortcuts | ctx.runShortcut(name, input?) | system | — | action | ✅ |
| Keep the Mac awake | ctx.keepAwake(enabled) | system | IOPMAssertion | action | ✅ |
| Custom web card (React or any framework) | web node | none | WebKit (package files only, no network) | any | ✅ |
| User-picked files / folders (read-only) | ctx.files.pick() .list() .entries() .read() .forget() | files | NSOpenPanel (the user chooses) + security-scoped bookmark · files.user-selected.read-only + files.bookmarks.app-scope | pick / forget: action | ✅ |
| Contacts (birthdays, upcoming) | ctx.contacts.* | contacts | TCC Contacts · personal-information.addressbook | any | ⏸ Not for now: little demand, the most sensitive data, and one more entitlement to justify in review; birthday reminders reach plugins through calendar / reminders once the user subscribes to them there |
| Expose plugin actions to Shortcuts / Siri | App Intents | — | — | — | 🗓 M5 (not a permission; leaves the entitlement set unchanged) |
| Volume / output device | ctx.audio.* | system | CoreAudio | action | 🗓 M5 (evaluate) |
| Shell, scripts, arbitrary files, AppleScript, screen, keyboard/mouse, camera, microphone, listening sockets | — | — | — | — | ⛔ |
3. Permissions in detail
network
- Only hosts in
allowedHosts(plus subdomains), https only (localhost excepted), 15 s per request, 5 MB per response, 30 s of host waiting per render. "*"allows any host and is shown in red at install time; market review looks at it closely.- Host entitlement:
com.apple.security.network.client(no server).
clipboard
- Read: the first read in each app session is confirmed once more on the card (PRD F-45), then not again that session; revoking the permission takes effect immediately.
- Write: action only, text up to 100 KB. Writing needs no session confirmation (nothing leaks).
notifications
- One per plugin per 60 seconds; the system authorization is requested on the first post; when turned off at the system level the notification is dropped and logged.
calendar / reminders
- Read only; on macOS 14+ full access is requested (EventKit's only readable tier). The first call triggers the system prompt; while it is up the plugin gets
os_access_pendingand the host re-renders once the user answers. - Writes only from an action (the user tapped a button):
calendar.add()creates an event,reminders.add()a reminder,reminders.complete(id)ticks one off (or reopens it). Only calendars and lists that allow modification; titles ≤ 200 characters, notes ≤ 2000, events ≤ 1 year long; no deleting, no bulk edits, no changes to other people's events. The bundled Reminders plugin usescompleteso the user can tick reminders off right in the menu bar.
location
- 1 km accuracy, 15-minute cache, reverse-geocoded city name. First call triggers the prompt, same flow as above.
system
freeMemory(): the host allocates and touches up to half of physical memory for at most 3 seconds so the kernel reclaims cached and inactive pages, then releases it; stops early when free pages run low. Returns free bytes before and after.runShortcut(name, input?): runs a Shortcut the user created throughshortcuts://run-shortcut. This is the general-purpose channel to the rest of the system (open apps, control HomeKit, call any service); the boundary is whatever the user put in the shortcut.keepAwake(enabled):IOPMAssertionCreateWithNameprevents idle sleep; one assertion per plugin, released automatically when the plugin is disabled, loses the permission, is uninstalled, or the app quits.- All three are action-only.
apps
apps.running(): name, bundle ID, frontmost and hidden flags of regular (Dock-visible) apps. No window titles, no process arguments.apps.open(bundleID): opens or activates an installed app;unavailablewhen it is not installed.allowedSchemes: URL schemes declared in the manifest (things,obsidian,raycast…) are the only onesctx.openURL("things:///add?title=…")lets through; undeclared schemes are refused.http/https/mailtoare always allowed;file/javascript/datanever.
files
- A plugin never gets read access to a path; it gets what the user picked by hand in the system file dialog:
ctx.files.pick({ folders, multiple, types, message })is action-only and returns at once. The host shows the dialog, names the plugin that is asking, stores a security-scoped bookmark for each chosen item and re-renders the plugin. - Afterwards, in any phase:
list()(picked items, withavailablefalse for moved or deleted ones),entries(id)(direct children of a picked folder, hidden files skipped by default, ≤ 500),read(id, { encoding, maxBytes })(≤ 1 MB, the rest flaggedtruncated). Children are addressed as"<id>/<relative path>";.., absolute paths and symlinks that leave the folder are rejected. - All reads happen inside the app's sandbox scope; the XPC runtime process never touches the file system. Content reaches the plugin as a JSON string.
- Revocation: the plugin can
forget(id); the user removes items one by one or all at once under Settings › Plugins › "Files this plugin can read"; switchingfilesoff clears everything; uninstalling removes the record. At most 50 items per plugin, newest kept. - Not offered: writing, creating, deleting, watching folders, walking the home directory (anything the user did not pick is invisible).
4. Deliberately out of scope
| Capability | Why |
|---|---|
| Shell / child processes / script execution | Impossible in the sandbox, and one shell plugin would destroy trust in the whole market |
| Arbitrary file access, home directory listing | Sandbox and review; only files the user picks by hand in a dialog are readable, behind the files permission |
| AppleScript / Apple Events | Needs a temporary-exception entitlement, high review risk; Shortcuts is Apple's sanctioned automation channel |
| Screen recording, Accessibility (keyboard/mouse control), input monitoring | Privacy and review; a menu bar dashboard does not need them |
| Camera, microphone | Same |
| Listening sockets / local servers | Plugins have no background execution and nothing should connect into them |
| Other apps' data, Keychain | Sandbox isolation; a plugin's secrets live only in its own secret settings |
| Native code, dynamic libraries | Guidelines 2.5.2 / 4.7; plugins are always JavaScript plus declarative UI (or a web card) |
5. How the host enforces least privilege
5.1 Entitlements (static minimum, locked by CI)
| File | Key | Capability |
|---|---|---|
Minibar.entitlements | app-sandbox | all |
network.client | registry and plugin updates; network requests are made by the XPC service | |
personal-information.calendars | calendar / reminders | |
personal-information.location | location | |
files.user-selected.read-only | local install, linked folders, the files permission's file dialog | |
files.bookmarks.app-scope | remembering linked folders and files picks across launches | |
MinibarRuntime.entitlements | app-sandbox, network.client, cs.allow-jit | where plugin code and http run |
Tests/MinibarCoreTests/EntitlementsTests.swift asserts that these two files contain exactly these keys; a new capability must change the test and this table together.
5.2 System prompts only on first use
| System permission | Triggered by | Never triggered by |
|---|---|---|
| Calendars / Reminders | a granted plugin's first ctx.calendar / ctx.reminders call | launch, install, onboarding, the moment a grant is given |
| Location | the first ctx.location() call | same (creating CLLocationManager does not prompt) |
| Notifications | the first ctx.notify() | same |
Onboarding only records plugin-level grants; its last page says that macOS may ask once when a plugin first uses a capability.
5.3 Revocation and cleanup
After a permission is revoked both checks apply immediately (no restart); revoking system or disabling / uninstalling a plugin releases its wake assertion; revoking clipboard also clears the session confirmation. Uninstalling removes the plugin's storage and settings.
5.4 For reviewers
- The complete interface available to plugins is §2; there is no hidden channel.
__hostaccepts strings only and permissions are checked in the bridge and again in the app process. - Every side effect needs a tap; every system prompt needs a prior plugin-level grant.
6. Guidance for plugin authors
- Declare only what you use and explain it in
description; the install prompt shows it verbatim. - Degrade gracefully: render something without grants. Use
ctx.permissions.granted('location')to choose a branch; catchpermission_deniedand offer a$allow:<permission>button so one tap grants it. - While a system prompt is up (
os_access_pending) do not retry in a loop; the host re-renders when the user answers. - Side effects go in actions: button → action →
ctx.openURL / apps.open / clipboard.write / freeMemory / runShortcut / keepAwake. - URL schemes must be listed in
allowedSchemesand need theappspermission. - No secrets in code: use a
secretsetting andctx.settings.get(); the host keeps it out of logs.
7. Roadmap
| Milestone | Capabilities | Notes |
|---|---|---|
| M3 (this round) | system, apps, clipboard.write, keepAwake, crypto, permissions, web card | implemented, awaiting on-device acceptance |
| M4 | ✅ files (user-picked files/folders via security-scoped bookmarks); ✅ reminders.add / reminders.complete / calendar.add; ⏸ contacts (not for now, see §2) | each follows declare → lazy prompt → action |
| M5 | App Intents (expose plugin actions to Shortcuts / Siri); audio (volume, output device); plugin-to-plugin messages (ctx.bus) | decided after a demand check |