symfonic.platform.extensions¶
extensions ¶
Extensions as something a composition root can compose.
Why this lives in platform. The extensions package is contained by its own suite: it may import only itself and the shared error taxonomy, so that a capability cannot reach into the engine, the legacy plugin surface or a transport. Binding it to the kernel needs the contribution contracts and the prompting types, and putting those imports inside would relax a rule that is doing its job -- the same reason the governance door sits here.
The package already merges providers: each one answers contribute() with an
:class:ExtensionContribution, and :func:compose folds them into one
ComposedExtensions with the name collisions resolved. What was missing was
the step after -- turning that into the kernel's CapabilityContribution --
and the collision is why nobody noticed. ExtensionProvider.contribute()
takes no argument and returns an extension contribution; the kernel's
contribute(request) takes one and returns a capability contribution. Two
different methods with one name, so a structural check for "exports something
with contribute()" reports this package as composable and it is not.
What an extension may contribute, and what it may not.
Tools and prompt fragments pass through, by different routes. Tools go in the
capability contribution, which has a field for them. Fragments come out of
:attr:ExtensionCapability.sources and are handed to the prompting capability,
because that is what compiles a prompt and a capability contribution has no
field for one. Both are bounded: a tool is admitted by name against the
reserved set, and a fragment carries its own layer, tier and order, so a plugin
cannot promote its text above the deployment's own.
Lifecycle hooks are delivered, but not through a stage. install runs when
the extensions are composed and teardown when :meth:aclose is called --
the close path the host already walks, since teardown is a kernel-owned
phase no capability may register into. The MCP adapter's only hook is
mcp.close, so refusing it outright would have blocked the commonest
adapter over a hook that had somewhere to go all along.
Policies do not pass. Nothing on a kernel turn asks a plugin to veto an action, so an extension that declared one would be a deployment believing it has a check it does not have. Those are refused at composition: a policy that never runs is not a weaker guarantee than one that does, it is a false one.
Nothing here widens authority. Grants stay whatever the plan already granted, which is the bound the fold's grant checking exists to enforce -- a capability that could ask for more by being composed would make the grant list a description rather than a limit.
ExtensionCapability ¶
The composed extensions, in the shape Agent accepts.
Source code in src/symfonic/platform/extensions.py
composed
property
¶
The merged extension set, for a caller that wants the diagnostics.
sources
property
¶
The extensions' prompt fragments, for PromptingCapability.
capabilities=[
exts,
PromptingCapability(sources=[*persona, *exts.sources]),
]
Every fragment is declared untrusted. A plugin's text is not the deployment's own, and rendering it verbatim at an authored tier is how an extension writes instructions nobody in the deployment approved.
aclose
async
¶
Run the teardown hooks the composed extensions declared.
Called by whatever owns this capability -- the host, for a generated
app. teardown is a kernel-owned phase, so a capability cannot
register a stage there; the hook reaches its moment through the close
path instead. Every hook runs even if an earlier one raises, because a
server left open by a failed close is worse than a traceback.
Source code in src/symfonic/platform/extensions.py
contribute ¶
Offer the extensions' tools to the turn.
Tools only. CapabilityContribution carries tools, stages, handlers
and grants -- there is no field for a prompt contribution, because the
prompt is compiled by the prompting capability from sources. So the
fragments come out of :attr:sources instead and go where every other
source goes, which is the same answer the knowledge door reached.
request is read for its grants and found to need none: an extension
contributes what it declared and performs no effect of its own.
Source code in src/symfonic/platform/extensions.py
extensions ¶
extensions(providers: Sequence[Any], *, reserved_tool_names: frozenset[str] = frozenset()) -> ExtensionCapability
Compose extension providers into one capability.
Agent(provider, capabilities=[extensions([McpExtensionAdapter(...)])])
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
providers
|
Sequence[Any]
|
objects answering the extension |
required |
reserved_tool_names
|
frozenset[str]
|
names the deployment has already bound, so an extension cannot shadow one. |
frozenset()
|
Raises:
| Type | Description |
|---|---|
ExtensionContractError
|
if a provider declares a policy. Nothing on a kernel turn consults one, and accepting it would report a check that never runs. |