Sandbox APIs
rmx.* API reference
Every rmx call available to remixlet code, including signatures, capabilities, return values, limits, and worker security checks.
rmx is how remixlet code uses the extension: storage, network requests, notifications, the clipboard, menu commands, and schedules, plus a few helpers that need no permission. Calls that need a browser privilege leave the sandbox and run in the extension's background worker. Reading and changing the page is a separate API, dom.
Security model#
Every worker call carries the remixlet id and a secret bridge token attached by the sandbox host. The token never enters remixlet code or the page. Before a call runs, the worker checks, in this order:
- the bridge token belongs to the remixlet;
- the call came from a page covered by the remixlet's
matches; - neither the remixlet nor its site is paused;
- the manifest declares the required capability;
- the user approved that exact capability.
Some calls add their own checks: rmx.fetch checks every URL and redirect against its approved hosts, and rmx.menu works only from the top-level page, not from a frame.
rmx.log is the only worker lane that skips the page and pause checks, so a paused or off-site caller can still record a diagnostic. It still requires a valid bridge token.
A denied call rejects with an Error, and the same message goes to the remixlet log. The exception is rmx.menu, whose denials reject without a log entry. Argument validation failures reject without performing the operation.
Capability index#
| API | Required capability | Authority |
|---|---|---|
rmx.prefix | None | The filled mark prefix, for code written before the id slot. |
rmx.keep | None | Coordinates sandbox callbacks with page mutation notices. |
rmx.relay.on | network:observe:<host-pattern> | Subscribes to low-level relay messages already filtered for this sandbox. Without the grant the callback never fires. |
rmx.navigation.onChange | None | Reports current-page URL changes. |
rmx.log.warn, rmx.log.error | None | Writes bounded diagnostic entries. |
rmx.storage.* | storage | Reads and writes this remixlet's extension-owned JSON store. |
rmx.fetch | fetch:<host-pattern> | Sends credentialed HTTP requests to approved hosts. |
rmx.network.onResponse | network:observe:<host-pattern> | Reads textual responses the page receives from approved hosts. |
rmx.notifications.* | notifications | Creates and clears owned text notifications. |
rmx.clipboard.writeText | clipboard | Writes text to the clipboard. |
rmx.menu.register | menu | Registers a command for the current tab and document. |
rmx.schedule.* | schedule | Stores fixed scheduled actions and site-open hooks. |
Runtime properties and callbacks#
rmx.prefix#
const prefix = rmx.prefix;Read-only string in the form rmx-<remixlet-id>, or an empty string until the sandbox has started the remixlet. Attributes and classes added to page-owned elements carry this prefix on the page. New code does not read it: write marks as rmx--<name> (data-rmx--mix, rmx--on) and the page agent fills the id in, so the same spelling works in code, in style.css, and in verification selectors. Code written before the id slot interpolates rmx.prefix and still runs. See ownership marks.
rmx.keep#
const stop = rmx.keep(label, {
when,
ensure,
apply,
});Registers a condition that the runtime keeps true while the site redraws itself, and returns a synchronous unsubscribe function. Most remixlets are built around one or more keeps: ensure checks whether the page is already the way the remixlet wants it, and apply makes it so.
| Field | Type | Behavior |
|---|---|---|
label | string | Required, non-empty, trimmed to 120 characters for logging. |
when | () => value | Promise<value> | Optional. A falsey result idles this keep. |
ensure | () => value | Promise<value> | Required. A truthy result means no write is needed. |
apply | () => void | Promise<void> | Required. Runs when when passes and ensure is false. |
The runtime evaluates keeps at registration, after coalesced page mutations, after URL changes, and when the page removes a node listened to during the latest apply. Evaluations are serialized. A notice received during evaluation schedules one more pass.
After apply, the runtime runs ensure again. A keep halts, and writes an error to the log, when:
- callbacks throw three times in a row;
applywrites to the page three times in a row andensureis still false each time;applyruns 200 times in a row without writing anything, meaning the element it waits for never appeared.
An apply that writes nothing and leaves ensure false is not counted toward the three-strike rule: on a page that is still rendering, it usually means the element is not there yet. The runtime logs one informational entry the first time this happens. Only page-changing calls count as writes, click and setChecked included; reads, focus, blur, and the scroll calls do not.
If a keep applies 25 times within a fixed one-minute window, the runtime writes one informational entry, once for the life of the keep, and carries on.
rmx.navigation.onChange#
const stop = rmx.navigation.onChange(({ url, previousUrl }) => {});Registers a callback and returns a synchronous unsubscribe function. The callback may be asynchronous. It runs only when the runtime's last known URL changes.
The page agent detects Navigation API changes, popstate, and hashchange. The extension worker and page relay provide fallback hints. Each hint causes a comparison against the last known URL, so forged or duplicate events cannot invent a URL change.
rmx.relay.on#
const stop = rmx.relay.on(topic, (data) => {});Registers a low-level relay callback and returns a synchronous unsubscribe function. The built-in relay emits network:response and navigation:change. Sequenced records buffered before registration are replayed oldest first. Unsequenced navigation hints are live only.
The page agent attaches a sandbox to the relay only when that remixlet has an approved network:observe:<host-pattern> capability. It forwards response records only when their URL matches one of the approved patterns. Relay payloads are untrusted page data and carry no bridge token or extension authority. Use rmx.network.onResponse and rmx.navigation.onChange for their typed filtering.
rmx.log.warn and rmx.log.error#
rmx.log.warn(message);
rmx.log.error(message);Writes a string to the per-remixlet log. Messages are capped at 500 characters. Each remixlet keeps at most 300 entries and 256 KB; past that, old log and info entries go before warn and error entries. All remixlet logs share a 3 MB cap, and past that the oldest entry goes first whatever its level.
The sandbox also captures console.log, console.info, console.warn, console.error, uncaught exceptions, unhandled rejections, bridge denials, and page-agent refusals. Console capture keeps 99 lines per page load, then one line saying the rest were cut, and 500 characters per line.
The log is read by the agent, which uses it to find out why a remixlet misbehaved. The extension's own pages do not display it.
Storage#
Requires storage.
| Call | Returns | Description |
|---|---|---|
await rmx.storage.get(key) | JSON value or undefined | Reads one key. |
await rmx.storage.set(key, value) | void | Stores a JSON-compatible value. |
await rmx.storage.delete(key) | void | Removes one key. |
rmx.storage.watch(key, callback) | () => void | Long-polls for store revisions and calls back with the watched key's current value. |
The worker namespaces storage by remixlet id. All tabs and matched hosts for the same remixlet share it. Pages cannot access it. Values may contain strings, numbers, booleans, null, arrays, and plain objects.
watch reacts to any revision of the remixlet's store, including a change to another key. The callback runs only when the revision changes. If the service worker stops, the sandbox retries the long-poll after one second. The unsubscribe function stops future polls.
Deleting a remixlet removes its store. Serialized writes cannot recreate storage after deletion begins.
Fetch#
Requires an approved fetch:host capability that covers the initial URL and every redirect.
const response = await rmx.fetch(url, {
method,
headers,
body,
timeoutMs,
});The worker sends the request with the user's cookies for the target host. Page CORS rules do not apply.
| Input | Rule |
|---|---|
url | Absolute HTTP or HTTPS URL without embedded credentials. |
method | GET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS. |
headers | At most 32 KB. Browser-owned headers including cookie, origin, referer, user-agent, host, and content-length are refused. |
body | String, at most 1 MB. Refused for GET and HEAD. |
timeoutMs | Defaults to 15000. Maximum 30000. |
| redirects | Maximum five. Every target must match an approved fetch: pattern. Cross-origin redirects drop authorization. |
| response body | Text, at most 2 MB. A larger response rejects. |
An exact pattern covers one host. fetch:api.example.com does not cover www.example.com. fetch:*.example.com covers the apex and all subdomains.
The result is a frozen RmxResponse, not a browser Response:
| Member | Type |
|---|---|
url | Final URL string |
status | Number |
statusText | String |
ok | true for status 200 through 299 |
redirected | Boolean |
headers | Headers |
body | Response text |
await text() | Resolves to the response text |
await json() | Parses the response text as JSON |
The worker removes set-cookie and set-cookie2 from returned headers.
Network response observation#
Requires an approved network:observe:host capability. The exact and wildcard host grammar matches fetch:.
const stop = rmx.network.onResponse(hostPattern, (entry) => {});Registers a callback and returns a synchronous unsubscribe function. hostPattern may be an exact host, a *. host, an empty string for all approved hosts, or a URL substring that narrows the approved records.
| Entry field | Type | Description |
|---|---|---|
url | string | Absolute request URL. |
method | string | Uppercase request method. |
status | number | Response status. |
contentType | string | null | Response content type. |
body | string | Textual response body, capped at 512 KB. |
truncated | boolean | Whether the body exceeded the per-response cap. |
seq | number | Increasing sequence number for this page. |
The extension relay wraps page fetch and XMLHttpRequest at document start. It reads a clone and leaves the page's response unchanged. It records text, JSON, XML, and JavaScript response types. The ring holds at most 250 entries and 6 MB, evicting the oldest first. Buffered records are replayed before live records and deduplicated by seq.
The page agent receives approved host patterns from stored grants, not from the callback argument. It filters every record before sending it to the sandbox. A network:observe: grant does not permit rmx.fetch or admit new URLs through dom.*.
Notifications#
Requires notifications.
| Call | Returns | Description and limits |
|---|---|---|
await rmx.notifications.show(title, message) | Notification id | Creates a text notification. Title maximum: 120 characters. Message maximum: 1000 characters. Both must be non-empty strings. |
await rmx.notifications.clear(id) | boolean | Clears a notification created by this remixlet. |
Notification ids are unguessable handles. The worker verifies ownership before clearing one. The API has no icon, button, or URL fields.
Clipboard#
Requires clipboard.
await rmx.clipboard.writeText(text);Writes a string of at most 1 MB of UTF-8 to the clipboard. The API has no clipboard read operation.
Menu commands#
Requires menu.
const registrationId = await rmx.menu.register(id, label, callback);Registers a command in the Remixlet toolbar popup for the current tab and document. id may contain at most 128 characters. label may contain at most 160. Registering the same id again replaces the earlier command.
The worker stores command metadata and pending invocation ids. The callback remains inside the sandbox. A long-poll delivers clicks to the current document and acknowledges them after the callback settles. The command disappears when the document, tab, remixlet, grant, or matching active site no longer qualifies.
Schedules#
Requires schedule.
Timed schedules#
| Call | Returns | Description |
|---|---|---|
await rmx.schedule.register(definition) | Stored schedule | Registers { id, at, action } or { id, every, action }. |
await rmx.schedule.at(id, when, action) | Stored schedule | Registers a one-shot schedule. when is epoch milliseconds or an ISO date string. |
await rmx.schedule.every(id, minutes, action) | Stored schedule | Registers a repeating schedule. The minimum interval is 0.5 minutes. |
await rmx.schedule.remove(id) | boolean | Removes an owned timed schedule. |
await rmx.schedule.list() | { timed, onSiteOpen } | Lists this remixlet's timed schedules and site-open hook names. See below. |
await rmx.schedule.clear() | void | Removes timed schedules, site-open hooks, queued hooks, session markers, and browser alarms. |
Ids and hook names may contain letters, digits, _, ., :, and -, must start with a letter or digit, and may contain at most 64 characters.
The worker stores data, not remixlet code. Each schedule uses one fixed action:
| Action | Effect and security |
|---|---|
{ type: "notify", title, message } | Shows an owned text notification with the notification length limits. |
{ type: "open", url } | Focuses an existing exact-URL tab or opens one. The URL must be absolute HTTP or HTTPS and covered by the remixlet's matches. |
{ type: "hook", name } | Queues a hook for the next matching page sandbox. |
register also accepts name as an alias for id, plus one shorthand action field: notify: { title, message }, open: url, open: { url }, or hook: name. Supplying action together with a shorthand action rejects as ambiguous.
Timers can fire without an open matching tab. One-shot schedules are removed after firing. Missed timers run once after browser startup; repeating schedules then move to their next future time.
Pausing the remixlet or its site drops scheduled actions rather than deferring them. A repeating schedule keeps its timing and does nothing until you resume. A one-shot schedule that comes due during a pause is removed without running.
list() resolves to { timed, onSiteOpen }, with both arrays empty when nothing is registered. timed holds this remixlet's timed schedules, sorted by id, each as { id, trigger, at?, everyMinutes?, nextRunAt, action }: trigger is "at" or "every", at is set for a one-shot schedule and everyMinutes for a repeating one, and nextRunAt is epoch milliseconds. onSiteOpen holds its site-open hook names, sorted.
Site-open and hook delivery#
| Call | Returns | Description |
|---|---|---|
await rmx.schedule.onSiteOpen(name) | name | Registers a hook that queues once per browser session, when a matching site first opens. If the registering page itself matches and the hook has not fired this session, it queues at once. |
await rmx.schedule.onSiteOpen(name, callback) | () => void | Registers the hook and a callback, then delivers matching queued hooks. |
await rmx.schedule.removeOnSiteOpen(name) | boolean | Removes the registration and queued instances of that hook. |
await rmx.schedule.consumeHooks() | string[] | Atomically consumes all queued hook names for this remixlet. Only a matching page may consume them. |
rmx.schedule.onHook(name, callback) | () => void | Registers a callback and delivers queued instances of that name. |
Each hook callback receives { name }. A callback registered with onHook is delivered each matching queued instance once for that callback. The returned function unsubscribes it.
A page consumes its queued hooks once. The first consumeHooks(), onHook, or onSiteOpen(name, callback) on a page takes the queue, and later calls on the same page reuse that result. Hooks queued after that are delivered on the next matching page load.
Compatibility#
dom.* and rmx.* share a versioned, add-only contract. Within one bridge version, methods and fields may be added but existing behavior is not renamed, removed, or changed. A breaking change raises the version.
The extension stamps the current bridge version into remixlet.json under builtWith.bridge. If a stored remixlet's stamp falls outside the installed extension's supported range, the extension does not run it, and shows it as "Not running" until a chat updates it. The current bridge version is 2, and version 2 is also the oldest supported.