Skip to content

Extension Manifest

The single source of truth for extension.toml — generated for subprocess extensions, handwritten for resource-only packages.

For people running agent work11 pages in this section

extension.toml is the daemon's contract for one extension: identity, compatibility floor, provide surfaces, permissions, packaged resources, and how to launch the subprocess.

For a subprocess extension you do not write it. compozy extension build runs your built binary in describe mode and generates the manifest into an immutable generation directory. Editing a generated manifest by hand means editing a file the next build overwrites — change the code instead.

You write extension.toml by hand only for resource-only extensions: packages that ship skills, agents, Loops, automation, or layouts and run no code of their own.

A native resource-only source can use extension build, dev, reload, and dev --watch when it declares at least one skill, agent, Loop, automation, or layout path. Build validates the handwritten manifest and copies those resource trees without running a build or describe command. Top-level subprocess behavior, runtime capabilities, Host API permissions, hooks, tools, MCP servers, command groups, and dynamic resource publication remain code-backed contracts and require package.json or go.mod.

CompozyOS reads extension.toml first and extension.json second. Both formats carry the same schema.

Generated example

This is the complete manifest compozy extension build produced for the quickstart extension:

[capabilities]
  provides = ["tool.provider"]

[extension]
  description = "Search extension-owned data"
  min_compozy_version = "0.3.0-beta.1"
  name = "hello"
  version = "0.1.0"

[permissions]
  requires = ["sessions/list"]

[resources]
  [resources.tools]
    [resources.tools.search]
      concurrency_safe = true
      description = "Search extension-owned data"
      destructive = false
      handler = "search"
      id = "ext__hello__search"
      input_schema = "{\"additionalProperties\":false,\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"type\":\"object\"}"
      open_world = false
      read_only = true
      risk = "read"
      visibility = "model"
      [resources.tools.search.backend]
        handler = "search"
        kind = "extension_host"

[subprocess]
  command = "./bin"

Generation is deterministic: identical source produces byte-identical output, with sorted keys and stable ordering. That is what makes dist/gen-<hash> a content-addressed identity.

[extension]

Core metadata. CompozyOS also accepts these keys at the document root; do not set the same field in both places with different values.

FieldRequiredNotes
nameyesRegistry identity. Must match the installed registry row on daemon start.
versionyesSemantic version.
descriptionnoShown in extension metadata and catalog surfaces.
min_compozy_versionyesSemantic version compared against the running daemon. Stamped by build from the SDK — never hand-written for subprocess extensions.
requires_envnoEnvironment variable names the extension needs. Diagnostics report names, never values; unset names surface as missing_env.

A manifest whose min_compozy_version is above the running daemon is rejected at install and at load. Because build stamps the value from the SDK you compiled against, an extension cannot claim compatibility its SDK does not have.

[capabilities]

[capabilities]
  provides = ["tool.provider"]

provides is a closed set. Registering a tool through the SDK adds tool.provider automatically, so most authors never write this block.

ProvideCompozyOS calls the extension withPublic
tool.providerprovide_tools, tools/callyes
model.sourcemodels/listyes
loop.watch_sourcewatch/pollyes
connectivity.providerconnectivity/establish, connectivity/status, connectivity/teardownyes
forge.providerforge/capabilities, forge/status, forge/pr_createyes

An unknown value fails manifest load with the valid set in the error. There is no shape-only validation and no silent no-op.

A connectivity.provider must be installed globally and declare gateway.private, gateway.public, or both in [gateway].permissions. The selected tier must match a declared permission. Workspace-scoped installs are rejected. See Develop Extensions for the request and trust contract.

A forge.provider must register all three daemon-initiated methods and return the closed capabilities/status/create contract. See Develop Extensions for vocabulary, draft, compare URL, credential-source, and idempotency requirements.

[permissions]

[permissions]
  requires = ["sessions/list", "skills/list"]

The Host API methods the extension calls, validated against the closed method set. Consent areas are derived from it. Full catalog and source ceilings: Extension Permissions.

[subprocess]

[subprocess]
  command = "./bin"
  args = ["serve"]
  health_check_interval = "30s"
  shutdown_timeout = "10s"

[subprocess.env]
  LOG_LEVEL = "info"

[subprocess.secret_env]
  API_TOKEN = "env:MY_TOKEN"

