SDK

Interactive actions

Register Mac capabilities, approve them, and invoke them through the widget SDK.

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

  1. Define — use register_action or the actions array on publish_widget.
  2. Consent — the action stays pending until approved through the Mac prompt or the Auto-consent controls setting.
  3. 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:

CapabilityShapeRuns
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

CallUseRuns onApproval
trigger(actionId, args?)Invoke a registered capabilityThe MacRequires an approved capability
pulse(detail?)Animate this phone’s bird on the MacThe MacNo action approval
haptic(kind)Play a system hapticThe phoneNo 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.

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-script and raw-command actions 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.