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
secretRefsetting 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
filePathsetting 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.openaccepts 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.
Install consent narrows the grant
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
controlpermission 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 nosettings.setin 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.