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 work7 pages in this section

extension.toml is the daemon's contract for one extension: identity, compatibility floor, provide surfaces, permissions, bundled 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, hooks, bundles, or MCP servers and run no code of their own.

Compozy 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 = ["memory/recall", "memory/store", "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. Compozy 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.

ProvideCompozy calls the extension withPublic
tool.providerprovide_tools, tools/callyes
memory.backendmemory/store, memory/recall, memory/forgetyes
model.sourcemodels/listyes
loop.watch_sourcewatch/pollyes
bridge.adapterbridges/deliver, bridges/targets/snapshotno

bridge.adapter is not yet a third-party surface. An installed manifest declaring it is rejected deterministically; see Develop Extensions for the current position.

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

[permissions]

[permissions]
  requires = ["memory/recall", "sessions/list"]

The Host API methods the extension calls, validated against the closed 87-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.

FieldLoaded as
resources.skillsMarkdown skill files parsed like normal SKILL.md files.
resources.agentsAgent definition directories containing AGENT.md.
resources.loopsLoop definitions.
resources.bundlesBundle specs packaging agents, channels, automation, layouts, bridges.
resources.hooksHook declarations, stamped with source extension.
resources.toolsExtension-host tools. Generated from SDK registrations.
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.

[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.hooks]]

Hooks declared through the SDK's supported hook events are generated with a subprocess executor pointed at the same binary. A resource-only extension writes them by hand:

[[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.

[bridge]

Provider metadata for bridge.adapter extensions. In-tree only today.

[bridge]
platform = "slack"
display_name = "Slack"

[network_participation]

Declares that bundle-owned work needs Compozy Network participation.

[network_participation]
required = true
mode = "live"
channel_scopes = ["builders", "release"]
FieldRequiredNotes
requiredyesfalse cannot carry a mode or channel scopes.
modeyesMust be "live" when the requirement is enabled.
channel_scopesnoTrimmed, deduplicated, sorted, and digested into the bundle activation requirement digest.

Compozy hashes the normalized block into the bundle activation's Network requirement digest. Activation requires explicit operator confirmation of that exact version; a changed digest clears the prior confirmation and blocks projection until the operator confirms again. Declared channels remain inventory — confirmation enrolls no execution into Live.

Resource-only manifests

No code, no subprocess, no permissions:

[extension]
name = "review-pack"
version = "0.1.0"
description = "Review skills, hooks, and MCP helpers"
min_compozy_version = "0.3.0-beta.1"

[resources]
skills = ["skills"]
agents = ["agents"]
bundles = ["bundles"]

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

These become active as soon as Compozy loads and registers their resources. Install them from a local directory or publish them 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