> For the complete documentation index, see [llms.txt](https://docs.getvenora.tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.getvenora.tech/plugin-api/plugin-security.md).

# Plugin security requirements

Part of the [Community Plugin API specification](/plugin-api.md).

These requirements are **normative and mandatory**. Every Venora release, every capability, and every event MUST meet them, including every later addition. A change that breaks one is a security bug. It MUST NOT be released, even when the change is a convenient fix for something else.

Imported plugin code and manifests are treated as **untrusted**. Bundled plugins get no exceptions. The trust boundary is the plugin's sandboxed frame together with its message channel:

* inside the frame, the plugin controls everything;
* outside it, Venora validates everything that crosses.

## Requirements

### SEC-1 Session token

No API, capability, event, setting, storage operation, error, lifecycle message, or debug output sent to plugin code may contain the raw Fermo/Venora session token, in whole or as part of a longer string. That includes:

* returning it;
* exposing it;
* serializing it;
* forwarding it;
* providing it in any other way.
* **Applies to every Fermo account** in the browser profile, not only the active one.
* **Two layers of enforcement:**
  1. Capabilities MUST return only the data plugins need. A capability that uses the token, such as the avatar lookup, uses it inside Venora and returns only a derived result.
  2. As a central backstop, every message Venora sends to a plugin after startup passes through one function, `post()` in `runtime/host.js`. It checks the message against the current tokens. A response containing a token becomes `sensitive_data_blocked`, an event containing one is dropped, and if the tokens cannot be read, nothing is sent.
* **The guard is not a licence.** Capabilities MUST NOT rely on it. It is a backstop, and a capability that would trigger it is itself a bug.
* **The token is never logged** by Venora's guard.
* **Tests:**
  * in `tests/runtime-security.test.js`, every `token guard:` test, covering Plugin → Capability → Response, Venora → Event → Plugin, settings, and the rule that only trusted services read the token;
  * the browser test checks that the token appears 0 times in all plugin and Venora output.

### SEC-2 No direct access

Plugin code MUST NOT be able to access any of the following directly:

| Resource                                                                        | Enforced by                                                                                                                   |
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Fermo's `localStorage`, `sessionStorage`, IndexedDB, and cookies                | Plugin code runs in a dedicated worker with an opaque origin                                                                  |
| Fermo's page DOM, Venora's UI, `window`, `document`, and the frame's `location` | The worker has no DOM, no window, and no navigable location. Page changes happen only through capabilities and validated CSS. |
| Navigation as a way out (sending data to another server in a URL)               | Plugin code cannot navigate anything: the worker has no frame or window to navigate                                           |
| Extension APIs and extension storage (Venora's internal state)                  | Sandboxed extension page; no `chrome.*` or `browser.*`                                                                        |
| Privileged DOM state, such as other frames or the runtime frame's nonce         | Frames inside a closed shadow root. The nonce is accepted once, from the parent only.                                         |
| The network                                                                     | The sandbox CSP `default-src 'none'`. Venora makes requests only inside capabilities that require `network`.                  |
| Another plugin, its frame, settings, storage, or subscriptions                  | One frame and one channel per plugin. All data is keyed by the bound plugin ID.                                               |

Tests:

* `tests/runtime.test.js`, sandbox and policy tests;
* `tests/plugin-ui.test.js`, the `worker:` and `sandbox page:` tests;
* browser test:
  * "Sandbox has no extension APIs / cannot read … / network access is blocked";
  * "Plugin code has no window, document, …";
  * "A plugin cannot navigate its frame";
  * "No plugin request or navigation ever reaches another server".

### SEC-3 Capabilities are the only path

* **One path.** Every privileged operation MUST go through the capability dispatcher.
* **Fixed checks.** The dispatcher checks that the capability exists, then every required permission, then the payload, in that order. Resource limits can refuse a request with `rate_limited` before these checks, and a capability's own rate limit after them (SEC-12).
* **No side channels.** No other message type may cause a privileged action.
* **Fixed registry.** Capabilities MUST be listed in the capability registry, which is fixed at load. Lookups use own properties only, so names such as `toString` or `__proto__` resolve to nothing.
* **Permissions are fixed per run.** A plugin's permissions are copied from its validated record when it starts. Nothing the plugin sends can change them.
* **Generic internal errors.** Internal exceptions are returned only as a generic `capability_failed`.

Tests: `tests/runtime.test.js`, "capability dispatch checks existence, permissions, and payloads…" and "permissions are fixed at start…".

### SEC-4 Plugin identity

* **Bound by Venora.** The plugin ID used by every capability and event MUST be the ID Venora bound to the plugin's channel when it started it.
* **Never from the plugin.** Requests MUST NOT carry a plugin ID. A request with fields outside the protocol is rejected.
* **Out of reach.** The channel is a private `MessagePort` transferred once, with a random nonce, to the plugin's own frame. Other plugins and Fermo's page cannot obtain it.

Tests: `tests/runtime.test.js`, "plugin identity comes from the channel…" and "the sandbox accepts only one connect message…".

### SEC-5 Event isolation

A plugin MUST receive an event only if all three of these hold:

* the event is in the public event registry;
* the plugin declared every permission the event requires;
* the plugin subscribed to it.

Further rules:

* **Own subscriptions only.** Subscriptions belong to one plugin instance, and no plugin can add, remove, or observe another's.
* **Scoped events.** Events about one plugin, such as `settings.changed`, MUST go only to that plugin.
* **No internal events.** Internal Venora events MUST NOT reach plugins.

Tests: every `events:` test in `tests/runtime-security.test.js`.

### SEC-6 Payload safety

Everything that crosses the bridge in either direction MUST be plain, JSON-compatible data within the limits, and both sides validate it.

* **No privileged objects.** Capabilities and events MUST NOT give plugins:
  * DOM nodes;
  * functions;
  * extension objects;
  * URLs that grant access, such as extension URLs or signed URLs;
  * any internal object.
* **Minimal results.** Results SHOULD contain only what the plugin needs. For example, `mentions.showAvatar` returns `{shown}`, not the avatar URL.

Tests: `tests/runtime.test.js`, "only JSON-compatible data can cross the bridge" and "plugins cannot send functions…"; `tests/runtime-security.test.js`, "events: payloads must be plain data…" and "token guard: the real capabilities return only sanitized results…".

### SEC-7 Settings and storage scoping

* **Own data only.** A plugin MUST NOT be able to read, write, list, detect, or receive events about another plugin's settings or storage.
* **No collisions.** No key chosen by a plugin may reach Venora's or Fermo's data.

See [plugin-settings-storage.md](/plugin-api/plugin-settings-storage.md#scoping-one-plugin-its-own-data).

Tests: `tests/runtime-security.test.js`, "events: targeted events reach only the owning plugin…"; `tests/plugin-api.test.js`, "storage: plugins are isolated…", "settings: venora.settings.get reads only the calling plugin's settings…", and the `hostile:` tests; browser test, "A hostile plugin cannot read another plugin's storage, even by naming it".

### SEC-8 Errors do not leak

Error responses MUST follow [plugin-api.md §8.3](/plugin-api.md#83-what-errors-must-never-contain) and pass through the session-token guard.

### SEC-9 Installation is validated and consented

* **Validated before storage.** An imported plugin MUST be fully validated before anything is stored.
* **Unknown permissions block installation.**
* **Sensitive permissions need explicit consent.** The user MUST explicitly confirm sensitive permissions at install and at every update that declares them. A caller that cannot ask the user MUST fail closed. Bundled plugins are not installed this way. Their permissions ship with Venora and are shown in the information dialog (see [plugin-permissions.md](/plugin-api/plugin-permissions.md#fixed-rules)).
* **Reserved manifest fields are never taken from an import.** These are `required`, `enabled`, `imported`, `script`, `style`, and `moduleUrl`.

Tests: `tests/plugin-install.test.js`; browser test, "Sensitive permissions show a warning…".

### SEC-10 Plugins cannot impersonate or alter Venora's UI

* **Attributed notices.** Text an imported plugin shows the user MUST be attributed to that plugin. Notices carry a fixed "Plugin" badge and the plugin's cleaned name, in a style that is not Venora's.
* **Names read as written.** Plugin names MUST NOT contain control or text-direction characters, and are at most 64 characters at install. Venora also removes those characters when it shows any name.
* **Text only.** Plugin-controlled strings MUST be rendered as text, never as HTML. This covers names, descriptions, authors, setting labels and descriptions, changelogs, notices, command labels and descriptions, and errors. Venora never uses the browser's own `alert`, `confirm`, or `prompt` for them.
* **Commands stay on the plugin's own card.** Command labels follow the name rules (no control or text-direction characters, at most 48 characters), and a command button is shown only inside the card of the plugin that registered it. Only the user can run a command; no capability runs one, and an invocation reaches only the plugin that owns the command (SEC-5).
* **Isolated, top-layer UI.** Venora's UI MUST NOT be reachable by page or plugin CSS:
  * shadow-root rendering;
  * inline `!important` host styles;
  * no page CSS variables;
  * dialogs and notices in the browser's top layer.
* **Validated CSS.** Plugin CSS MUST NOT load resources (`url()`, `@import`, escapes) and MUST NOT write text (quoted strings other than empty ones, `@counter-style`).
* **No API for Venora's UI.** There is no API that renders HTML, opens Venora dialogs or prompts, or returns DOM objects.

Tests:

* `tests/plugin-ui.test.js`, the `names:`, `css:`, and `trusted UI:` tests;
* `tests/plugin-commands.test.js`;
* `tests/plugin-install.test.js`;
* browser test:
  * the injector plugin checks (HTML in every plugin string, CSS targeting all of Venora's UI, a plugin named "Venora");
  * "Plugin CSS that writes text onto the page is not applied";
  * "The warning is a modal dialog in the top layer…".

### SEC-11 Plugins act only on Fermo's own page

* **Only the top-level page.** Plugins MUST run, and capabilities MUST act, only on Fermo's own top-level page at `https://fermo.sbar.fyi`.
* **Never in frames or other origins.** Venora MUST NOT run in a framed copy of Fermo, whether framed by another site or by Fermo itself, nor on any other origin, even a page that imitates Fermo's DOM. The manifest does not inject into frames, and the content script checks `window.top` and the origin again.
* **Context is checked on every use.** The runtime host checks the page context before every start, request, and event. Leaving `/channels/` or replacing Fermo's document ends it. Requests are then refused with `runtime_stopped`, events are not sent, and the plugins are stopped.
* **Re-checked after waiting.** Operations that wait, such as the avatar lookup, check again before they change the page.

Tests:

* `tests/plugin-ui.test.js`, the `context:` tests;
* `tests/extension-scripts.test.js`, the framing, origin, and replaced-document tests;
* browser test:
  * "Fermo framed inside Fermo…";
  * "Fermo framed by another site…";
  * "A page on another origin that imitates Fermo…";
  * "After Fermo's document is replaced…".

### SEC-12 One plugin cannot exhaust Venora or another plugin

* **Separate accounts.** Each running plugin MUST have its own resource account, tied to the plugin bound to its channel, so a plugin cannot use, observe, or exhaust another plugin's limits.
* **What the account limits.** It caps the plugin's:
  * requests waiting for an answer;
  * general request rate;
  * rate of each costly capability;
  * event rate.
* **Over the limit.** Requests over a limit are refused with `rate_limited` before any work is done. Events over the limit are dropped.
* **No unbounded queues.** Every queue a plugin can fill (pending requests, storage operations, avatar lookups) MUST be bounded by these limits.
* **Bounded messages.** Every message a plugin sends MUST be bounded in total size. A plugin that sends a larger one MUST be stopped.
* **Unresponsive plugins.** A plugin whose worker stops answering MUST be stopped, and its frame and worker removed. A capability that never answers MUST NOT hold a plugin's request slot forever.
* **Repeated problems.** A plugin that keeps misbehaving MUST NOT be restarted indefinitely on the same page.
* **Kept inside Venora.** Resource accounting MUST stay inside the extension.

The limits are listed in [plugin-api.md §12](/plugin-api.md#12-resource-limits).

Tests:

* `tests/plugin-resources.test.js`;
* browser test:
  * "A plugin stuck in a loop is stopped and its worker is gone";
  * "Other plugins keep working…";
  * "The flooding plugin's card…".

### SEC-13 Recovery from a hostile or broken plugin

* **A way to recover that plugins cannot reach.** Venora MUST keep a way to stop any imported plugin that no plugin can hide.
* **The toolbar popup provides it.** It is an extension page, outside Fermo's page, so no plugin CSS applies to it. It offers:
  * safe mode, which stops all imported plugins;
  * enable and disable for each plugin;
  * uninstall for imported plugins.
* **Changes apply at once.** Each change MUST take effect in open Fermo tabs straight away: running plugins stop, and their CSS is removed, without a reload.

Tests:

* `tests/plugin-resources.test.js`, the `management:` and `popup:` tests;
* browser test:
  * "Hostile CSS can hide Fermo's settings…";
  * "Safe mode from the popup stops the hostile CSS…";
  * "The popup uninstalls the hostile plugins".

## Known limitations

These are accepted risks, documented in [runtime-architecture.md](/runtime-architecture.md#known-limitations):

* **CPU and memory:** browsers give extensions no way to cap one worker's CPU time or memory.
  * Venora stops plugins that stop responding.
  * It bounds what a plugin can make Venora do or hold (SEC-12).
  * A plugin that stays responsive can still use CPU and memory inside its own worker.
* **Plugin CSS and Fermo's page:** plugin CSS (with `ui`) can restyle or hide parts of Fermo's own page, including the settings area that holds the Plugins page. This is what styling Fermo means, so it is not prevented.
  * Plugin CSS cannot reach Venora's dialogs, notices, or the toolbar popup.
  * CSS cannot load resources or write text.
  * The popup's safe mode removes any imported plugin's CSS at once (SEC-13).

Frame navigation is no longer a limitation: plugin code runs in a worker, which cannot navigate. Any capability that would return user data beyond public IDs MUST still be reviewed against SEC-1, SEC-6, and SEC-11 before it is added.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.getvenora.tech/plugin-api/plugin-security.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
