# Manifest reference Every field in extension.json — type, whether it is required, its default, its constraints, and what it controls. Source: https://docs.vampikez.fun/reference/manifest/ This page is the exhaustive field list for `extension.json`, derived from the schema the host validates against. Use it to look up a type, a default, or the full value set of an enum. For the reasoning behind the choices — which fields you actually need and which ones fail quietly — read [The manifest](/build/manifest/) first. ## Where it lives, and where the id comes from `extension.json` sits in the extension's root directory, beside `main/`, `ui/`, `skills/`, and `dist/`. It is the only required file: a directory with UI or main code but no manifest is reported as a failed scan. The extension's id is **the directory name**, not a manifest field. There is no `id` key. The directory name must be lowercase kebab-case (`^[a-z][a-z0-9-]*$`) because every id-shaped field elsewhere — `dependencies`, catalog slugs — is validated against that pattern. ## Top-level fields The root object is **permissive**: an unknown top-level key is accepted and ignored rather than failing validation. | Field | Type | Required | Default | Constraint | | --- | --- | --- | --- | --- | | `name` | string | **yes** | — | non-empty | | `version` | string | **yes** | — | `^\d+\.\d+\.\d+$` — no pre-release tags | | `description` | string | no | — | — | | `origin` | object | no | — | Generated, read-only provenance for an imported Agent Plugins, Claude, or Codex plugin. See [Portable plugins](/build/portable-plugins/). | | `longDescription` | string | no | — | Markdown | | `icon` | string | no | — | a `lucide-react` export name | | `author` | string | no | — | — | | `permissions` | array | no | `[]` | see [Permissions](#permissions) | | `capabilities` | string[] | no | `[]` | unknown strings accepted | | `products` | string[] | no | — | each `^[a-z0-9][a-z0-9-]*$` | | `webRequestRewrite` | object | no | — | see [webRequestRewrite](#webrequestrewrite) | | `contributes` | object | no | — | see [contributes](#contributes) | | `build` | object | no | — | strict; see [build](#build) | | `activationEvents` | string[] | no | `["onStartupFinished"]` | see [Activation events](#activation-events) | | `dependencies` | string[] | no | — | each `^[a-z][a-z0-9-]*$` | | `main` | string | no | — | path relative to the extension root | | `server` | string | no | — | path relative to the extension root | | `requiresElectron` | boolean | no | `false` | — | | `compat` | `{ pluginApi: string }` | no | — | `pluginApi` non-empty; a semver range | What each one controls: | Field | Controls | | --- | --- | | `name` | The human label in the sidebar, the catalog card, and search | | `version` | Update comparison at install; the version reported to the registry | | `description` | One-line summary on catalog cards, in search results, and in the detail hero | | `longDescription` | The Markdown body of the catalog detail page | | `icon` | The sidebar and catalog icon | | `author` | Attribution shown in the catalog | | `permissions` | Which host APIs are exposed or allowed, and what the catalog discloses | | `capabilities` | Call-time gates on `ctx.api.webviews`, and which webview backing is used | | `products` | Which branded catalogs list the extension. Absent or empty means all | | `webRequestRewrite` | Default header rewriting for webviews this extension creates | | `contributes` | Everything declarative: pages, views, commands, skills, settings, servers | | `build` | Per-path loader overrides for the public SDK builder | | `activationEvents` | When the `main` module is loaded and `activate()` runs | | `dependencies` | Other extensions installed recursively alongside this one | | `main` | Entry point of the host/Electron-side module | | `server` | Entry point of the headless (electron-free) module | | `requiresElectron` | A headless core skips this extension at scan unless `server` is also declared | | `compat.pluginApi` | Plugin-API range checked against the host at registration; a mismatch refuses activation | ### Plugin API compatibility The host checks `compat.pluginApi` when registering an extension. A missing range is permissive; an incompatible range refuses activation. The host's current plugin-API version is `3.0.0`. A range with syntax the host cannot parse falls back to exact string comparison against that version, so a typo blocks activation rather than matching everything. ## Permissions `permissions` is an array whose entries are either a string from the closed set below, or one of the two structured forms. > Permissions gate named WAMP host APIs only. They do not sandbox the package: > Node entry points retain ambient Node access, and UI code runs in the host > renderer's page realm. Installing an extension means trusting its code. | Permission | What declaring it does | | --- | --- | | `http-routes` | Adds `ctx.api.http` | | `notifications` | Adds `ctx.api.notifications` | | `process` | Adds `ctx.api.process`, `ctx.api.pty`, and `ctx.api.git` | | `auth.identity` | Adds `ctx.api.auth`; `fetch` additionally requires `auth.outbound` | | `ai` | Model-call disclosure; also required for an ACP runtime declaring `modelAccess: "host"` | | `network` | Disclosure only | | `terminal` | Disclosure only | | `filesystem` | Disclosure only — `ctx.api.fs` is present either way | The structured forms, which may share one object: ```json { "permissions": [ { "auth.outbound": ["https://api.example.com"], "auth.resource": [{ "audience": "internal-docs", "baseUrl": "https://docs.example.com/api/v1" }] } ] } ``` `auth.outbound` takes an array of absolute URLs — each entry must parse as a URL or the manifest fails validation. It declares the origins `ctx.api.auth.fetch` may target; other origins are refused at the host proxy. `auth.resource` takes one to eight `{ audience, baseUrl }` entries, each audience at most once. `audience` matches `^[a-z][a-z0-9-]{1,127}$`; `baseUrl` is a canonical HTTPS URL with a path and no credentials, query, fragment or trailing slash. It declares the connected resources `pluginAPI.auth.resourceFetch` may read as the viewer; see [Read your service from extension UI](/identity/connected-resources/#read-your-service-from-extension-ui). The object and each `auth.resource` entry are **strict**: any other key fails validation. The local typed store (`ctx.api.data`) is present with no permission at all. See [Permissions](/build/permissions/) for the enforcement model and [Plugin API reference](/reference/plugin-api/) for the per-namespace table. ## Capabilities `capabilities` is a separate list, unrelated to `permissions`. Four strings are recognized: | Capability | Gates | | --- | --- | | `webviews.navigate` | Navigation control on a webview handle | | `webviews.executeScript` | Script injection into a webview | | `webviews.interceptRequests` | Per-request inspection and rewriting | | `webRequestRewrite` | Header rewriting declared via the `webRequestRewrite` field | Declaring any of the three `webviews.*` values also selects the `WebContentsView` backing for `ctx.api.webviews`; without one of them the webview uses the sandboxed-iframe backing. The allowlist is informational. An unrecognized string validates and then matches nothing the host gates on, so a misspelling produces a capability that is silently never granted. ## Activation events Every entry must match one of six forms. The literal `*` is rejected. | Form | Fires when | | --- | --- | | `onStartupFinished` | The initial extension scan completes | | `onCommand:` | A contributed command with that id is invoked | | `onAgent:` | An agent with that id is about to run | | `onTool:` | A tool with that name is about to execute | | `onMcpServer:` | An MCP server with that id is starting | | `onChatCommand:` | That chat command is typed | The id segment accepts `[\w.:-]+`. An absent or empty array is replaced with `["onStartupFinished"]` for an extension with a selected Node entry (`main`, or `server` on a headless host). Omitting it does not make that entry lazy. ## `webRequestRewrite` Default header rewriting applied to webviews this extension creates. All three fields are optional. | Field | Type | Effect | | --- | --- | --- | | `stripCSP` | boolean | Removes `Content-Security-Policy` response headers | | `stripFrameOptions` | boolean | Removes `X-Frame-Options` response headers | | `allowlist` | string[] | Restricts the rewrite to these hosts | Using it requires the `webRequestRewrite` capability. ## `build` Per-path loader overrides for `wamp ext dev` and `wamp ext pack`, so a modest asset need stays on the standard public builder. The `build` object and each rule are **strict**. | Field | Type | Required | Constraint | | --- | --- | --- | --- | | `loaders` | array | no | — | | `loaders[].match` | string | **yes** | non-empty glob, relative to the extension root | | `loaders[].loader` | enum | **yes** | `text` \| `json` \| `base64` \| `dataurl` \| `binary` | ```json { "build": { "loaders": [{ "match": "presets/**.svg", "loader": "text" }] } } ``` ## `contributes` Every contribution key is optional. The `contributes` object itself is permissive; strictness varies per contribution type and is tabulated in [Strict and permissive objects](#strict-and-permissive-objects). | Key | Type | | --- | --- | | [`pages`](#contributespages) | array | | [`views`](#contributesviews) | array | | [`commands`](#contributescommands) | array | | [`skills`](#contributesskills) | boolean, string, or string[] | | [`services`](#contributesservices) | array | | [`settings`](#contributessettings) | array | | [`toolMetadata`](#contributestoolmetadata) | array | | [`mcpServers`](#contributesmcpservers) | array | | [`agentRuntimes`](#contributesagentruntimes) | array | | [`ipcNamespaces`](#contributesipcnamespaces) | string[] | :::caution[There is no `contributes.tools`] The schema has no `tools` key under `contributes`, and because `contributes` is permissive, writing one is accepted and then dropped — no error, no tools. The only way to contribute a tool is `ctx.api.tools.register` at runtime from the `main` half. See [Contributing tools](/build/tools/). ::: ### `contributes.pages` A page is a top-level workspace surface plus a sidebar entry. Strict — an unknown key fails the manifest. | Field | Type | Required | Default | Notes | | --- | --- | --- | --- | --- | | `id` | string | **yes** | — | non-empty; the bundle's `views[]` export is auto-registered into `workspace.main` with `type: ` | | `title` | string | **yes** | — | non-empty; the sidebar label | | `icon` | string | no | — | `lucide-react` export name | | `context` | enum | no | `both` | `project` (needs an open workspace) \| `both` | | `presentation` | enum | no | `docked` | `docked` \| `app` | | `placement` | enum | no | `top` | `docked` pages only: `top` \| `footer` | `context` is normalized to `both` at parse time, so every consumer reads a concrete value. `presentation: 'app'` hides the WAMP chrome and lets the page render its own via `AppShell`; `placement: 'footer'` puts the sidebar row in the fixed bottom group instead of the reorderable list. See [Pages and windows](/build/pages-and-windows/). Do **not** also declare a `views` entry for the page's own surface — the page id already registers one. ### `contributes.views` A component mounted into a named slot. Use this only for slots other than the page's own `workspace.main` surface. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `id` | string | **yes** | non-empty; must match a key of the bundle's `views` export | | `slot` | enum | **yes** | one of the five slots below | | `type` | string | no | discriminator within `workspace.main` | | `title` | string | no | label where the slot shows one | | `icon` | string | no | `lucide-react` export name | Every `ViewSlot` value, the props its component receives, and whether a host renders it today: | Slot | Props | Renders today | | --- | --- | --- | | `workspace.main` | `{ paneId: string; type: string }` | yes — the main workspace pane | | `session.dock` | `SessionDockContext` (below) | yes — the session dock strip | | `chat.newSessionControls` | `{ workspace; activeWorkspacePath; draftGeneration; setSubmissionPending(pending) }` | yes — one compact control beside the local new-chat run-destination pill. To add a *place a chat can run*, publish destinations with `host.chat.setSessionDestinations` instead of drawing a control | | `chat.composerStatus` | `{ chatId; visible; workspace; activeWorkspacePath; isStreaming; onInsert(text) }` | yes — status and next action above the active chat composer | | `chatInput.attachments` | `{ draftText: string; onInsert(text): void; onAttachmentRemoved(id): void }` | yes — below the chat composer | Publishing validates the slot strictly, so a typo is refused at upload. An INSTALLED manifest is treated differently: a view whose slot this build no longer knows is dropped at load and named in the log, and everything else the extension contributes still loads. An extension published against a slot that is later retired therefore keeps working without that one view. `SessionDockContext`, the props a `session.dock` tool receives: | Field | Type | Notes | | --- | --- | --- | | `instanceId` | `string \| undefined` | Present for a rendered instance; absent for a hidden controller that publishes tabs | | `restoredTabIds` | `readonly string[] \| undefined` | Saved provider-local tab references; restore lazily, without starting resources | | `visible` | `boolean \| undefined` | Hidden instances retain state and should suspend unnecessary work | | `workspace` | `SessionWorkspaceSource` | Discriminated engine or Desktop Cloud authority; described below | | `sessionId` | `string \| null` | `null` on the new-session surface, before a record exists | | `scopeId` | string | Opaque shell-owned dock-layout identity for transient UI state; never workspace authority | | `context` | `SessionContext \| undefined` | `undefined` until a project is chosen | | `hydrated` | boolean | `false` while the workspace is still loading | | `activeTabId` | `string \| null` | This instance's provider-local resource tab; `null` means render a default surface. Controllers also receive `null` | | `requestFocus` | `(tabId?: string) => void` | brings your tool to the front of the dock strip | `SessionContext` is `{ kind: 'dir'; path: string; coreUrl?: string; worktree?: string }`. `SessionWorkspaceSource` has two authority branches: - `engine`: `{ authority: 'engine', context, hydrated }`. The host can address `context.path` through its engine. The Cloud **browser** also uses this branch because its engine is the sandbox. - `cloud`: `{ authority: 'cloud', organizationId, sessionId, state }`. Desktop brokers the Cloud session's files; the private sandbox root is not a local filesystem path. `state` is `connecting`, `ready`, `reconnecting`, `paused`, `read-only`, `unavailable`, or `access-lost`. `paused` is the normal resting state of a finished session whose sandbox was released and can resume, so present it neutrally; `unavailable` means the workspace cannot come back. Before a session exists, `sessionId` is null, `state` is `unavailable`, and `organizationId` may also be null. Use `workspace.authority` to select the supported file path. The older `context` and `hydrated` props are only the engine projection. A dock view that supports engine files alone should render an explicit unavailable state for Desktop Cloud authority. [Lifecycle example](/build/pages-and-windows/#views-in-slots). ### `contributes.commands` Palette entries. Strict. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `id` | string | **yes** | non-empty | | `title` | string | **yes** | non-empty; the palette label | | `keybinding` | string | no | e.g. `"mod+shift+k"` | | `category` | string | no | palette grouping | | `scope` | string | no | non-empty. `focused-view` \| `focused-slot` \| `global`, or a custom string | Declaring a command does not give it behavior — `ctx.api.commands.register` supplies the handler, and nothing registers on your behalf. The declaration is what the marketplace listing shows and what the contract check holds your activation to; a declared command your activation never registers never reaches the palette, and the contract check fails the build naming it. ### `contributes.skills` | Value | Meaning | | --- | --- | | `true` | Scan `/skills` | | `false` | Register nothing | | `"some/dir"` | Load a relative skill root; absolute paths and escapes are rejected | | `["a", "b"]` | Load each relative skill root; results accumulate under one extension id | The string forms name directories, not individual files. A root containing `SKILL.md` is one skill; `"."` names the extension root. Otherwise, its immediate child directories containing `SKILL.md` form a collection. A loose `.md` file is not a skill package. A `skills/` directory without this declaration loads nothing. Skill directories and manifests must stay inside the extension; symlinks escaping it are rejected. `wamp ext pack` currently copies skill packages from `skills/` only. Use `"skills"` or `true` for a packed WAMP extension; custom roots work in a directly loaded extension directory. ### `contributes.services` Declares service ids this extension intends to register. Permissive. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `id` | string | **yes** | non-empty; the id other extensions pass to `services.require` | | `interface` | string | no | documentation only | The declaration is bookkeeping; `ctx.api.services.register` does the actual registering. Consumers read a registered service with `ctx.api.services.require` (throws if the id isn't registered yet) or subscribe with `ctx.api.services.onRegister` to wait for it. ### `contributes.settings` User-editable settings surfaced in the host's settings UI. Strict. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `key` | string | **yes** | `^[a-zA-Z][a-zA-Z0-9._-]*$` | | `title` | string | **yes** | non-empty | | `description` | string | no | helper text | | `kind` | enum | **yes** | `boolean` \| `string` \| `number` \| `enum` | | `default` | any | no | returned by `ctx.api.settings.get` until the user stores an override | | `enum` | string[] | no | the choice list for `kind: 'enum'` | Reads and writes go through `ctx.api.settings`, persisted in the extension's storage under the key prefix `settings:`. ### `contributes.toolMetadata` Display and behavior hints for tools, keyed by tool name. Strict. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `id` | string | **yes** | non-empty; the tool name | | `icon` | string | no | `lucide-react` export name | | `label` | string | no | display label on the tool card | | `category` | string | no | grouping | | `persistenceQuota` | number | no | `>= 0` | | `compactable` | boolean | no | the result may be dropped during compaction | | `longRunning` | boolean | no | relaxes execution timeouts | | `readOnly` | boolean | no | the tool makes no changes | ### `contributes.mcpServers` Each entry registers a runnable MCP server under `_`. Permissive, but with one cross-field rule: `transport: 'http'` or `'sse'` **requires** `url`; anything else **requires** `command`. Violating it fails the manifest with the message "stdio MCP servers require command; http/sse MCP servers require url." Declare an npm-packaged server as `"command": "npx"`, `"args": ["-y", "@", …]`. WAMP installs that exact pin once into its own package store and then runs the package's bin directly, so later starts need neither npm nor the network. A range, a tag or any other launcher is run exactly as declared. A server the extension ships itself names its files with a value that starts with `${extensionDir}/`, in `command`, `args` or an `envVars` default — the rule `contributes.agentRuntimes` uses. WAMP replaces it with the installed path when the extension loads: ```json { "id": "words", "command": "node", "args": ["${extensionDir}/runtime/server.mjs"] } ``` Keep the server under `runtime/`, which `wamp ext pack` ships as-is; every build replaces `dist/`. The token must start the value and name a path inside the extension. `wamp ext pack` refuses any other use of it, and a path that is not in the archive. The server's working directory is the open project, not the extension, so it finds its other files from its own path (`import.meta.url`). Other `${NAME}` and `${NAME:-default}` references in `command` and `args` expand when the server starts, from its `envVars` values and then the engine's environment. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `id` | string | **yes** | non-empty | | `transport` | enum | no | `stdio` \| `http` \| `sse`. Absent means stdio | | `command` | string | no | required for stdio; non-empty | | `args` | string[] | no | stdio only | | `envVars` | array | no | surfaced in Settings so users can edit credentials | | `envVars[].key` | string | **yes** | non-empty | | `envVars[].description` | string | no | — | | `envVars[].required` | boolean | no | — | | `envVars[].default` | string | no | — | | `url` | string | no | required for http/sse; must parse as a URL | | `headers` | `Record` | no | http/sse only | | `oauth.clientId` | string | no | non-empty; skips dynamic registration | | `oauth.flow` | `"device_code"` | no | requires `oauth.clientId`; absent keeps authorization code + PKCE | | `wampAuth.audience` | string | no | WAMP-managed identity for a registered resource slug; http/sse over HTTPS only | Prefer `http` (Streamable HTTP) over `sse`; `sse` exists because several hosted servers still ship SSE-only endpoints. OAuth flows are run by the host — the extension never sees tokens. A pre-registered public client may select device authorization with `{ "clientId": "…", "flow": "device_code" }`; the bundled GitHub MCP connector uses this path. `wampAuth` is mutually exclusive with `oauth` and a static `Authorization` header. The host requires the URL origin to match the origin registered for that audience; see [Connected resources](/identity/connected-resources/). ### `contributes.agentRuntimes` An external process that can answer a chat turn — the agent analogue of an MCP server. Strict at every level, including the nested `home`, `usage`, `limits`, and `identity` objects. `command`, `args`, `env` and a terminal login's `command` and `args` name a file the extension ships the way an MCP server does: a value starting `${extensionDir}/`. | Field | Type | Required | Notes | | --- | --- | --- | --- | | `id` | string | **yes** | `^[a-z][a-z0-9-]*$`; stored on the chat | | `label` | string | **yes** | non-empty | | `transport` | literal | **yes** | `"acp"` — the only accepted value | | `command` | string | **yes** | non-empty; executable speaking the protocol on stdio | | `args` | string[] | no | — | | `env` | `Record` | no | layered over the inherited environment | | `icon` | string | no | brand-icon registry key | | `requires` | string | no | the underlying CLI, probed to report "not installed" | | `modelAccess` | literal | no | `"host"` requests the host's metered model plane; requires the extension's `ai` permission | | `steering.method` | string | **yes** within `steering` | `^_[a-zA-Z0-9][a-zA-Z0-9._/-]*$`; private ACP request for mid-turn delivery | | `steering.idleBehavior` | literal | **yes** within `steering` | `"promptRequired"`; an idle request must reject without consuming its content | | `sessionFork.point` | literal | **yes** within `sessionFork` | `"jetbrains.air.v1"`; qualified exact-message native forks, requiring live fork and resume/load capabilities | | `tools` | string[] | no | WAMP tools lent over MCP. Omitted lends only `present_files`; `[]` lends nothing | | `authMethods` | array | no | `min 1` entries; ACP method ids or declarative headless login commands, in order. Omitted means every ACP method | | `home.env` | string | **yes** within `home` | non-empty; env var that repoints the agent at a WAMP-owned directory | | `home.credentials` | string[] | **yes** within `home` | `min 1`; files inside that directory carrying the login | | `home.defaultDir` | string | no | inherited machine login location when `home.env` is unset; read-only | | `home.continuationRoots` | string[] | no | non-empty safe relative POSIX directories eligible for exact native-session resume | | `skills.handover` | string \| string[] | **yes** within `skills` | `"directory"`, `"plugin-root"`, `"agents-root"`, distinct, tried in order | | `skills.directory` | string | **yes** when `handover` includes `"directory"` | safe relative POSIX directory inside the managed home that receives the skill-set link | | `mcpServers` | string[] | no | non-empty namespaced server ids; WAMP starts a private copy for each ACP session and offers the ones that start within 30 s | | `usage.kind` | `"http" \| "service"` | **yes** | selects the usage provider | | `usage.url` | string | **yes** for HTTP usage | must parse as a URL | | `usage.credential` | string | **yes** for HTTP usage | non-empty; file in the home holding the token | | `usage.tokenPath` | string | **yes** for HTTP usage | non-empty; dotted path to the token in that file | | `usage.headers` | `Record` | no | HTTP form only | | `usage.windows` | array | **yes** for HTTP usage | `min 1` of `{ label, path }`, both non-empty; dotted paths to `{ utilization, resets_at }` | | `usage.service` | string | **yes** for service usage | extension-owned service id implementing `AgentRuntimeUsageProvider` | | `limits.patterns` | string[] | **yes** within `limits` | non-empty quota-exhaustion patterns; case-insensitive | | `limits.transient` | string[] | no | non-empty throttle patterns; report without benching or rotating the account | | `limits.terminalAuth` | array | no | exact structured authentication failures: `{ code, data?: { path, equals } }`; `code` is integer and `equals` is string, finite number, boolean, or null | | `identity.kind` | `"declarative" \| "service"` | no for declarative; **yes** for service | omitted remains the backward-compatible declarative form | | `identity.credential` | string | **yes** for declarative identity | non-empty | | `identity.path` | string | no | dotted path to the identity inside `credential` | | `identity.claim` | string | no | JWT claim name, when the value at `path` is a JWT | | `identity.url` | string | no | must parse as a URL — the endpoint mode | | `identity.tokenPath` | string | no | dotted path to the bearer, for endpoint mode | | `identity.headers` | `Record` | no | — | | `identity.paths` | string[] | no | `min 1`; response paths tried in order | | `identity.service` | string | **yes** for service identity | extension-owned service id implementing `AgentRuntimeIdentityProvider` | `steering` requires both this declaration and the agent's initialize response `_meta.steering.supported`; a declaration alone does not enable delivery. `sessionFork` is optional. Declare it only after verifying that native forks preserve the exact selected prefix, including compacted context. WAMP retains private message boundaries and loads the fork before its first prompt; it does not clone vendor transcript files. Unsupported adapters and older conversations without those boundaries continue through retained portable history. `modelAccess: "host"` lets the host choose the model endpoint and credential; you cannot override those through `env`. `args[]`, `env` values, and object `authMethods[].args[]` may contain a whole `${npmPackage:@}` value, for example `${npmPackage:@agentclientprotocol/claude-agent-acp@0.84.0}`. The version must be exact, without build metadata. The engine replaces the value with an absolute, read-only package directory before preparing credentials or starting the process. It installs runtime packages without lifecycle scripts, so an adapter must use shipped files as-is and write derived data under its own runtime cache. Commands and substrings cannot contain this placeholder. A pack using it must declare `compat.pluginApi: ">=4.0.0 <5.0.0"`; older hosts refuse the pack. `skills` hands the user's WAMP skill set to the agent. WAMP stages one content-addressed copy of the set in its own store and hands it over through the first `handover` kind whose precondition holds. `"directory"` links the staged set at `skills.directory` inside a WAMP-managed home and therefore requires `home`; it never writes into the machine's own vendor login. `"plugin-root"` passes the set as a local plugin directory for one session, in both adapter fields that accept one: Claude Code's `_meta.claudeCode.options.plugins` and Grok Build's `_meta.pluginDirs`. `"agents-root"` passes it as an extra `.agents/skills` root through `_meta["dev.wamp/skillRoots"]`, which the Codex adapter accepts without widening its sandbox. When no kind applies, the runtime reports the omission instead of copying skills anywhere. Declare `handover` explicitly; a `directory` value also requires a managed `home`. `mcpServers` selects configured MCP servers independently of `tools`, which lends the WAMP tool registry. A selected server is offered when it starts. One that does not start within 30 s is left out of that session, the transcript says why, and the turn runs without it. Each `authMethods` entry is either an ACP method id string or a strict object `{ id, name, description?, command, args?, input? }`. Use the object only when the ACP agent exposes a local-browser login but the same vendor CLI provides an official remote-safe command. WAMP runs it in the managed `home`; set `input: false` for device-code commands that poll after printing their URL/code. `usage` and `identity` are only meaningful alongside `home`, which supplies the managed credential layout and optional inherited `defaultDir`. The HTTP usage form covers one bearer `GET`; declarative identity covers JSON, JWT, and one bearer `GET`. For dynamic headers, RPCs, fallback endpoints, vendor payloads, or dynamic auth-file layouts, register an extension service and declare `{ "kind": "service", "service": "…" }`. The service must belong to the same extension and implement `AgentRuntimeUsageProvider` or `AgentRuntimeIdentityProvider`. It receives the credential home currently used by the runtime plus an abort signal and must remain read-only. WAMP owns service ownership checks, timeout, normalization, caching, and persistence of the credential-free result. An inherited machine login is read in place; it is never copied into WAMP or added to account rotation by a metadata probe. ### `contributes.ipcNamespaces` Namespace prefixes this extension claims for its own IPC channels. | Constraint | Value | | --- | --- | | Type | string[] | | Each entry | `^[a-z][a-z0-9._-]*$` | Claims happen at registration, before first use. A channel whose prefix is not declared is refused by `ctx.api.ipc.broadcast`, synchronously. Two extensions claiming the same prefix is a collision: the second faults and does not load. Prefix with your extension id. ## Strict and permissive objects Two failure modes, and which one you get depends on where the typo is. A strict object rejects unknown keys and names the offending field; a permissive one accepts and silently ignores them. | Object | Unknown keys | | --- | --- | | Manifest root | ignored | | `contributes` | ignored | | `contributes.pages[]` | **rejected** | | `contributes.commands[]` | **rejected** | | `contributes.settings[]` | **rejected** | | `contributes.toolMetadata[]` | **rejected** | | `contributes.agentRuntimes[]` and its nested objects | **rejected** | | `build` and `build.loaders[]` | **rejected** | | `{ "auth.outbound": [...], "auth.resource": [...] }` and each `auth.resource` entry | **rejected** | | `contributes.services[]` | ignored | | `contributes.views[]` | ignored | | `contributes.mcpServers[]` | ignored | A rejected manifest is reported as a failed scan with the field path, and the extension does not appear. An ignored key costs you a debugging session, so when something you declared has no effect, check the spelling of the key one level up first. ## A complete manifest Every optional field is left out except the ones this extension needs. It has a page, a palette command, and a tool contributed at runtime from `main`. ```json { "name": "Release Watch", "description": "Lists release feeds and files a task on refresh.", "version": "1.4.0", "compat": { "pluginApi": "^4.0.0" }, "icon": "Radar", "author": "Example Corp", "main": "dist/main.js", "permissions": ["notifications"], "activationEvents": ["onStartupFinished"], "contributes": { "pages": [ { "id": "release-watch", "title": "Releases", "icon": "Radar", "context": "both" } ], "commands": [ { "id": "release-watch.refresh", "title": "Releases: Refresh now", "keybinding": "mod+shift+r" } ], "toolMetadata": [ { "id": "release_watch_check", "label": "Check releases", "readOnly": true } ], "ipcNamespaces": ["release-watch"] } } ``` ## Related - [The manifest](/build/manifest/) — which fields to set and why. - [Permissions](/build/permissions/) — what each declaration actually unlocks. - [Plugin API reference](/reference/plugin-api/) — the namespaces each permission adds. - [Troubleshooting](/reference/troubleshooting/) — manifest mistakes that fail quietly.