Interactive widgets invoke named, pre-registered capabilities on the Mac,
such as launching an app or adjusting volume. Widget JavaScript supplies an
action id and arguments; it cannot register new actions or select arbitrary
commands through trigger.
The Mac checks consent before execution. Registering an action through MCP only proposes it; by default, you approve it in the Mac app. Actions work in release builds. The Auto-consent controls option, off by default, automatically approves pending actions and skips individual prompts.
Consent controls which capabilities a widget may invoke. Approved scripts and shell commands are not confined to a per-widget Mac sandbox.
The three stages
- Define — use
register_actionor theactionsarray onpublish_widget. - Consent — the action stays pending until approved through the Mac prompt or the Auto-consent controls setting.
- Invoke — call
window.PREEN.trigger. The hub checks the session, action registration, consent, reported foreground widget, arguments, and rate limit.
1. Define the action (backend)
register_action binds an actionId to a capability:
| Capability | Shape | Runs |
|---|---|---|
launch-app | { "type": "launch-app", "app": "Music" } | open -a <app> |
run-script | { "type": "run-script", "path": "/usr/local/bin/volset.sh" } | the existing file via /bin/sh |
http-request | { "type": "http-request", "method": "POST", "url": "http://…" } | a call to the declared URL, with a 10-second request timeout |
raw-command | { "type": "raw-command", "command": "yamaha-ctl volset" } | an arbitrary shell command; flagged as advanced in the consent prompt |
Prefer a specific capability when it fits. For example:
// register_action
{
"widgetId": "wgt_abc123",
"actionId": "volset",
"capability": { "type": "run-script", "path": "/usr/local/bin/volset.sh" },
"description": "Set soundbar volume",
"argKeys": ["vol"]
}
The script must already exist on the Mac. publish_widget also accepts an
optional actions array with the same shape, minus the repeated widgetId.
2. Approve it on the Mac
With Auto-consent controls off, the Mac prompts with the widget’s declared capabilities, including a warning for raw shell commands. Built-in widgets with actions ask during installation; custom actions added through MCP also enter this approval flow.
Approval is tied to the widget’s HTML and its action definitions:
- Replacing custom widget HTML requires approval for the new code, even if its action definitions stay the same.
- Adding or changing actions beyond the approved set requires approval. Keeping the same code and identical actions, or removing actions, does not.
- Returning to an earlier version you already approved can reuse its grant.
- Shipped built-in template updates can retain approval for existing actions; additional or changed capabilities still require approval.
You can revoke actions from the widget’s entry in the Mac app. Revocation removes grants for earlier versions too; deleting the widget removes them as well. Turn Auto-consent controls off before revoking if you want the widget to remain unapproved: when enabled, that setting approves pending actions again.
3. Fire it (frontend)
button.addEventListener("click", () => PREEN.trigger("volset", { vol: 40 }));
The native app supplies the current widget id. trigger returns void, with
no Promise, execution result, or command output. To show the outcome, have a
producer push refreshed state through the data feed.
Use trigger in response to user interaction, but do not treat a tap as an
enforced authorization check: trigger is not touch-gated. The
requiresConfirmation action field is stored but currently does not cause a
per-invocation confirmation prompt. Haptics have a
separate physical-touch gate.
How arguments are passed
For process-based capabilities, invocation args arrive as a JSON string in
$PREEN_ARGS. Preen does not interpolate them into a command line. The
registered command or script path stays fixed; the payload varies. Scripts must
still parse and validate that data, quote values, and avoid evaluating arguments
as shell code.
# Inside volset.sh: reject missing, nonnumeric, or out-of-range volume.
VOL=$(printf '%s' "$PREEN_ARGS" | jq -er '.vol | numbers | select(. >= 0 and . <= 100)') || exit 1
# Pass "$VOL" as a quoted argument to your volume-control program.
args must be an object. If argKeys is declared, unexpected keys are rejected;
it does not require every listed key or validate value types and ranges.
The http-request capability uses only its registered URL and method. It does
not send invocation arguments as a body or provide custom request headers, and
its response is discarded. It is not a general-purpose API proxy. Use a
registered script when you need to construct a request and push resulting data
back to the widget.
trigger vs pulse vs haptic
| Call | Use | Runs on | Approval |
|---|---|---|---|
trigger(actionId, args?) | Invoke a registered capability | The Mac | Requires an approved capability |
pulse(detail?) | Animate this phone’s bird on the Mac | The Mac | No action approval |
haptic(kind) | Play a system haptic | The phone | No action approval; touch-gated |
All three work in release builds. Widget calls cross a native message-handler
bridge. The native app then makes authenticated HTTP requests for trigger and
pulse; haptic stays on the phone. The widget’s connect-src 'none' CSP
remains in force.
Enforcement on the Mac
The invoke endpoint enforces these checks in release builds:
- Authentication — a valid paired-device session is required.
- Allowlist — the action must be registered against the requested widget.
- Consent — the widget’s current HTML and action set must be covered by an approval, whether manually granted or created by Auto-consent controls.
- Foreground cross-check — if the phone has reported an open widget, the hub rejects an invocation naming a different widget. An absent report does not block it; this is not proof of visibility or a physical tap.
- Arguments and rate limit — object arguments, allowed keys when declared, and at least 0.5 seconds between accepted invocations per widget, shared across its actions.
What consent covers
Consent governs widget-triggered Mac actions. Publishing HTML or registering an action through the admin API cannot itself grant approval. Community widgets installed from the registry receive no Mac actions; the registry envelope does not carry capabilities. If you later add actions locally, they enter the same approval flow.
The widget web sandbox and the Mac execution boundary are different:
- Approved
run-scriptandraw-commandactions launch ordinary processes with the hub’s user permissions and inherited environment. Preen adds no per-widget filesystem or network sandbox, process timeout, or concurrency limit. The invocation rate limit does not stop an already running process. - A script approval identifies a path, not a hash of the file’s contents. Editing that file does not by itself trigger fresh Preen consent.
- Datastore reads/writes and the bird pulse deliberately require no action approval. Haptics stay on the phone and are touch-gated.
- A script, agent, cron job, or other program you install directly on the Mac runs under its own permissions. Preen’s action approval does not intercept it.
New Mac functionality initiated by a widget should be exposed as a declared action. The consent mechanism cannot guarantee that all code running elsewhere on the Mac passes through Preen.