# Extensions

An extension is a service you run that adds to one workspace: a page in the menu, a panel
on a record, a tool its agents can call, or work that starts when something changes. It
runs on your infrastructure, reaches the workspace only through this API, and shows its
pages inside the app in a frame. Nothing it does runs inside the platform.

Gated on the **module-extensions** entitlement. Extensions are registered by GenuineAI and
offered to a workspace; a workspace administrator then enables each one and chooses what it
may do.

## The manifest

Everything the platform knows about an extension is in its manifest, `extension.json`. Its
JSON Schema ships with the extension SDK as `@genuineai/ext-sdk/schema`.

```json
{
  "slug": "warranty",
  "name": "Warranty triage",
  "version": "1.4.0",
  "scopes": ["object:view", "object:edit", "extension-state:read", "extension-state:write"],
  "ui": {
    "origin": "https://warranty.example.com",
    "contributions": [
      {"slot": "menu.page", "path": "/claims", "title": "Warranty claims", "icon": "ph-wrench"},
      {"slot": "object.item.panel", "path": "/panel", "title": "Triage", "objectTypes": ["warranty-claim"]}
    ]
  },
  "events": {"url": "https://warranty.example.com/events", "types": ["object.created", "object.state_changed"]},
  "tools": {"url": "https://warranty.example.com/tools", "items": [
    {"name": "claim_history", "description": "Claims on file for a serial number",
     "input_schema": {"type": "object", "properties": {"serial": {"type": "string"}}, "required": ["serial"]}}
  ]},
  "egress": ["erp.example.com"]
}
```

| Field | What it decides |
|---|---|
| `scopes` | The permissions the extension asks for. An administrator grants any subset they hold themselves. Administration of users, roles, settings, keys and billing, the audit and log views, and other extensions can never be granted. |
| `ui` | Where its pages are served from, and which slots they fill. |
| `events` | Which changes in the workspace it hears about, and where they are sent. |
| `tools` | Tools the workspace's agents may call, with the input each one takes. |
| `egress` | The outside hosts it talks to, shown to the administrator who enables it. |

## Two identities

**The install key.** Enabling an extension issues a key that acts as the extension itself,
with exactly the scopes the administrator granted, narrowed to the workspace's plan. Use it
for background work: handling events, syncing a system, writing results.

```bash
curl https://api.genuineai.app/api/v1/extensions \
  -H "X-Api-Key: gai_…" -H "X-Tenant-Id: <workspace-id>"
```

**A delegated token.** When a person opens an extension page, or an agent working for a
person calls a tool, the platform hands the extension a token for that person. It is
signed with RS256, lasts ten minutes, and lets the extension act as that person: what the
person may do today, narrowed to the extension's scopes. The audit trail names the person.
Verify it against the published keys:

```
GET https://api.genuineai.app/.well-known/jwks.json
```

The token's audience is `ext:<slug>`. Send it as `Authorization: Bearer <token>` to call the
API for that person. Neither identity reaches workspace administration.

## Pages and slots

A page is served from your `ui.origin` and framed by the app. The frame and the app talk
through `postMessage`, and each side drops messages from any other origin.

| Slot | Where it appears | What the page is told |
|---|---|---|
| `menu.page` | A menu entry and a full page | Nothing beyond the person |
| `object.item.panel` | A panel on a record; `objectTypes` limits the kinds | The record id and kind |
| `document.item.tab` | A tab on a document | The document id |
| `chat.context.section` | A section beside a conversation | The conversation id |
| `dashboard.tile` | A card on the dashboard | Nothing beyond the person |
| `admin.tab` | A tile in workspace administration | Nothing beyond the person |

The page announces itself with `ready`; the app answers with `init`, which carries the
token, the person, the workspace, the light or dark theme and the colors that make a page
look native. The page can ask the app to resize the frame, navigate, show a message,
reload a record it changed, and refresh the token. The SDK's `connect()` does all of this:

```js
import {connect} from '@genuineai/ext-sdk/bridge';

const bridge = await connect();
const claim = await bridge.fetch(`/objects/${bridge.context.subject.id}`);
```

Serve every page with `Content-Security-Policy: frame-ancestors` naming the app's hosts.

## Events

When a subscribed change happens, the platform POSTs an envelope to `events.url`:

```json
{
  "id": "0b7e…",
  "type": "object.state_changed",
  "occurred_at": "2026-10-03T14:20:03Z",
  "sequence": 48211,
  "tenant": "8b1f…",
  "subject": {"type": "object", "id": "a3c9…"},
  "actor": {"type": "user", "id": "77d0…"},
  "data": {"to": "c41e…"}
}
```

The envelope carries ids, never record contents: read the record through the API with the
install key. `X-GenuineAI-Signature: t=<unix time>,v1=<hex>` is an HMAC-SHA256 of
`<t>.<raw body>` with the install's signing secret; refuse anything that does not verify or
is more than five minutes old. Answer with any 2xx once the work is done. Anything else is
retried with growing gaps for about an hour, then kept as failed, where an administrator
can send it again. Deliveries can repeat and arrive out of order: treat `id` as the
idempotency key, and `sequence` to discard an older change to a subject you have already
applied.

Event types follow `resource.action`: `object.created`, `object.updated`,
`object.state_changed`, `file.created`, `task.status_changed`, `task.assigned`,
`form.submitted` and others. Nothing is sent for a type no enabled extension subscribes to.

## Agent tools

Each tool in the manifest becomes available to the workspace's agents as
`ext:<slug>:<name>`. When an agent calls it, the platform POSTs to `<tools.url>/<name>`:

```json
{"tool": "claim_history", "arguments": {"serial": "7HX-44019-C"}, "tenant": "8b1f…", "user": "77d0…", "thread": "…"}
```

The request is signed like an event, and carries a delegated token for the person the agent
is working for when there is one. Answer within 30 seconds with JSON; answers past about
16,000 characters are shortened before the model reads them.

## State

Most extensions need somewhere to keep a decision, a cursor or a draft, and nothing more.
Each extension can keep up to 1,000 JSON documents of up to 256 KB in a workspace:

```
GET    /extensions/{slug}/state
GET    /extensions/{slug}/state/{key}
PUT    /extensions/{slug}/state/{key}      {"exs_value": …}
DELETE /extensions/{slug}/state/{key}
```

Every read returns the document's version as an `ETag`. Send it back as `If-Match` and a
write made since is refused with `412` instead of overwritten.

## Developing

The extension template starts a local server, a public tunnel and a coding session with one
command:

```
npx genuineai dev
```

In a sandbox workspace, **Administration → Extensions → Open the Workbench** pairs with it.
The sandbox install then sends its events and tool calls to the tunnel and frames the dev
pages, so a change is visible in the app as soon as it is saved. Dev builds can be connected
only in workspaces marked as sandboxes, and the connection ends on its own after twelve hours.
