symfonic.agent.cutover.guardrails¶
guardrails ¶
The enforcement point for contributed guardrails (TA8.21).
One function, in its own module, because it is a named thing: the acceptance for this task asks for guardrail contributions to be carried "with their enforcement point named by file and symbol", and a symbol buried among the composition helpers is named the way a needle is named by its haystack.
:func:enforce_guardrails is reached from exactly one caller,
symfonic.agent.engine.SymfonicAgent.validate_action, in the position that
method's own inline loop over self._plugins used to occupy. One derivation
site rather than two that agree by inspection is the shape TA8.19 gave
session_id, and it is what makes "both routes enforce the same guard" a
property instead of a claim -- there is no second implementation to drift.
What it replaced could not have served an admitted turn at all: a policy contributed by a plugin was a declaration nothing folded, so the kernel route had no guardrail path whatsoever.
enforce_guardrails
async
¶
enforce_guardrails(capability: Any, action: str, context: Mapping[str, Any], *, plugins: Sequence[Any] = (), quarantined: Sequence[Any] = (), warned: MutableMapping[str, int] | None = None) -> bool
Ask every contributed policy about action; False if one refuses.
Answers bool because that is what the public method has always answered
and an adopter's if not await agent.validate_action(...) has to keep
working. The richer
:class:~symfonic.capabilities.extensions.composition.PolicyOutcome -- the
verdict and every guard that failed to answer -- is available from
ExtensionSurface.decide for a caller that wants it, which is what makes
the abstention below a value rather than only a log line.
None with no plugins is an allow, and it is the branch every agent
in the fleet takes: an agent that loaded no plugin folded no capability and
has no contributed guard to consult.
None with plugins loaded is not the same fact, and is not treated as
one -- see :func:_unfolded_outcome. The capability is the ordinary route
and the plugin list is the floor beneath it, so a build that failed costs
the fragments and the tools it was carrying and does not cost the vetoes.
warned is the caller's latch for the report that path emits, and is
passed through untouched; it changes no verdict.
quarantined are the plugins whose admission this agent refused. They
are asked first and deny-only -- see :func:_quarantine_outcome. Nothing
about them is registered; what a refusal may not do is delete a veto the
adopter installed.
Fail-closed. A guard that raises, or answers something that is not a verdict, abstains -- and an abstention denies. That closes SEC-FCP-4 / TM-17, filed HIGH against the inline loop this replaced, which swallowed the exception and allowed. The abstention is still a value rather than a swallowed exception, so "the only guard with an opinion was down" and "nobody objected" remain different readings of a turn; they simply no longer produce the same verdict.