> 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.md).

# Venora Community Plugin API, version 1

**Status:** version 1 is fully implemented. It was implemented in Phase 3.2; Phases 3.3 and 3.4 secured its UI boundary and added resource limits without adding API. Phase 3.6 added `venora.commands` and the `commands.invoked` event (Venora 0.2.0), which version 1 allows as additions (section 9.3). Each capability and event below is marked **Implemented**, **Planned**, or **Diagnostic**, and [plugin-api-status.md](/plugin-api-status.md) lists what is left for later phases.

This document is the contract between plugins and Venora. You can write a plugin from it without reading Venora's source code.

The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are used as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). Requirements written this way are part of the specification. A Venora build that breaks one has a bug.

| Document                                                             | Covers                                                                                                                     |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **plugin-api.md** (this file)                                        | Architecture, the `venora` object, conventions, capability and event reference, errors, versioning, naming rules, browsers |
| [plugin-manifest.md](/plugin-api/plugin-manifest.md)                 | Every `plugin.json` field, and ZIP packaging                                                                               |
| [plugin-permissions.md](/plugin-api/plugin-permissions.md)           | Permissions, and how they map to capabilities and events                                                                   |
| [plugin-lifecycle.md](/plugin-api/plugin-lifecycle.md)               | When plugin code runs, cleanup, failures, install/update/uninstall                                                         |
| [plugin-settings-storage.md](/plugin-api/plugin-settings-storage.md) | Plugin settings and plugin-scoped storage                                                                                  |
| [plugin-security.md](/plugin-api/plugin-security.md)                 | Mandatory security requirements, including session-token protection                                                        |
| [plugin-api-status.md](/plugin-api-status.md)                        | What is implemented, and what is left for later phases                                                                     |
| [runtime-architecture.md](/runtime-architecture.md)                  | How the sandbox and message bridge are built (internals)                                                                   |

## 1. Architecture

```
Plugin code (ES module, untrusted)
  │  venora.ui.toast({...})                 namespaced method
  │  = venora.request("ui.toast", {...})    the same call, at the lowest level
  ▼
venora object          dedicated worker in a sandboxed frame: opaque origin, no extension APIs, no network
  ▼
Message bridge         private MessagePort; plain data only; plugin ID bound to the port
  ▼
Capability dispatch    capability exists? → every required permission declared? → payload valid?
  ▼
Trusted services       Venora's content script: settings, storage, page changes, Fermo account
  ▼
Session-token guard    every response and event to the plugin is checked before it is sent
```

The design rests on five rules.

1. **The `venora` object is the plugin's only interface.** Plugins cannot touch Fermo's page, Venora's internals, extension APIs, other plugins, or the network. See [plugin-security.md](/plugin-api/plugin-security.md).
2. **Every privileged operation is a capability.** A capability is a named operation. It declares the permissions it requires, validates its input, and returns plain data.
3. **One method maps to one capability.** `venora.<namespace>.<method>(payload)` is the same as `venora.request("<namespace>.<method>", payload)`. Each capability's permissions, input, output, and errors are defined once, in section 5.
4. **Only copies of plain data cross the bridge.** No functions, DOM nodes, class instances, or references to Venora objects ever reach a plugin.
5. **Venora assigns identity.** Venora decides which plugin made a request from the channel the request arrived on, never from the request's contents.

Version 1 is deliberately small. Each namespace exists because a real plugin needs it, and anything that can be built from existing capabilities is left out.

| Namespace         | Purpose                                            | Permissions                                     |
| ----------------- | -------------------------------------------------- | ----------------------------------------------- |
| `venora.ui`       | Short text notices on the Fermo page               | `ui`                                            |
| `venora.settings` | Read the plugin's own user-configurable settings   | `storage`                                       |
| `venora.storage`  | The plugin's own private key–value data            | `storage`                                       |
| `venora.mentions` | Decorate user mentions on the page with avatars    | `ui`; avatars also need `account` and `network` |
| `venora.commands` | Named actions the user runs from the plugin's card | `ui`                                            |
| `venora.on`       | Subscribe to Venora events                         | Set per event                                   |

