symfonic.capabilities.extensions.bridge¶
bridge ¶
The plugin compatibility bridge — a legacy plugin as an extension (T4.2.2).
Every plugin an adopter has already written keeps working. The bridge reads one of them and answers typed contributions, so nothing downstream needs to know which API the plugin was written against.
What it maps, and what each mapping costs:
inject_contributions / inject_system_prompt
→ :class:~.declarations.PromptFragment per contribution, via
:class:~.legacy_prompts.LegacyPromptReader. That module owns the position
and tier mapping and the argument for each.
validate_state_transition
→ one :class:~.declarations.PolicyContribution. False becomes DENY,
True becomes ALLOW, and a raise becomes ABSTAIN — a value naming
the failure rather than a swallowed exception. What an abstention costs
is the enforcement point's decision, not this bridge's:
:func:~symfonic.agent.cutover.guardrails.enforce_guardrails denies on one
(TA8.21, closing SEC-FCP-4). Keeping the distinction here is what lets a
trace say "the only guard with an opinion was down" instead of "denied".
get_domain_tools
→ refused. A non-empty return raises
:class:~.errors.PrivilegeWideningError at construction, keeping the
v8.6.6 refusal but moving it off the engine and giving it a name that says
what it is. Domain tools are constructor input, because the catalogue is
closed when the graph compiles.
install / teardown / aclose
→ :class:~.declarations.LifecycleContribution. Optional; most legacy
plugins define none, and the bridge contributing none for them is the honest
answer rather than a synthesised no-op. A plugin holding an HTTP client
previously had no place to close it, so the client closed when the process
did.
The bridge never touches the plugin. It reads attributes, calls the hooks the plugin already exposes, and holds the results — no attribute is assigned, no method is wrapped in place, and the plugin object leaves a bridge exactly as it arrived.
LegacyPluginBridge ¶
One legacy plugin, read as an extension provider.
Source code in src/symfonic/capabilities/extensions/bridge.py
contribute ¶
The bundle, using whatever the last :meth:harvest produced.
Source code in src/symfonic/capabilities/extensions/bridge.py
contribute_for
async
¶
Harvest this turn's prompts, then answer the whole bundle.
harvest
async
¶
Call the plugin's prompt hook once and keep what it answered.
lifecycle ¶
Bind whichever optional lifecycle hooks the plugin actually defines.
Source code in src/symfonic/capabilities/extensions/bridge.py
policies ¶
One policy when the plugin has a guard, none when it does not.
A plugin without validate_state_transition contributes no policy
rather than an always-allow one. An always-allow stage in a trace is
indistinguishable from a stage that examined the action and approved.
Source code in src/symfonic/capabilities/extensions/bridge.py
tools ¶
legacy_guard_policies ¶
One legacy plugin's validate_state_transition, as a policy — or none.
Module-level rather than a :class:LegacyPluginBridge method because the
guard outlives the bridge: a plugin the bridge refuses still has a veto,
and dropping it would fail open in a way the pre-refactor inline loop over
self._plugins never did.
:func:~symfonic.agent.cutover.guardrails.enforce_guardrails is that
caller -- it asks the engine's quarantine of refused plugins, and the
plugin list itself when no capability folded -- and sharing this function
is what keeps those from being separate implementations of deny-wins that
agree by inspection.
A plugin without the hook contributes no policy rather than an always-allow one: an always-allow stage in a trace is indistinguishable from a stage that examined the action and approved it.
The hook is handed a copy, and that is a narrowing. The inline loop this
replaced passed SymfonicAgent.validate_action's own context object
straight through, so a guard could annotate it and the caller would read the
annotation back. request.context is frozen by
:class:~symfonic.capabilities.extensions.values.PolicyRequest (AS-INT-3 --
a policy answers a question, it does not edit the question) and dict()
of it is a fresh mutable mapping the legacy signature can accept, so that
side-channel is closed on both routes. Named in validate_action's
docstring as the one observable that did change.