Extension Manifest
The single source of truth for extension.toml — generated for subprocess extensions, handwritten for resource-only packages.
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.
| Field | Required | Notes |
|---|---|---|
name | yes | Registry identity. Must match the installed registry row on daemon start. |
version | yes | Semantic version. |
description | no | Shown in extension metadata and catalog surfaces. |
min_compozy_version | yes | Semantic version compared against the running daemon. Stamped by build from the SDK — never hand-written for subprocess extensions. |
requires_env | no | Environment 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.
| Provide | CompozyOS calls the extension with | Public |
|---|---|---|
tool.provider | provide_tools, tools/call | yes |
memory.backend | memory/store, memory/recall, memory/forget | yes |
model.source | models/list | yes |
loop.watch_source | watch/poll | yes |
connectivity.provider | connectivity/establish, connectivity/status, connectivity/teardown | yes |
forge.provider | forge/capabilities, forge/status, forge/pr_create | yes |
bridge.adapter | bridges/deliver, bridges/targets/snapshot | no |
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.
| Field | Loaded as |
|---|---|
resources.skills | Markdown skill files parsed like normal SKILL.md files. |
resources.agents | Agent definition directories containing AGENT.md. |
resources.loops | Loop definitions. |
resources.automation | TOML files or directories declaring package jobs and triggers. |
resources.layouts | Strict window_layout JSON files or directories. |
resources.hooks | Hook declarations, stamped with source extension. |
resources.tools | Extension-host tools. Generated from SDK registrations. |
resources.command_groups | Presentation-only command groups. Generated from commandGroup(...). |
resources.mcp_servers | Named MCP server declarations. |
resources.publish | Requested 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" }| Field | Notes |
|---|---|
transport | stdio for a command or http for a remote URL. |
command | Required executable for stdio; resolved with the extension runtime environment. |
args | Optional argument list for stdio. |
env | Non-secret environment values for stdio. |
secret_env | Vault-backed environment names for stdio; values never appear in reads or logs. |
url | Required endpoint for http; non-loopback hosts must use HTTPS. |
headers | Non-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:
| Field | Meaning |
|---|---|
id | Canonical tool ID, ext__<extension>__<tool>. |
handler, backend.handler | Handler name dispatched over tools/call. |
backend.kind | Always extension_host for SDK-registered tools. |
input_schema, output_schema | Canonical JSON Schema strings. The digest of input_schema must match what the running extension reports. |
risk, read_only, destructive, open_world, requires_interaction, concurrency_safe | Policy metadata the tool runtime enforces. |
visibility | model for agent-callable tools. |
command | Optional 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"]| Field | Required | Notes |
|---|---|---|
required | yes | false cannot carry a mode or channel scopes. |
mode | yes | Must be "live" when the requirement is enabled. |
channel_scopes | no | Trimmed, 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 jsonvalidate 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.
Related
Develop Extensions
The authoring loop for code-backed and resource-only CompozyOS extensions — build, validate, dev, reload, logs, and publish.
Extension Permissions
Declare the Host API methods an extension calls, read the consent areas CompozyOS derives from them, and understand the ceilings applied per install source.