Not in version 1: notifications other than toasts, account data, general network access, DOM access, and inter-plugin messaging. These names are reserved for possible future use (see [section 10](#10-naming-and-stability-rules)). Network and account access were reviewed in Phase 3.6 and deliberately left out; see [plugin-network-account-review.md](/plugin-api/plugin-network-account-review.md). Plugin CSS is supported through the manifest, not through a method (see [section 5.5](#55-plugin-css)).

## 2. A minimal plugin

```js
// index.js — the entry script named by "main" in plugin.json
export default async (venora) => {
  const greeting = await venora.settings.get({key: "greeting"});
  await venora.ui.toast({message: greeting});

  const unsubscribe = await venora.on("settings.changed", ({key, value}) => {
    if (key === "greeting") venora.ui.toast({message: value});
  });

  return () => {
    // Cleanup: clear your own timers. Venora removes anything it added to the
    // page for this plugin, and ends its event subscriptions, automatically.
    unsubscribe();
  };
};
```

```json
{
  "id": "greeter",
  "name": "Greeter",
  "version": "1.0.0",
  "main": "index.js",
  "permissions": ["storage", "ui"],
  "compatibility": {"apiVersion": 1, "minVenoraVersion": "0.1.0"},
  "settings": [{"key": "greeting", "type": "string", "default": "Hello!"}]
}
```

Every namespaced method has an exact `venora.request` equivalent, for example `venora.request("settings.get", {key: "greeting"})`. Plugins written for the Phase 2.5 runtime, which only had `venora.request`, keep working unchanged.

## 3. The `venora` object

The plugin's default export is called with one argument, `venora`. Venora creates a new `venora` object each time the plugin starts.

* The object and its namespace objects are frozen.
* It holds no references to Venora, the page, or other plugins.
* It is not shared with any other plugin.

| Member                                              | Type                          | Status      | Description                                                                                         |
| --------------------------------------------------- | ----------------------------- | ----------- | --------------------------------------------------------------------------------------------------- |
| `pluginId`                                          | string                        | Implemented | The plugin's `id` from `plugin.json`. Informational: Venora never trusts an ID sent by a plugin.    |
| `apiVersion`                                        | number                        | Implemented | The Plugin API version this `venora` object implements. It is `1` for every plugin in this version. |
| `protocolVersion`                                   | number                        | Implemented | Version of the internal message bridge. **Not** the API version; plugins SHOULD NOT use it.         |
| `request(capability, payload?)`                     | function → Promise            | Implemented | Calls any capability by name. `payload` defaults to `{}`.                                           |
| `on(event, listener)`                               | function → Promise\<function> | Implemented | Subscribes to an event; see [section 6](#6-events).                                                 |
| `ui`, `settings`, `storage`, `mentions`, `commands` | frozen objects of methods     | Implemented | Namespaced methods; see [section 5](#5-capability-reference).                                       |

`venora.request` is the stable low-level layer, and the namespaced methods are thin wrappers around it. Both are part of the API, and they MUST behave the same for the same capability.

## 4. Conventions

### 4.1 Arguments

Every method takes **at most one argument: a plain object**. Its fields are the capability's payload, and an omitted argument means `{}`.

Venora uses an object, not positional arguments, so that adding an optional field later does not change existing calls.

Fields that a method does not define are ignored. In particular, a payload cannot name a plugin: fields such as `pluginId` have no effect.

Payloads MUST be plain data:

* allowed: `null`, booleans, finite numbers, strings, arrays, and plain objects;
* not allowed: functions, class instances (`Date`, `Map`, `Error`, …), `undefined` anywhere (including an object field set to `undefined`, such as `{durationMs: undefined}`; leave the field out instead), or cycles;
* size limits: depth at most 16, strings at most 100,000 characters, and at most 1,000 entries in any array or object. A whole message is also bounded ([section 12.1](#121-enforced-limits)).

### 4.2 Return values

Every method returns a **Promise**. There are no synchronous API calls, because every call crosses the bridge.

* On success, the Promise resolves with a **copy** of plain data. Changing it has no effect on Venora.
* Methods with nothing to report resolve with `null`. Plugins SHOULD NOT depend on that value.
* Results are objects, so that a field can be added later without breaking plugins (for example `{shown: true}`, not `true`). The exceptions are `settings.get` and `storage.get`, which return the stored value itself.

### 4.3 Async behaviour and ordering

* A request that gets no response within **30 seconds** rejects with `timeout`. The operation may still have taken effect.
* Venora MAY process concurrent requests in parallel, so responses can arrive in a different order from requests. Await a request before starting one that depends on it.
* The exception is storage: `storage.*` operations from one plugin MUST be applied in the order they were requested (see [plugin-settings-storage.md](/plugin-api/plugin-settings-storage.md#storage-semantics)).
* Messages from Venora to one plugin (responses and events) arrive in the order Venora sent them.

### 4.4 Errors

A failed call rejects with an `Error` whose `code` is one of the codes in [section 8](#8-errors). Use `error.code` to handle errors. `error.message` is for humans, and its text can change in any release.

## 5. Capability reference

Each capability lists the following:

* **Since:** the first Venora version that has it. Declare that version with `minVenoraVersion`.
* **Status:** Implemented or Planned.
* **Permissions:** the plugin MUST declare all of them.
* **Input:** the payload.
* **Output:** the resolved value.
* **Errors:** codes beyond the ones any call can return. Any call can return `timeout`, `runtime_stopped`, `capability_failed`, `sensitive_data_blocked`, `invalid_message`, and `rate_limited`.
* **Allows** and **does not allow:** the capability's security boundary.

The order of checks is fixed:

1. The capability exists; otherwise the call fails with `unknown_capability`.
2. Every required permission was declared; otherwise `permission_denied`.
3. The payload is valid; otherwise `invalid_payload`.

So a plugin without the required permission always gets `permission_denied`, whatever payload it sent.

The resource limits in [section 12](#12-resource-limits) add two checks that can answer `rate_limited`:

* before step 1, the plugin's general limits (requests waiting for an answer, and requests per second);
* after step 3, the capability's own rate limit, if it has one.

### 5.1 `venora.ui`

#### `venora.ui.toast({message, durationMs?})`

|                |                                                                                                                          |
| -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Capability     | `ui.toast`                                                                                                               |
| Permissions    | `ui`                                                                                                                     |
| Since / Status | Venora 0.1.0 · Implemented                                                                                               |
| Input          | `message`: string, at most 200 characters, not blank. `durationMs` (optional): integer from 1000 to 10000; default 5000. |
| Output         | `null`                                                                                                                   |
| Errors         | `permission_denied`, `invalid_payload`                                                                                   |

**Allows:** a short plain-text notice at the top of the Fermo page, which disappears after `durationMs`.

* A plugin can show at most 3 notices at once; a 4th replaces its oldest.
* Venora removes the plugin's notices when it stops.

**Does not allow:**

* HTML, Markdown, links, or images; the text is shown literally;
* styling, positioning, buttons or other actions, or notices that stay;
* notices that look like Venora or Fermo.

**Security:** notices are part of Venora's own UI (see [section 5.6](#56-venoras-ui-and-plugin-ui)).

A notice from an **imported** plugin always has three parts:

* a fixed **Plugin** badge;
* the plugin's `name`, with control and text-direction characters removed and shortened to 48 characters;
* the message.

It is shown in a neutral style that differs from Venora's own notices, so it cannot pose as Venora, Fermo, or another plugin, even if the plugin calls itself "Venora".

Notices from plugins bundled with Venora are Venora's own, so they are shown as written, in Venora's style. Plugin CSS cannot restyle, hide, or cover any notice.

### 5.2 `venora.settings`

#### `venora.settings.get({key})`

|                |                                                                                    |
| -------------- | ---------------------------------------------------------------------------------- |
| Capability     | `settings.get`                                                                     |
| Permissions    | `storage`                                                                          |
| Since / Status | Venora 0.1.0 · Implemented                                                         |
| Input          | `key`: string, at most 100 characters, that the plugin declared in its `settings`  |
| Output         | The value the user saved; otherwise the declared `default`; otherwise `null`       |
| Errors         | `permission_denied`, `invalid_payload` (including keys the plugin did not declare) |

**Allows:** reading the plugin's **own** declared settings.

**Does not allow:**

* reading another plugin's settings, since the plugin ID comes from the bridge, not the payload;
* reading undeclared keys;
* writing settings, which only the user can do in version 1.

Rules for declaring settings, the value types returned, and change events are in [plugin-settings-storage.md](/plugin-api/plugin-settings-storage.md#settings).

### 5.3 `venora.storage`

A private key–value store for the plugin's own data, separate from its settings. The full rules (key and value types, limits, persistence, ordering) are in [plugin-settings-storage.md](/plugin-api/plugin-settings-storage.md#storage).

| Method                             | Capability       | Input                               | Output                                            | Errors                                                   |
| ---------------------------------- | ---------------- | ----------------------------------- | ------------------------------------------------- | -------------------------------------------------------- |
| `venora.storage.get({key})`        | `storage.get`    | `key`: string, 1–100 characters     | The stored value, or `null` if the key is not set | `permission_denied`, `invalid_payload`                   |
| `venora.storage.set({key, value})` | `storage.set`    | `key` as above; `value`: plain data | `null`                                            | `permission_denied`, `invalid_payload`, `quota_exceeded` |
| `venora.storage.remove({key})`     | `storage.remove` | `key` as above                      | `null`; succeeds even if the key was not set      | `permission_denied`, `invalid_payload`                   |
| `venora.storage.clear()`           | `storage.clear`  | `{}`                                | `null`                                            | `permission_denied`                                      |

All four need the `storage` permission. Since Venora 0.1.0 · Implemented.

**Allows:** storing up to 1 MiB of plain data that persists across page loads, restarts, disable/enable, and plugin updates.

**Does not allow:**

* naming another plugin's storage, since there is no plugin ID parameter;
* reading or writing settings, extension storage, or Fermo's storage;
* listing other plugins.

### 5.4 `venora.mentions`

Mentions are the `@user` links Fermo renders in messages. Venora finds them on the page and gives each one an opaque `mentionId`. Plugins never touch the mention elements themselves.

#### `venora.mentions.showAvatar({mentionId})`

|                |                                                                                 |
| -------------- | ------------------------------------------------------------------------------- |
| Capability     | `mentions.showAvatar`                                                           |
| Permissions    | `ui`, `account`, `network`                                                      |
| Since / Status | Venora 0.1.0 · Implemented                                                      |
| Input          | `mentionId`: string from a `mentions.added` event (`m` followed by 1–12 digits) |
| Output         | `{shown: boolean}`                                                              |
| Errors         | `permission_denied`, `invalid_payload`                                          |

**Allows:** asking Venora to show the mentioned user's avatar beside a mention.

* Venora looks the avatar up itself: first from Fermo's local data, then if needed with one request to Fermo's API made with the signed-in account.
* Venora then inserts a small decorative image before the mention.
* Repeating the call for the same mention has no further effect.

The result is `{shown: false}` in these cases:

* the mention has left the page;
* the user cannot be resolved;
* the plugin stopped while the avatar was being looked up.

**Does not allow:**

* receiving the avatar URL, the user's profile, the signed-in account, or the session token;
* choosing the image, its position, or which element it is attached to;
* decorating anything other than a mention Venora reported.

**Security:** this capability needs `account` and `network` because Venora uses the signed-in account to make a network request on the plugin's behalf. Those permissions are a consent marker shown to the user at install. They give the plugin no data, and the plugin only learns whether an avatar was shown.

#### `venora.mentions.setAvatarSize({size})`

|                |                                        |
| -------------- | -------------------------------------- |
| Capability     | `mentions.setAvatarSize`               |
| Permissions    | `ui`                                   |
| Since / Status | Venora 0.1.0 · Implemented             |
| Input          | `size`: integer from 12 to 32 (pixels) |
| Output         | `null`                                 |
| Errors         | `permission_denied`, `invalid_payload` |

**Allows:** setting the size of avatars **this plugin** shows, now and later. The default is 16.

**Does not allow:** changing anything another plugin added.

### 5.5 Plugin CSS

A plugin can restyle the Fermo page with the CSS file named by `css` in `plugin.json`. This is declared in the manifest rather than called as a method. Venora applies the CSS when the plugin starts and removes it when the plugin stops.

The CSS is applied only if all of these are true:

* the plugin declares the `ui` permission;
* the CSS contains none of `url(`, `@import`, `image-set(`, `image(`, `expression(`, `-moz-binding`, `behavior:`, or a backslash;
* the CSS contains no quoted string other than an empty one (`""` or `''`), and no `@counter-style` rule. Comments do not count.

Why these constructs are banned:

* The first group can load resources, which could leak page content through URLs.
* The second group can write text onto the page, through `content`, `list-style`, `::marker`, `quotes`, `text-overflow`, or counter styles. A plugin could use that to draw messages that look like Venora's.

Decorative pseudo-elements (`content: ""`), unquoted font names, and attribute selectors without quotes (`[data-state=open]`) are all still allowed. If the CSS breaks any rule, Venora logs the reason and does not apply it, but the plugin still runs.

Plugin CSS applies to Fermo's page only. It cannot select into, restyle, hide, or cover Venora's own UI (section 5.6).

> **Changed in Phase 3.3, under the security exception in** [**section 9.4**](#94-breaking-changes-which-need-a-new-api-version)**.** Earlier versions accepted quoted strings in plugin CSS. Rejecting them is a breaking change, and it was made without a new API version because it is the smallest change that stops plugin CSS from writing text that poses as Venora. Bundled plugins were not affected.

### 5.6 Venora's UI and plugin UI

Venora's own UI is:

* its dialogs: install, update, and sensitive-permission prompts, confirmations, permission information, and plugin settings;
* its notices;
* the Plugins page.

Plugins cannot create, change, or imitate any of it.

* **Isolated rendering.** Venora renders this UI inside shadow roots that page stylesheets, including plugin CSS, cannot select into. The host elements carry inline `!important` styles, which no stylesheet can override, and Venora uses no page CSS variables in them.
* **Top layer.** Dialogs are modal (`<dialog>` with `showModal()`) and notices use the popover top layer. The browser draws both above anything the page or a plugin can draw, so they cannot be covered. Dialogs are also drawn above notices, so no plugin notice can cover a warning or prompt.
* **Text only.** Every plugin-provided string is set as text, never parsed as HTML: names, descriptions, authors, setting labels and descriptions, changelog notes, notice messages, command labels and descriptions, and error messages.
* **Names must read as written.** At install, a plugin's name must have no control or text-direction characters and be at most 64 characters (see [plugin-manifest.md](/plugin-api/plugin-manifest.md)).
* **Imported plugins are marked.** Their cards say "Imported", and their notices are badged as described in section 5.1.
* **No API for Venora's UI.** There is no API for opening dialogs or prompts, rendering HTML, or reading or changing the DOM. The only UI operations are the capabilities in this section, command buttons on the plugin's own card (section 5.9), and plugin CSS.

**Known limitation and recovery:** plugin CSS can still restyle or hide parts of Fermo's own page, which is what the `ui` permission allows. That includes the settings area that contains the Plugins page. It cannot affect Venora's dialogs or notices, which is where consent and warnings are shown.

The way back is the **Venora toolbar popup**, an extension page that no plugin CSS can reach. It offers:

* safe mode, which stops every imported plugin;
* enable and disable for each plugin;
* uninstall for imported plugins.

Open Fermo tabs apply these changes at once, removing the plugin's CSS without a reload.

### 5.7 Event subscription capabilities (runtime use)

| Capability           | Permissions                                | Input             | Output | Errors                                                  |
| -------------------- | ------------------------------------------ | ----------------- | ------ | ------------------------------------------------------- |
| `events.subscribe`   | none itself; the event's permissions apply | `{event: string}` | `null` | `unknown_event`, `permission_denied`, `invalid_payload` |
| `events.unsubscribe` | none                                       | `{event: string}` | `null` | `invalid_payload`                                       |

`venora.on` uses these capabilities internally. Plugins SHOULD call `venora.on`, not these capabilities. Calling them directly subscribes the plugin but delivers events to no listener, and their direct use is not covered by the stability guarantees.

### 5.8 Diagnostic capabilities

`test.echo` and `test.permissionEcho` exist to test the bridge and permission checks.

* `test.echo` takes `{message}` (at most 1000 characters) and returns `{message, pluginId}`.
* `test.permissionEcho` is the same, but requires `storage`.

These capabilities are **not part of the stable API**. They can change or be removed in any release, and plugins MUST NOT depend on them in production.

### 5.9 `venora.commands`

A command is a named action a running plugin offers the user. Venora shows each command as a button on the plugin's own card on the Plugins page, under "Commands". When the user presses it, Venora sends the plugin a [`commands.invoked`](#commandsinvoked) event. The plugin decides what the command does.

```js
export default async (venora) => {
  await venora.on("commands.invoked", async ({id}) => {
    if (id === "clear-history") {
      await venora.storage.remove({key: "history"});
      await venora.ui.toast({message: "History cleared."});
    }
  });
  await venora.commands.register({id: "clear-history", label: "Clear history", description: "Deletes the saved history."});
};
```

#### `venora.commands.register({id, label, description?})`

|                |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Capability     | `commands.register`                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| Permissions    | `ui`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Since / Status | Venora 0.2.0 · Implemented                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Input          | `id`: lowercase words joined by single hyphens (`^[a-z][a-z0-9]*(-[a-z0-9]+)*$`), at most 40 characters, unique among this plugin's commands. `label`: text of 1–48 characters. `description` (optional): text of 1–200 characters, shown as the button's tooltip. Both lengths are counted after surrounding whitespace is trimmed, and the trimmed text is what Venora shows. Labels and descriptions MUST NOT contain control characters (including line breaks) or text-direction characters. |
| Output         | `null`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| Errors         | `permission_denied`, `invalid_payload` (including an `id` that is already registered), `quota_exceeded` (more than 20 commands)                                                                                                                                                                                                                                                                                                                                                                   |

#### `venora.commands.unregister({id})`

|                |                                                         |
| -------------- | ------------------------------------------------------- |
| Capability     | `commands.unregister`                                   |
| Permissions    | `ui`                                                    |
| Since / Status | Venora 0.2.0 · Implemented                              |
| Input          | `id`: a command id                                      |
| Output         | `null`; succeeds even if the command was not registered |
| Errors         | `permission_denied`, `invalid_payload`                  |

**Allows:** up to 20 commands per running plugin, each a button with a plain-text label on the plugin's own card. To change a command's label, unregister it and register it again.

**Lifecycle:**

* Commands belong to the running plugin instance. A plugin that is stopping offers none: its buttons are hidden and cannot be pressed. Its commands are removed when it stops for any reason: leaving `/channels/`, disable, uninstall, safe mode, a failed start, or being stopped by Venora. A plugin registers its commands again each time it starts, normally during initialization.
* The Plugins page shows the commands registered when it was opened. Pressing a button whose command has since been removed does nothing to the plugin; Venora says the command is no longer available.
* A command is delivered only if the plugin has subscribed to `commands.invoked`. Otherwise, and when the plugin is over its event limit (section 12), Venora tells the user the plugin did not receive the command.

**Does not allow:**

* running a command, its own or another plugin's: only the user can, through Venora's UI, and there is no capability for it;
* seeing, registering, or removing another plugin's commands; ids are per plugin, so two plugins can both use `"refresh"`;
* buttons anywhere else, icons, HTML, styling, or keyboard shortcuts;
* any payload in the invocation other than the command's `id`.

**Security:** command buttons are part of Venora's own UI (section 5.6): they sit in the Plugins page's shadow root, inside the plugin's own card, which names the plugin and says "Imported" for imported plugins. Labels follow the same character rules as plugin names. Every invocation is an event, so it passes the page-context check and the session-token guard like any other.

**Future command palette:** the registry (`runtime/plugin-commands.js`) is kept per plugin and can list every running plugin's commands, so a palette can be added later without changing this API.

## 5.10 Capability index

This table is checked by `tests/plugin-api-spec.test.js` against the registry in `venora-chrome/runtime/capabilities.js`.

* An **Implemented** or **Diagnostic** row MUST exist in the registry with exactly these permissions.
* A **Planned** row MUST NOT exist in the registry yet.
* Every registry entry MUST appear here.

The **own rate limit** of a capability is its burst size and refill rate (for example `5, 0.5/s`: 5 at once, then one every 2 seconds). It applies on top of the general limits in [section 12](#12-resource-limits), and is checked by the spec test against each capability's `rateLimit` in the registry.

| Capability               | Method                          | Permissions                | Status      | Own rate limit |
| ------------------------ | ------------------------------- | -------------------------- | ----------- | -------------- |
| `ui.toast`               | `venora.ui.toast`               | `ui`                       | Implemented | 5, 0.5/s       |
| `settings.get`           | `venora.settings.get`           | `storage`                  | Implemented | none           |
| `storage.get`            | `venora.storage.get`            | `storage`                  | Implemented | none           |
| `storage.set`            | `venora.storage.set`            | `storage`                  | Implemented | 60, 20/s       |
| `storage.remove`         | `venora.storage.remove`         | `storage`                  | Implemented | 60, 20/s       |
| `storage.clear`          | `venora.storage.clear`          | `storage`                  | Implemented | 60, 20/s       |
| `mentions.showAvatar`    | `venora.mentions.showAvatar`    | `ui`, `account`, `network` | Implemented | 100, 50/s      |
| `mentions.setAvatarSize` | `venora.mentions.setAvatarSize` | `ui`                       | Implemented | 10, 2/s        |
| `commands.register`      | `venora.commands.register`      | `ui`                       | Implemented | 20, 2/s        |
| `commands.unregister`    | `venora.commands.unregister`    | `ui`                       | Implemented | 20, 2/s        |
| `events.subscribe`       | `venora.on`                     | none                       | Implemented | 20, 2/s        |
| `events.unsubscribe`     | `venora.on`                     | none                       | Implemented | 20, 2/s        |
| `test.echo`              | none                            | none                       | Diagnostic  | none           |
| `test.permissionEcho`    | none                            | `storage`                  | Diagnostic  | none           |

For an Implemented row, the namespaced method in the second column is implemented too. The test also checks that the sandbox exposes exactly these methods.

## 6. Events

Events are how Venora tells a plugin that something happened. They flow one way, from Venora to the plugin.

### 6.1 Subscribing

```js
const unsubscribe = await venora.on("settings.changed", (payload) => { /* … */ });
// later
unsubscribe();
```

* `venora.on(event, listener)` resolves with an unsubscribe function once Venora has accepted the subscription.
* It rejects with `unknown_event` if the event does not exist, or `permission_denied` if the plugin lacks the event's permissions.
* A plugin can add several listeners for the same event. They share one subscription, and Venora is unsubscribed when the last one is removed.
* Calling an unsubscribe function more than once has no effect.
* All subscriptions end when the plugin stops. A plugin MAY simply let that happen instead of unsubscribing in its cleanup.

### 6.2 Rules

* **Authorization:** each event lists the permissions needed to subscribe. A plugin receives an event only if it declared all of them **and** subscribed. Declaring a permission never subscribes a plugin automatically.
* **Scope:** subscriptions belong to one running plugin instance. A plugin cannot subscribe, unsubscribe, or observe another plugin's subscriptions. Plugin-specific events such as `settings.changed` go only to the plugin concerned.
* **Payload:** always a plain-data copy, with the same rules as section 4.1. Each plugin receives its own copy, and changing it affects nothing else.
* **Sensitive data:** an event whose payload would contain the session token is dropped, not delivered (see [plugin-security.md](/plugin-api/plugin-security.md)).
* **Delivery:** at most once, with no acknowledgement or retry.
  * Events that occur before the subscription is accepted, or while the plugin is not running, are not queued. The documented replay for `mentions.added` is the only exception.
  * The listener is registered before the subscription is confirmed, so no event is missed between confirmation and the resolution of `venora.on`.
* **Ordering:** events arrive in the order Venora sent them to that plugin. There is no ordering across plugins.
* **Notifications only:** events report what already happened. Venora does not wait for listeners, ignores their return values, and listeners cannot cancel or change anything.
* **Listener errors:** an exception or rejected promise in a listener is reported to Venora as a plugin error. The other listeners still run, and the subscription stays active.
* **Isolation:** a slow or failing listener in one plugin cannot delay or affect delivery to other plugins.

### 6.3 Public and internal events

There are two kinds of events.

* **Public events** are in the event registry (`EVENTS` in `venora-chrome/runtime/capabilities.js`) and documented below. Plugins can subscribe to these and nothing else.
* **Internal events** are everything else that happens inside Venora, such as route changes, plugin installs, runtime state, and storage changes. They are not public, and `venora.on` rejects them with `unknown_event`.

An internal event MUST NOT become visible to plugins except by being added to the registry. That requires permissions, a payload contract, and an entry in this document, and it counts as an API addition (see [section 9](#9-api-versioning)).

### 6.4 Event reference

#### `settings.changed`

|                |                                                                                          |
| -------------- | ---------------------------------------------------------------------------------------- |
| Permissions    | `storage`                                                                                |
| Since / Status | Venora 0.1.0 · Implemented                                                               |
| Payload        | `{key: string, value}`, where `value` is the new value, of the setting's declared type   |
| Sent to        | Only the plugin that owns the setting                                                    |
| Sent when      | The user changes the setting in the plugin's settings dialog while the plugin is running |

It is not sent for the initial value at startup; read that with `settings.get`. It is also not sent for changes made while the plugin was not running.

#### `mentions.added`

|                |                                                                                                                                                   |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Permissions    | `ui`                                                                                                                                              |
| Since / Status | Venora 0.1.0 · Implemented                                                                                                                        |
| Payload        | `{mentions: [{mentionId: string, userId: string}]}`, in page order                                                                                |
| Sent to        | Every subscribed plugin                                                                                                                           |
| Sent when      | Mentions of users appear on the page. When a plugin subscribes, Venora also sends the mentions already on the page, each mention once per plugin. |

* `mentionId` is opaque and valid only for the current page load. Use it with `mentions.showAvatar`.
* `userId` is the mentioned user's public Fermo user ID, as written in the message.

#### `commands.invoked`

|                |                                                                       |
| -------------- | --------------------------------------------------------------------- |
| Permissions    | `ui`                                                                  |
| Since / Status | Venora 0.2.0 · Implemented                                            |
| Payload        | `{id: string}`, the id of one of the plugin's own registered commands |
| Sent to        | Only the plugin that registered the command                           |
| Sent when      | The user presses the command's button on the Plugins page             |

It is sent once per press, never replayed, and not queued while the plugin is not running. See [section 5.9](#59-venoracommands).

### 6.5 Event index

Checked by `tests/plugin-api-spec.test.js` against `EVENTS`, with the same rules as the capability index.

| Event              | Permissions | Status      |
| ------------------ | ----------- | ----------- |
| `settings.changed` | `storage`   | Implemented |
| `mentions.added`   | `ui`        | Implemented |
| `commands.invoked` | `ui`        | Implemented |

## 7. The plugin's environment

Plugin code runs in a **dedicated worker** inside a sandboxed frame.

* The worker has an opaque origin and no network access.
* It has no `window`, `document`, `parent`, `top`, or `opener`, and its `location` cannot be assigned. A plugin therefore cannot navigate its frame, for example to send data to another server in a URL.

Apart from `venora`, plugin code has the standard JavaScript built-ins, `console`, timers, `crypto.getRandomValues`, `TextEncoder`/`TextDecoder`, and `Intl`. These are web-platform features, not Venora APIs, and Venora makes no promises about them.

Plugins **cannot** use:

* the network: `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`, and loading images, scripts, styles, or frames are all blocked by the sandbox's Content Security Policy;
* the DOM: there is no `document` or `window`, and nothing from Fermo's page is ever passed to a plugin;
* navigation: `location` cannot be assigned, and there is no frame or window to navigate;
* `localStorage`, `sessionStorage`, IndexedDB, Cache Storage, or cookies; use `venora.storage`;
* `chrome.*` or `browser.*` extension APIs;
* `eval` or `new Function`, which the sandbox's policy blocks;
* relative `import`s, because the module is loaded from a `data:` URL; bundle dependencies into one file;
* `alert`, `confirm`, `prompt`, forms, or popups.

Plugins MUST NOT rely on any of these becoming available. They are blocked on purpose.

**Where plugins act.** Plugins run, and their requests are served, only while the page is Fermo's own top-level `https://fermo.sbar.fyi/channels/…` page, in the document they were started in.

* Fermo shown in a frame, by another site or by Fermo itself, gets no plugins.
* So does any other origin, even one that imitates Fermo's page.
* As soon as Fermo leaves `/channels/`, or its document is replaced, Venora refuses further requests with `runtime_stopped`, sends no more events, and stops the plugins. This happens before any operation could act on a different page.

## 8. Errors

### 8.1 Structure

Rejections from `venora.request`, the namespaced methods, and `venora.on` are `Error` instances with two fields.

| Field     | Type   | Stable?                              | Meaning                                                                                  |
| --------- | ------ | ------------------------------------ | ---------------------------------------------------------------------------------------- |
| `code`    | string | **Yes**, within an API major version | One of the codes below                                                                   |
| `message` | string | **No**                               | An English explanation for developers. It can change in any release, so do not parse it. |

On the bridge the error is `{code, message}`; the sandbox turns it into an `Error`. Plugins MUST treat an unrecognized `code` as a general failure, because new codes may be added in a minor release (see section 9).

### 8.2 Codes

Checked by `tests/plugin-api-spec.test.js` against `ERROR_CODES` in `venora-chrome/runtime/bridge.js`.

| Code                     | Meaning                                                                         | Typical cause                                                                                                      | Retrying helps?                                | Status      |
| ------------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | ----------- |
| `invalid_message`        | The request could not be read                                                   | A capability name that is not a dotted name such as `ui.toast`                                                     | No                                             | Implemented |
| `unknown_capability`     | This Venora has no such capability                                              | A typo, or a capability added in a newer Venora                                                                    | No; check `minVenoraVersion`                   | Implemented |
| `unknown_event`          | No public event has this name                                                   | A typo, an internal event, or a newer event                                                                        | No                                             | Implemented |
| `permission_denied`      | The plugin did not declare a required permission                                | `permissions` is missing from `plugin.json`                                                                        | No                                             | Implemented |
| `invalid_payload`        | The input does not match the contract                                           | A wrong type, out of range, or an undeclared setting key                                                           | No                                             | Implemented |
| `quota_exceeded`         | A storage or command limit would be exceeded; nothing was written or registered | Too much data in `venora.storage`, or more than 20 commands                                                        | After freeing space or unregistering a command | Implemented |
| `rate_limited`           | The plugin is over one of its request limits; nothing was done                  | Too many requests at once, too many per second, or too many of one costly kind ([section 12](#12-resource-limits)) | After waiting; retry with a delay              | Implemented |
| `sensitive_data_blocked` | Venora withheld the result because it contained sensitive data                  | A value containing the session token                                                                               | No                                             | Implemented |
| `capability_failed`      | Venora could not complete the operation                                         | An unexpected internal failure; details go to Venora's console, not to the plugin                                  | Possibly                                       | Implemented |
| `timeout`                | No response within 30 seconds                                                   | Venora is busy or the page is hung; the operation may still have happened                                          | Possibly                                       | Implemented |
| `runtime_stopped`        | The plugin is stopping or stopped                                               | A call made during or after cleanup, or after the page changed (for example after Fermo left `/channels/`)         | No                                             | Implemented |

Errors detected inside the sandbox reject with the same codes:

* a payload that is not plain data rejects with `invalid_payload`;
* a non-string capability name rejects with `invalid_message`.

An event name that `venora.on` does not recognize, in any format, rejects with `unknown_event`.

`venora.on` with a listener that is not a function rejects with a `TypeError`. That is a programming error and has no `code`.

### 8.3 What errors must never contain

Error messages are built by Venora, and MUST NOT include:

* session tokens, credentials, cookies, or `Authorization` header values;
* data about the signed-in account beyond what the capability returns on success;
* another plugin's ID, settings, storage, or state;
* stack traces, source paths, extension URLs, or internal storage keys;
* raw responses from Fermo or other servers, such as HTTP bodies or server error text.

Three things enforce this:

* Capabilities report expected failures with fixed, Venora-written messages.
* Any other exception is logged inside Venora and becomes a generic `capability_failed` (`"<capability> failed."`).
* The session-token guard checks error responses like any other response.

## 9. API versioning

### 9.1 What `apiVersion` means

`compatibility.apiVersion` in `plugin.json` is the **major version of the Community Plugin API** that the plugin was written for. It is a positive integer, and it is declared inside `compatibility`, not at the top level of the manifest. See [plugin-manifest.md](/plugin-api/plugin-manifest.md#compatibility) for why.

```json
"compatibility": {"apiVersion": 1}
```

**API version 1** is the contract in this document set:

* the `venora` object;
* capability names, payloads, results, and required permissions;
* events and their payloads;
* error codes;
* the lifecycle contract in [plugin-lifecycle.md](/plugin-api/plugin-lifecycle.md);
* the settings and storage rules;
* the manifest fields that affect the API.

The plain-data rules, timeouts, and limits are part of it too. They may be raised within version 1 but never lowered.

A plugin that does not declare `apiVersion` is treated as API version 1. Every plugin written for the Phase 2.5 runtime uses only capabilities, events, and behaviour that version 1 keeps unchanged, so those plugins are version 1 plugins without any change. New plugins SHOULD declare `apiVersion` explicitly.

`apiVersion` is separate from the following:

* **The plugin's own `version`:** the plugin's release number.
* **`minVenoraVersion`:** the oldest Venora release that has every API the plugin uses.
* **`venora.protocolVersion`:** the internal bridge format, which can change without any change to the API.

### 9.2 Compatibility check

Venora has a list of supported API versions, which is `[1]` today. A plugin is compatible only if both of these are true:

* its declared `apiVersion` (default 1) is in that list;
* its `minVenoraVersion`, if declared, is no newer than the running Venora.

| When                                           | If not compatible                                                                                                                       |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Install or update                              | Rejected before anything is stored, for example: *"This plugin needs Plugin API version 2. This version of Venora supports version 1."* |
| Load (for example after Venora was downgraded) | Not started. Its card says "Not compatible: …", and other plugins still run.                                                            |

An `apiVersion` that is not a positive integer is invalid, and the install is rejected. The supported list is `SUPPORTED_API_VERSIONS` in `venora-chrome/plugin-compatibility.js`. The value plugins see as `venora.apiVersion` is set by the sandbox runtime, never by the plugin.

### 9.3 Changes that do not need a new API version

These can be made in any Venora release:

* adding a capability, method, namespace, event, or permission;
* adding an optional input field, or a field to a result or event payload;
* accepting input that used to be rejected;
* raising a limit (size, rate, timeout);
* adding an error code;
* deprecating something (section 9.5);
* fixing a bug so that behaviour matches this specification.

A plugin that uses something added in Venora X.Y.Z declares `"minVenoraVersion": "X.Y.Z"`. The **Since** entries in section 5 say which version that is. Older Venora releases then refuse to install the plugin instead of failing at runtime with `unknown_capability`.

### 9.4 Breaking changes, which need a new API version

* removing or renaming a capability, method, namespace, event, payload field, or result field;
* changing a field's type, meaning, units, or default;
* making an optional input required, or rejecting input that used to be valid;
* **adding a permission requirement to an existing capability or event;**
* changing which error code an existing condition produces;
* lowering a limit;
* changing lifecycle behaviour plugins rely on, such as when cleanup runs or what `venora.on` resolves with.

A breaking change ships as API version 2.

* Venora MAY support several API versions at once and give each plugin the `venora` object for its declared version.
* Removing support for an API version is itself a breaking Venora change, and must be announced in Venora's release notes.

**Security exception:** if a vulnerability can only be fixed by a breaking change, Venora MAY make it without a new API version. It MUST document the change in its release notes, and the change MUST be as small as possible.

### 9.5 Deprecation

1. The item is marked **Deprecated** in this document, with the Venora version and the replacement if there is one.
2. Its registry entry gets a `deprecated` note, `{since: "<Venora version>", replacement?: "<name>"}`. The first time a plugin uses it during a run, Venora logs one console warning. The plugin is not told, and the call works as before.
3. It keeps working for the rest of that API major version, and is removed no earlier than the next one.

Nothing is deprecated in version 1.

## 10. Naming and stability rules

Contributors adding to the API MUST follow these rules. They are checked by `tests/plugin-api-spec.test.js` where possible.

| Item                      | Rule                                                                                                       | Examples                             |
| ------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| Namespace                 | One lowercase word, `[a-z][a-z0-9]*`, named after the area of Venora or Fermo, not after a permission      | `ui`, `settings`, `mentions`         |
| Capability                | `<namespace>.<method>`: exactly two segments                                                               | `ui.toast`, `mentions.showAvatar`    |
| Method                    | lowerCamelCase, a verb or a verb phrase. Standard verbs: `get`, `set`, `remove`, `clear`, `show…`, `hide…` | `get`, `showAvatar`, `setAvatarSize` |
| JavaScript method         | Exactly `venora.<namespace>.<method>`, the same as the capability                                          | `venora.ui.toast`                    |
| Event                     | `<namespace>.<pastTenseVerb>`, in the namespace of the capabilities it relates to                          | `settings.changed`, `mentions.added` |
| Payload and result fields | lowerCamelCase. Units go in the name, IDs are strings, and booleans use `is…`/`has…` or a past participle. | `durationMs`, `mentionId`, `shown`   |
| Permission ID             | One lowercase word, `[a-z]+`                                                                               | `storage`, `account`                 |
| Error code                | lowercase snake\_case                                                                                      | `permission_denied`                  |
| Manifest field            | lowerCamelCase                                                                                             | `minVenoraVersion`                   |

**Reserved names.** These names MUST NOT be used for new public namespaces without a specification change.

| Kind                       | Reserved names                                               | Why                                                                                  |
| -------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| Runtime plumbing           | `events`, `test`, `internal`, `runtime`, `debug`, `venora`   | Used, or kept, for the runtime itself; not public API                                |
| `venora` object members    | `pluginId`, `apiVersion`, `protocolVersion`, `request`, `on` | Already used                                                                         |
| Possible future namespaces | `account`, `network`, `notifications`                        | Not in version 1. Reserved so that if they are added, they are designed deliberately |

**Deprecated names:** none.

**Adding a capability.** A pull request that adds a capability MUST do all of the following:

1. Add an entry to `createCapabilities` in `runtime/capabilities.js` with:
   * `permissions`: every existing permission ID it needs;
   * `validate`: rejects anything outside the documented input;
   * `handle`: returns only the plain data the plugin needs.
2. Use the plugin ID only from the bridge context, never from the payload.
3. Document it in section 5, with its **Since** version, input, output, errors, what it allows, and what it does not allow.
4. Add a row to the capability index.
5. Add the namespaced method to `API_METHODS` in `runtime/plugin-runtime.js`.
6. Add tests for:
   * `permission_denied` without each required permission;
   * `invalid_payload`;
   * the session-token guard for its results.
7. Not expose browser-specific objects or behaviour (section 11).

Adding an event follows the same steps, using `EVENTS` and the event index.

## 11. Chrome and Firefox

**The public API is the same in every browser.** A plugin never needs to know which browser it is running in, and MUST NOT try to detect it.

| Part                                                                                                         | Status                                                                                                                                                                                    |
| ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The `venora` object, capabilities, events, payloads, errors, limits, quotas                                  | Cross-browser API, identical in every browser. Venora sets its own limits rather than passing on browser quotas.                                                                          |
| The plain-data rules                                                                                         | Based on structured cloning over `MessagePort`, which Chrome and Firefox both support                                                                                                     |
| Module format (one ES module with a default export)                                                          | Cross-browser                                                                                                                                                                             |
| Sandboxing mechanism, frame creation, the extension's storage backend (`chrome.storage` / `browser.storage`) | Runtime-specific and hidden from plugins. Browser differences MUST be handled inside Venora (`createBrowserFrameFactory` in `runtime/host.js`, and the trusted services in `content.js`). |
| Web-platform features inside the sandbox (section 7)                                                         | Not part of the Venora API; may differ slightly between browsers                                                                                                                          |

The API MUST NOT expose browser-specific objects, such as `chrome.*` types, `Port` objects, or extension URLs, or semantics that only one browser supports. A browser that cannot support a capability as specified MUST answer it with `unknown_capability`, not a different behaviour.

**UI and runtime features used inside Venora:** `<dialog>` with `showModal()`, the popover top layer for notices, open shadow roots, and a `data:` URL worker that inherits the sandbox's content security policy.

* If the popover API is missing, notices fall back to a fixed layer at the highest z-index.
* These features are not part of the plugin API.

**Known limitations:**

* Venora has been tested only in Chromium-based browsers (Edge 154 and Brave 155), not in Google Chrome itself. This includes the plugin worker inheriting the no-network policy.
* Running the sandbox in Firefox has not been verified. Open questions include `sandbox` manifest support and `data:` module imports under the sandbox CSP; see [runtime-architecture.md](/runtime-architecture.md#chrome-and-firefox).
* If Firefox needs a different isolation mechanism, the change stays inside the runtime. The API defined here does not change.

## 12. Resource limits

Venora gives every running plugin its own resource account, kept by the runtime host and tied to the plugin's private channel. A plugin cannot use, see, or exhaust another plugin's limits. The accounting never leaves the extension.

The limits below are chosen from real use:

* the bundled plugins send a few requests per page change;
* a polling plugin sends a few per second;
* a page of mentions produces a few dozen events and avatar requests at once.

Limits may be raised within API version 1, but never lowered (section 9.3).

### 12.1 Enforced limits

These are hard guarantees, enforced by Venora in every browser.

| Resource                                       | Limit                                                                                                                                                                                     | When exceeded                                                   |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Requests waiting for an answer, per plugin     | 64                                                                                                                                                                                        | `rate_limited`; nothing is done                                 |
| Requests per plugin, all capabilities together | Up to 120 at once, refilled at 60 per second                                                                                                                                              | `rate_limited`; nothing is done                                 |
| Requests of one costly capability              | The capability's **own rate limit** in the [capability index](#510-capability-index), for example 5 `ui.toast` at once, then 1 every 2 seconds. It counts only permitted, valid requests. | `rate_limited`; nothing is done                                 |
| Events sent to a plugin                        | Up to 100 at once, refilled at 50 per second                                                                                                                                              | The event is dropped (delivery is at most once, section 6.2)    |
| One message, either way                        | At most 50,000 values and 1,000,000 characters of strings and keys, on top of section 4.1's per-value limits                                                                              | The plugin is **stopped**: receiving it has already cost memory |
| A capability that does not answer              | 30 seconds                                                                                                                                                                                | `timeout`; the request's slot is freed                          |
| Storage                                        | 64 KiB per value, 1 MiB and 1,000 keys per plugin ([plugin-settings-storage.md](/plugin-api/plugin-settings-storage.md#limits))                                                           | `quota_exceeded`                                                |
| Commands                                       | 20 per running plugin ([section 5.9](#59-venoracommands))                                                                                                                                 | `quota_exceeded`                                                |
| Plugin files at install                        | ZIP at most 16 MiB; `plugin.json` and CSS at most 131,072 characters each; entry script at most 2,000,000 characters ([plugin-manifest.md](/plugin-api/plugin-manifest.md#packaging))     | Not installed                                                   |

**Repeated rejections stop the plugin.** If more than 200 of a plugin's requests are rejected with `rate_limited` within 10 seconds, the plugin is stopped.

### 12.2 Unresponsive plugins (watchdog)

While a plugin is running, Venora pings its worker every 2 seconds. A plugin whose worker has not answered within 5 seconds is **stopped**, with the error "The plugin stopped responding."

* This catches a plugin stuck in a loop, in its own code or in an event listener.
* Stopping it removes its frame and worker, which ends its code at once.

Startup (10 seconds) and cleanup (2 seconds) keep their own time limits (see [plugin-lifecycle.md](/plugin-api/plugin-lifecycle.md#time-limits)).

### 12.3 Repeated problems

A plugin stopped for misbehaviour 3 times on the same page is not started again until the page is reloaded. Misbehaviour here means any of:

* not responding;
* staying over its request limits;
* an oversized message;
* more than 20 malformed messages.

Its card says "Stopped after repeated problems on this page." Other plugins keep running.

### 12.4 What Venora does not limit

Browsers give extensions no portable way to cap the CPU time or memory of one worker, so Venora does not claim to.

* **CPU:** a plugin that stays responsive, answering pings between bursts of work, can still use CPU. The watchdog detects unresponsiveness. It does not measure or cap CPU use.
* **Memory:** there is no per-plugin memory cap, so a plugin can still use memory inside its own worker. Memory that a plugin can make **Venora** hold is bounded by:
  * the message and in-flight limits;
  * the queue limits that follow from them;
  * the storage quotas;
  * the install limits.
* **Browser behaviour:** the browser may throttle or suspend a plugin's worker in a background tab. Whether a frame and its worker run in a separate process depends on the browser's site isolation.


---

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