Skip to content

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.

Source code in src/symfonic/agent/cutover/guardrails.py
async def 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.
    """
    if quarantined:
        refused = await _quarantine_outcome(quarantined, action, context)
        if not _allows(refused, action):
            return False
    if capability is None:
        if not plugins:
            return True
        outcome = await _unfolded_outcome(plugins, action, context, warned)
    else:
        outcome = await capability.surface.decide(action, context)
    return _allows(outcome, action)