Manifest reference
Every field of intentic-extension.json. The manifest is both the approval dialog the owner reads and the gate the host enforces, so what's here is exactly what your extension can do.
Top level
| Field | Required | Meaning |
|---|---|---|
publisher | yes | Lowercase slug. With name it derives the extension id, acme.incidents. |
name | yes | Lowercase slug, unique within the publisher. |
version | yes | Your semver, for display. The code identity is always the installed commit sha. |
engines.intentic | yes | Semver range over the host's extension-API version, checked before activation. Current API: 0.4.0. |
category | no | The section your extension sits under in the sandbox's Extensions tab: work, workspace, connections, knowledge or sandbox. Anything else (or nothing) lists it under Other; it installs and runs either way. |
logo | no | A simple-icons slug — "linear", "notion" — drawn wherever your extension is listed. Add "/<hex>" ("github/f5f5f5") to force a colour for a mark that would otherwise vanish against the surface. |
icon | no | A glyph from the app's own icon set, drawn when there is no brand to use — and when an offline sandbox can't reach the icon CDN. Neither field is required and neither can break the page: a slug that 404s, a glyph this build has never heard of, or nothing at all all fall through to your extension's initials. |
permissions.sandbox | no | The daemon routes you may call. Omit if you call none. |
contributes | no | Everything below. |
permissions.sandbox
Each entry is "<METHOD> <path-glob>", where * matches exactly one path segment. A call through
api.sandbox.request or api.sandbox.json to anything not covered throws. An extension has no ambient client
to your daemon.
"permissions": {
"sandbox": ["GET /panels", "POST /panels/*/start", "GET /memory/file"]
} Keep it as narrow as the feature needs. This list is what a reviewer reads to decide what your extension can reach.
contributes.views
| Field | Meaning |
|---|---|
id | Kebab-case slug; must match the id your code registers. |
label | The view family's human name. |
surface | rail (always-visible left column), directory (a per-repo panel in the Workspace tree), or sandbox (a tab on the Sandbox hub). |
badge | Whether the tile may show a count. Without it the host drops any badge you register. |
Pick the surface by what the view is about. The rail is a place people act from and its slots are permanent, so earn one. A view about the box itself (its logs, its consumption) belongs on the Sandbox hub, where things are inspected occasionally rather than worked in.
contributes.files
An intentic workspace is file-first: the agent edits files directly, out of band from every HTTP route. Declaring the files behind your view is what gets you into the daemon's change push instead of polling for it.
| Field | Meaning |
|---|---|
path | Workspace-root-relative, forward-slash, matched by prefix, not a glob. Keep a trailing slash on directories so .intentic/drafts/ can't match a sibling file. |
invalidates | The query keys those contents feed: the first parts of your own api.sandbox.key(...) keys. At least one. |
Narrow paths matter. A broad prefix costs every connected browser a refetch on every matching write, and a write-heavy path turns that into a request storm.
contributes.viewers
A renderer for a file type. The host resolves the open file, fetches its content, and renders your component with it.
| Field | Meaning |
|---|---|
id | Kebab-case slug, matched to your runtime registration. |
extensions | Bare file extensions, no dot: ["docx", "xlsx"]. |
fetch |
How the host hands you the file. "text" is decoded utf8, as a text prop.
"blob" is the whole file's bytes, as blob, for a format that must be parsed end to end.
"url" is a streaming URL to point an element at, as src, for audio and video, which
range-read gigabytes and should never be held in memory. The host mints the credential on that URL, so your
component never sees one.
|
contributes.commands
| Field | Meaning |
|---|---|
command | Dotted slug with at least two segments, as in incidents.acknowledge. |
title | What the command palette shows. |
icon | Optional glyph from the host's icon set. |
keybinding | Optional global shortcut in chord notation, as in "Mod+Shift+K", where Mod is ⌘ on Apple and Ctrl elsewhere. |
A global shortcut is consequential, so like the title it's declared here and the host binds only what was approved.
contributes.settings
Typed settings the host renders into the Settings page and persists daemon-side, shared across all the owner's browsers.
| Field | Meaning |
|---|---|
key | Slug you read with api.settings.get(key). |
type | boolean, string, number, or enum. |
title, description | Label and help text. |
default, enum | Initial value; the choices, for enum. |
secret | Masks the value and strips it from reads, so a set secret round-trips as "still set", never as itself. |
env | Injects the stored value into the agent's shell under this name, every turn. How a credential reaches the agent's CLI tools. |
Settings live on the daemon keyed by publisher.name, so they outlive a re-clone, an update, or a remove and re-add.
contributes.capabilities
A whole third-party integration expressed as data, with no code. It produces a real capability card in the app: a "+" tile, a config
form, credentials stored as secrets, environment variables in the agent's shell, and a SKILL.md cheatsheet that teaches
the agent to use the tool.
You contribute a card, never a handler. The machinery that acts on a capability lives in the daemon, because some of it grants real privilege — putting the sandbox on a private network, running it privileged, changing what an agent may do on somebody's personal computer. So the four kinds below are the ones whose daemon-side behaviour is entirely generic over your data; a manifest naming any other kind is rejected at install.
"capabilities": [
{
"id": "linear",
"kind": "cli",
"catalog": {
"name": "Linear",
"logo": "linear",
"description": "Read and file issues from the agent's shell.",
"category": "Project management",
"guide": {
"url": "https://linear.app/settings/api",
"linkLabel": "Create a personal API key",
"steps": ["Open Settings → API", "Create a personal key", "Paste it here"]
}
},
"fields": [{ "key": "apiKey", "label": "API key", "secret": true }],
"env": { "LINEAR_API_KEY": "${apiKey}" },
"skill": "skills/linear/SKILL.md"
}
] | Field | Meaning |
|---|---|
id | Slug the card is keyed by, unique within its kind. It becomes the card's URL and the hidden field tying each connection back to it. |
kind | "cli" a command-line tool the agent drives; "browser" a site the agent acts on as the user (add a loginUrl); "host" an operating-system guide for a connected computer; "agent" a preset for an outside chat agent (fields only). |
catalog | The card: name, description, category, an optional brand logo or fallback icon, and a guide walking the user through creating the credential. |
fields | The config form: key, label, and flags for secret, optional, multiline, options, and a when gate. |
env | Environment variables for the agent, templated over the fields: ${field} substitutes, ${field:uri} percent-encodes. |
skill | Path to the SKILL.md that teaches the agent the tool. Two placeholders are substituted when a user adds it: ${id} becomes the name they chose, and ${tools} becomes the built-in note for that kind (how to drive the shared browser, what a connected computer's tools are). |
fragment | Optional image fragment installing the client binary. cli only. |
This is the highest-leverage contribution point in the manifest: one entry plus two files adds an integration to the product.
contributes.processes and contributes.listener
A process is a long-lived background program the daemon supervises. Pair it with a listener and you have a realtime event source that
can wake the agent, exactly how the Discord and IMAP integrations work.
A gateway process with a listener "processes": [
{ "name": "gateway", "command": "node dist/gateway.js", "port": "auto", "autoStart": true }
],
"listener": { "provider": "linear", "eventTypes": ["issue_created", "comment_added"] }
Field Meaning processes[].nameSlug you address it by; undeclared names are refused. processes[].command, cwdWhat to run, and where relative to the checkout. processes[].port"auto" assigns a free port and injects it as PORT. processes[].previewExposes the port on a tunneled preview hostname. processes[].autoStartLaunch on install and on daemon boot. listener.providerThe slug automation triggers fire on. listener.eventTypesThe kinds a trigger may narrow to. The daemon validates automations against this list.
The daemon holds no connection to your provider; your process does. It gets a provider-scoped control surface to reconcile state,
dispatch a wake, and report status or failure.
contributes.agent and contributes.bin
Field Meaning agent.pathA checkout-relative directory that is a Claude Code plugin: skills, subagents, hooks, commands, .mcp.json. Handed to the agent's plugin loader every turn. Omit path to use the checkout root. binA checkout-relative directory of executables prepended to the agent's PATH each turn. How you ship it a command-line tool.
These two are how an extension changes what the agent can do rather than what the owner can see. Both are composed per turn,
so they take effect from the next one, with no reload.
contributes.environment
A Dockerfile fragment baked into the sandbox image overlay, for when your tool has to actually be installed: a database client, a
media encoder, a language runtime.
Field Meaning fragmentCheckout-relative path to a file containing only RUN and ENV instructions.
FROM and privileged directives are rejected. An extension can install tools but can't claim container privileges or
swap the base image. The owner approves the composed overlay and rebuilds out of band, so this is the one contribution that applies
on the next image rebuild rather than immediately.
When each contribution takes effect
Contribution Applies views, viewers, commands, files, settings, processes, capabilities, listenerImmediately agent, binNext agent turn environmentNext image rebuild
The Extensions tab states this per extension, so nobody has to guess why a change hasn't landed yet.