command is resolved relative to the extension root. env values may use {{env:NAME}} to read the daemon process environment and {{config_dir}} for the extension root. secret_env binds resolved secrets; their values are masked in logs, events, and every transport.

A manifest requires a subprocess when it declares subprocess.command, capabilities.provides, or tools.

[resources]

Static assets the daemon loads while the extension is enabled. Paths resolve inside the extension root and may not escape it.

Use tables with path and an optional profile for skills, agents, Loops, automation, and layouts. The previous string-array form, such as agents = ["agents"], is accepted through v0.3.0-beta.30 and normalized to unplaced paths when loaded. Builds emit the table form. Update handwritten sources before v0.3.0-beta.31, when string-array support is removed; replace each string with a table containing the same path, leaving profile omitted to preserve its visibility.

FieldLoaded as
resources.skillsMarkdown skill files parsed like normal SKILL.md files.
resources.agentsAgent definition directories containing AGENT.md.
resources.loopsLoop definitions.
resources.automationTOML files or directories declaring package jobs and triggers.
resources.layoutsStrict window_layout JSON files or directories.
resources.hooksHook declarations, stamped with source extension.
resources.toolsExtension-host tools. Generated from SDK registrations.
resources.cmd_paletteCommands, declarative views, and default shortcut suggestions.
resources.command_groupsPresentation-only command groups. Generated from commandGroup(...).
resources.mcp_serversNamed MCP server declarations.
resources.publishRequested resource families and scope ceiling for resources/snapshot.

Every static resource declaration accepts an optional profile field, naming one of the operator's profiles. Omit it to publish the resource in every profile. Set it to a profile name to publish only when that profile exists and the extension is enabled there:

[[resources.skills]]
path = "skills/release-notes"
profile = "marketing"

[[resources.skills]]
path = "skills/code-review"

The same placement field applies to skills, agents, Loops, automation, layouts, hooks, tools, MCP servers, command groups, and command-palette commands and views. A placement for an absent profile is dormant: the extension remains valid, but CompozyOS does not publish that resource until the profile exists. The extension detail response lists dormant placements and provides the profile create action.

[[profiles]]

An extension can declare profiles that installation creates when their names do not exist:

[[profiles]]
name = "marketing"
color = "#5FBF85"
icon = "chart-line"

[profiles.defaults]
agent = "growth-analyst"
provider = "openai"

[[profiles.credentials]]
provider = "openai"
slot = "api_key"
FieldRequiredNotes
nameyesNormalized profile name. Reserved names and invalid profile grammar fail validation.
colornoSeeded identity color.
icon or emojinoSeeded identity mark. Set at most one.
profiles.defaultsnoInitial agent and provider values.
profiles.credentialsnoVault-backed provider and slot requirements shown as needs setup until filled.

Declarations seed only a newly created profile. An existing name is bound without changing its identity or defaults. Updates create only newly declared names and never rewrite existing profiles. Removing the extension leaves profiles and their credential requirements in place. If an operator deletes a declared profile, the extension marker prevents a later update from recreating it.

Install preview reports each declared profile as create or bind, lists credential needs, and names every placement before the install mutates state.

[resources.mcp_servers.<name>]

A native extension manifest may package a local stdio server or a remote HTTP server:

[resources.mcp_servers.local-search]
transport = "stdio"
command = "uvx"
args = ["mcp-server-git"]
env = { REPO_ROOT = "{{env:REPO_ROOT}}" }

[resources.mcp_servers.deployment-api]
transport = "http"
url = "https://deploy.example.com/mcp"
headers = { "X-Client" = "acme-tools" }
FieldNotes
transportstdio for a command or http for a remote URL.
commandRequired executable for stdio; resolved with the extension runtime environment.
argsOptional argument list for stdio.
envNon-secret environment values for stdio.
secret_envVault-backed environment names for stdio; values never appear in reads or logs.
urlRequired endpoint for http; non-loopback hosts must use HTTPS.
headersNon-secret remote headers. Bind credential headers through the extension secrets surface.

These fields belong to packaged extension resources. They do not add headers to the workspace mcp.json configuration sidecar. Agent Plugins packages use type: "streamable-http"; ingestion normalizes that portable name to the native http transport.

[resources.tools.<handler>]

Generated from each SDK tool registration. Fields the daemon reads:

