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.
Code
| 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.
Code
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:
Code
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:
Code
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:
Code
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>:
Code
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:
Code
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:
Code
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.
