symfonic.agent.cutover.plugin_providers¶
plugin_providers ¶
An adopter's loaded plugins, read as extension providers (TA8.21).
Left :mod:~symfonic.agent.cutover.extensions when that module passed its line
budget, and the seam is a real one rather than a line count: this module owns
the legacy end -- how an object written against the pre-refactor plugin API
becomes something the composer can ask -- and extensions owns the kernel
end, where the answer becomes a CapabilityContribution.
Both halves of what a plugin is handed and how it is reached live here:
:func:plugin_state builds the mapping its prompt hook receives, and
:class:PluginProviders turns the engine's live list into bridges.
Nothing here downgrades a plugin. An earlier revision carried a plugin the
bridge refused -- and a plugin whose name another had already claimed -- as a
guard-only shadow: its veto kept, its prompt fragment dropped, its identity
synthesised, and no error anywhere. That is accept-and-do-less with a
rationale, and it is what this module now refuses to do. A roster that cannot
be composed whole raises :class:~symfonic.capabilities.extensions.errors.ExtensionAdmissionError
out of :meth:PluginProviders.__call__, and the one caller that can act on it
-- SymfonicAgent.load_plugin -- refuses the load before any registry is
touched. What survives a refused plugin is not a downgraded contribution but a
quarantine the engine keeps: see SymfonicAgent.validate_action.
PluginProviders ¶
The engine's live plugin list, read as extension providers.
One bridge per plugin object, kept across turns. Rebuilding them per turn
would re-run LegacyPluginBridge.__init__, which calls the plugin's
get_domain_tools() and re-validates its name on every dispatch -- work
bridge.py states happens once, at construction and never per run.
A callable rather than a fixed sequence, because the roster is read again
on every turn and on every guarded action. It does not reopen the tool
question: :class:~symfonic.capabilities.extensions.harvest.ExtensionSurface
seals that at the compile seam.
A plugin that cannot be bridged, or whose name is already claimed, is
refused whole. Both used to be survivable here -- the first by dropping
the plugin, the second by re-emitting it under a synthetic identity -- and
both were the same defect: an adopter's plugin was accepted by
load_plugin and then served by less than the whole of what it declared,
with the shortfall visible only in a log line. Raising
:class:~symfonic.capabilities.extensions.errors.ExtensionAdmissionError
instead moves the decision to the one place an adopter can act on it. The
engine calls this on a trial roster from load_plugin before it
registers anything, so the raise becomes a refused load rather than an
agent that cannot take a turn.
A refusal never costs a veto. The plugin the engine refused goes into its
quarantine and validate_action still asks it, deny-only; and if a
roster is somehow made unbridgeable after admission -- a plugin appended
to _plugins directly, a name mutated in place -- this raise reaches
_extensions_capability, which answers None, and
:func:~symfonic.agent.cutover.guardrails.enforce_guardrails asks the
whole population directly. There is no path from a refusal to an allow.
Source code in src/symfonic/agent/cutover/plugin_providers.py
plugin_state ¶
The state a plugin's prompt hook is handed on the migrated path (D4).
Built from the turn, not from a rendered HMS template. The legacy
HMS-shaped keys -- TOOL_MANIFEST, MEMORY_CONTEXT, SOUL_SCHEMA
and the rest -- are deliberately absent, and
evidence/RET-PREP/decision-plugin-tier-layer.md D4 is where that is
decided rather than discovered: several of them are the output of the
legacy prompt build this path replaces, so supplying them would mean running
that build in order to feed a plugin whose output then goes somewhere else
entirely.
Read-only, because it is handed to third-party code: a hook that edited the mapping would otherwise change what the next plugin was told about the same turn.