FieldMeaning
idCanonical tool ID, ext__<extension>__<tool>.
handler, backend.handlerHandler name dispatched over tools/call.
backend.kindAlways extension_host for SDK-registered tools.
input_schema, output_schemaCanonical JSON Schema strings. The digest of input_schema must match what the running extension reports.
risk, read_only, destructive, open_world, requires_interaction, concurrency_safePolicy metadata the tool runtime enforces.
visibilitymodel for agent-callable tools.
commandOptional operator-verb presentation block. See Extension Commands.

Because both the schema and the handler come from one code declaration, the schema-digest drift that a hand-maintained manifest allowed cannot occur.

[resources.cmd_palette]

For subprocess extensions, this table is generated from the SDK definition's resources.cmd_palette field. Resource-only extension.toml files may declare static command-palette entries directly. Tool-backed and programmable entries still require the corresponding code-backed resources. Local IDs are namespaced with the extension name at runtime, tool actions may target only tools from the same manifest, and declarative view sources must be read-only. See Command Palette Contributions for the complete contract and examples.

[[resources.hooks]]

Hooks declared through the SDK's supported hook events are generated with a subprocess executor pointed at the same binary. Hooks require a supported code toolchain; resource-only sources cannot declare executable hooks:

[[resources.hooks]]
name = "review-session-ready"
event = "session.post_create"
mode = "async"
command = "/usr/bin/env"
args = ["bash", "{{config_dir}}/hooks/session-ready.sh"]

Extension hooks carry source extension and default priority 300. See Hook Declaration Format.

[resources.publish]

[resources.publish]
families = ["window_layouts"]
max_scope = "workspace"

Declares the resource families the extension may publish through resources/snapshot and the widest scope it requests. The effective grant is the narrowest of this request, the source tier, the operator policy in [extensions.resources], and the session scope.

[gateway]

Connectivity providers declare the exposure tiers they need:

[gateway]
permissions = ["gateway.private", "gateway.public"]

The permission list is normalized and bound to gateway_requirement_digest. Enable, install, and updates that change this requirement require explicit consent to that exact digest through --confirm-gateway-requirement or confirm_gateway_digest. A stale digest cannot grant access. The selected Gateway tier must be declared. Workspace-scoped connectivity providers are rejected.

Retired manifest entries

Manifests written before the memory removal keep loading. CompozyOS drops these retired entries from a TOML or JSON manifest before it validates the rest:

  • capabilities.provides entries for the removed memory backend surface;
  • permissions.requires entries memory/recall, memory/store, memory/forget, memory.read, and memory.write;
  • the hook matcher keys compaction_reason and compaction_strategy on a [[resources.hooks]] entry. The hook keeps running and observes every compaction; compaction_trigger replaces the two keys. See the hook declaration format.

Each manifest load that dropped anything logs one extension.retired_entries_ignored warning with the extension name and the dropped entries, for example permissions.requires=memory/store or hooks[0].compaction_reason. compozy extension list shows the extension as loaded. An unknown entry outside that retired family keeps failing validation.

The same shim covers automation resources, which load from the TOML files that resources.automation points at. A [[triggers]] entry for the removed memory-consolidation event is skipped, and the skip happens before CompozyOS checks the trigger's name, agent, or prompt. Every job and every other trigger in the file, and every other resource of the extension, still loads. Each skipped trigger logs an extension.retired_entries_ignored warning with the extension name, the resource file, and the retired event name as the dropped entry.

A running extension that calls memory/recall, memory/store, or memory/forget gets the JSON-RPC method-not-found error (-32601), the same response as any other unknown Host API method. The shim that drops the retired entries is removed in v0.6.0. See the migration guide.

Resource-only manifests

No code, no subprocess, no permissions:

[extension]
name = "review-pack"
version = "0.1.0"
description = "Review skills, agents, automation, and layouts"
min_compozy_version = "0.3.0-beta.1"

[[resources.skills]]
path = "skills"

[[resources.agents]]
path = "agents"

[[resources.automation]]
path = "automation"

[[resources.layouts]]
path = "layouts"

Installation is inert. These resources become live only after the extension is enabled, and disable removes the extension-owned records. Install from a local directory or publish like any other extension.

Validate before you ship

compozy extension validate ./dist/gen-<hash> -o json

validate never executes extension code. It reports positioned issues (path, line, column, field, message, severity) plus the derived consent areas, so an author sees the operator's view before an operator does.

On this page