Concepts

How the sandbox works

The layers that keep widgets contained, why widget identity comes from the frame and not the payload, and what install-time grants mean.

Widgets are third-party code running on someone's desktop, so DeskDash is designed as if every widget might be hostile. Not because yours is, but because the guarantees users rely on have to hold for the worst widget on the marketplace, not the average one. This page explains the layers, from the frame outward, and what they mean for what your code can and cannot do.

The frame

Every widget document runs in a sandboxed iframe that allows scripts and nothing else. Without same-origin privileges, the frame gets an opaque origin: no reaching into the surrounding app's DOM, no shared cookies or storage with anything, and on modern runtimes its own renderer process. The app's internal command channel is unusable from widget frames, so there is no path from widget JavaScript to the host application, the filesystem, or the OS. What is left inside the frame is a plain web page that can compute and render.

The network

Every response served to a widget document carries this content security policy:

default-src 'none';
script-src http://widget.localhost;
style-src http://widget.localhost 'unsafe-inline';
img-src http://widget.localhost data:;
font-src http://widget.localhost data:;
connect-src 'none'; frame-src 'none'; object-src 'none';
base-uri 'none'; form-action 'none'

connect-src 'none' is the load-bearing line: no fetch, no XHR, no WebSockets, from any widget, ever. Scripts load only from the widget's own folder (so no remote code, no inline script, no eval), images and fonts only from the folder or data: URIs, and no nested frames. The only network egress is dd.http.fetch, which runs host-side against the exact hostnames the manifest declared and the user consented to, re-validated on every redirect hop.

Identity and the bridge

Everything a widget asks of the system travels as a message to the shell, and the shell knows who is asking from which frame sent the message, never from anything inside it. Ids in a payload are ignored as identity; a widget cannot claim to be another widget, because identity is not a claim.

On the other side of that channel, every call is re-validated in the host: the calling widget's granted permissions are checked, inputs are validated, and failures come back as typed errors. The SDK is convenience, not enforcement; the checks live host-side, so nothing a widget does in JavaScript can skip them.

User-granted, never widget-chosen

The pattern that repeats across every sensitive capability: the widget names things, the user supplies them, the host resolves them.

  • A secretRef setting names a credential. The user picks which stored secret it means, the host injects the value into the outgoing request after validating it, and the value itself never enters widget code, logs, or error messages.
  • A filePath setting is a path only the user can fill in. Manifest defaults for these fields are deliberately ignored, so no widget can ship pre-pointed at anyone's files.
  • Desktop and folder items are opaque ids issued by the host's own enumeration. A widget can open what the user put on their desktop and nothing else; it never handles a real path.
  • Wifi connect works only for networks Windows already has a profile for; passwords never cross the bridge. System settings pages open by whitelisted key, never by arbitrary target.
  • links.open accepts only URLs starting with a manifest-declared prefix, a host-side denylist of dangerous schemes applies no matter what the manifest says, and the call only works while the user's cursor is inside the widget.

At install time the user sees every requested permission in plain language, with the http hosts and links prefixes spelled out item by item, and can withhold any of them. What they confirm is intersected with the manifest and enforced from then on; a withheld capability behaves exactly as if the manifest never declared it. The intersection only ever narrows (no stored state can escalate past the manifest), a corrupted grant record fails closed to deny-all, and an update that requests something new re-prompts for exactly the additions.

Design consequence for authors: any call can come back PERMISSION_DENIED even though your manifest declares it, because the user said no to that part. Degrade the feature, not the widget.

Input, popouts, and dialogs

Mouse capture is decided by the host from where the user placed your widget. A widget can narrow its capture area (that is what region marking is), but it can never extend it past its own rect. Cursor events stop at your own bounds too: no widget observes the cursor anywhere else on the desktop.

The sanctioned ways to reach past your rect are all consent-shaped: popouts and host dialogs open only on a real user gesture inside the widget, are size-capped by the manifest, and are force-closed by the host the moment context changes. Dialogs go further: the window is drawn by the host from host data, and the only thing that returns to the widget is the shaped answer.

Between widgets

Widgets are mutually invisible. There is no widget-to-widget channel, no shared storage, no way to enumerate what else is running. The two narrow exceptions both run through the user:

  • The control permission works only on instances the user explicitly ticked in that widget's settings, and instance settings are writable only through the host's own dialog. There is deliberately no settings.set in the SDK; adding one would let widgets approve themselves.
  • Peer drag-and-drop works only between instances of the same widget type, the consent is the physical drag gesture itself, and the payload is carried and delivered by the host, not the DOM.

A hidden widget unmounts entirely, so nothing invisible keeps running or listening.

What this means for you

Practically, the model gives you three habits:

  • Ask for the minimum. Grants are visible, itemized, and refusable; small permission lists install better and review faster.
  • Design for denial. Any gated call can reject; a native-feeling widget loses a feature, not its composure.
  • Never route around the SDK. There is no other door, and anything that looks like one is a bug worth reporting.

The flip side is what you gain: users can install your widget without trusting you very much, and that is precisely what makes them willing to install it.