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 work9 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, bridge metadata, 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 = ["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. 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
memory.backendmemory/store, memory/recall, memory/forgetyes
model.sourcemodels/listyes
loop.watch_sourcewatch/pollyes
connectivity.providerconnectivity/establish, connectivity/status, connectivity/teardownyes
forge.providerforge/capabilities, forge/status, forge/pr_createyes
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.

A connectivity.provider must be installed globally and must declare required Live Network participation with gateway.private, gateway.public, or both in channel_scopes. The selected tier must match one of those scopes. 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 = ["memory/recall", "sessions/list"]

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

[bridge]

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

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

[network_participation]

Declares that enabling the extension requires Compozy Network Live participation consent.

[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 included in the extension requirement digest.

CompozyOS hashes the normalized block into the extension's Network requirement digest. Enable requires explicit confirmation of that exact digest. Update compares the candidate digest before swapping any files and refuses a changed, unconfirmed requirement without changing installed state. Declared channels remain inventory — confirmation does not enroll an execution into Live.

For connectivity.provider, channel_scopes is required and must include gateway.private, gateway.public, or both. Gateway rejects an enable when the selected tier is outside that set.

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 = ["skills"]
agents = ["agents"]
automation = ["automation"]
layouts = ["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