RemixletDocs

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:

  1. the bridge token belongs to the remixlet;
  2. the call came from a page covered by the remixlet's matches;
  3. neither the remixlet nor its site is paused;
  4. the manifest declares the required capability;
  5. 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#

APIRequired capabilityAuthority
rmx.prefixNoneThe filled mark prefix, for code written before the id slot.
rmx.keepNoneCoordinates sandbox callbacks with page mutation notices.
rmx.relay.onnetwork:observe:<host-pattern>Subscribes to low-level relay messages already filtered for this sandbox. Without the grant the callback never fires.
rmx.navigation.onChangeNoneReports current-page URL changes.
rmx.log.warn, rmx.log.errorNoneWrites bounded diagnostic entries.
rmx.storage.*storageReads and writes this remixlet's extension-owned JSON store.
rmx.fetchfetch:<host-pattern>Sends credentialed HTTP requests to approved hosts.
rmx.network.onResponsenetwork:observe:<host-pattern>Reads textual responses the page receives from approved hosts.
rmx.notifications.*notificationsCreates and clears owned text notifications.
rmx.clipboard.writeTextclipboardWrites text to the clipboard.
rmx.menu.registermenuRegisters a command for the current tab and document.
rmx.schedule.*scheduleStores fixed scheduled actions and site-open hooks.

Runtime properties and callbacks#

rmx.prefix#

js
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#

js
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.

FieldTypeBehavior
labelstringRequired, 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;
  • apply writes to the page three times in a row and ensure is still false each time;
  • apply runs 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#

js
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#

js
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#

js
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.

CallReturnsDescription
await rmx.storage.get(key)JSON value or undefinedReads one key.
await rmx.storage.set(key, value)voidStores a JSON-compatible value.
await rmx.storage.delete(key)voidRemoves one key.
rmx.storage.watch(key, callback)() => voidLong-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.

js
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.

InputRule
urlAbsolute HTTP or HTTPS URL without embedded credentials.
methodGET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS.
headersAt most 32 KB. Browser-owned headers including cookie, origin, referer, user-agent, host, and content-length are refused.
bodyString, at most 1 MB. Refused for GET and HEAD.
timeoutMsDefaults to 15000. Maximum 30000.
redirectsMaximum five. Every target must match an approved fetch: pattern. Cross-origin redirects drop authorization.
response bodyText, 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:

MemberType
urlFinal URL string
statusNumber
statusTextString
oktrue for status 200 through 299
redirectedBoolean
headersHeaders
bodyResponse 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:.

js
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 fieldTypeDescription
urlstringAbsolute request URL.
methodstringUppercase request method.
statusnumberResponse status.
contentTypestring | nullResponse content type.
bodystringTextual response body, capped at 512 KB.
truncatedbooleanWhether the body exceeded the per-response cap.
seqnumberIncreasing 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.

CallReturnsDescription and limits
await rmx.notifications.show(title, message)Notification idCreates a text notification. Title maximum: 120 characters. Message maximum: 1000 characters. Both must be non-empty strings.
await rmx.notifications.clear(id)booleanClears 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.

js
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.

Requires menu.

js
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#

CallReturnsDescription
await rmx.schedule.register(definition)Stored scheduleRegisters { id, at, action } or { id, every, action }.
await rmx.schedule.at(id, when, action)Stored scheduleRegisters a one-shot schedule. when is epoch milliseconds or an ISO date string.
await rmx.schedule.every(id, minutes, action)Stored scheduleRegisters a repeating schedule. The minimum interval is 0.5 minutes.
await rmx.schedule.remove(id)booleanRemoves 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()voidRemoves 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:

ActionEffect 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#

CallReturnsDescription
await rmx.schedule.onSiteOpen(name)nameRegisters 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)() => voidRegisters the hook and a callback, then delivers matching queued hooks.
await rmx.schedule.removeOnSiteOpen(name)booleanRemoves 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)() => voidRegisters 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.