Skip to content

symfonic.agent.cutover.authority

authority

The 11.0 governance and execution-authority contract (TA8.43, C1-K).

TA8.25's subgroup C1-K is three rows with one owner -- "governance and execution authorization": credential_patterns, fabrication_refuse_min_confidence and is_admin. The triage recommended REPLACE for all three, on the argument that "raw credential regexes, a fabrication threshold and a boolean admin flag are legacy security mechanics; governance outcomes survive, but the old fields should yield to typed policy and authenticated claims". This lane applies that recommendation to all three, and this module is the contract 11.0 publishes for them -- written before the guard below and the tests next door depend on it. The rows themselves are one table in :mod:~symfonic.agent.cutover.governance_surfaces.

The replacement, in one sentence each.

  • credential_patterns -> GovernanceCapability.compose(patterns=...), which builds :class:~symfonic.capabilities.governance.stages.credentials.CredentialHygieneStage.
  • fabrication_refuse_min_confidence -> GovernanceCapability.compose(refuse_min_confidence=..., min_confidence=...), which builds :class:~symfonic.capabilities.governance.stages.fabrication.FabricationStage.
  • is_admin -> :class:~symfonic.platform.values.AuthenticatedPrincipal's is_admin, derived once per request by a :class:~symfonic.platform.ports.ScopeResolver and bound to the turn by :func:bind_admin_authority.

Why all three are REPLACE and not RETIRE. The safety outcome survives in every case -- a deployment that tuned credential hygiene can still tune it, one that calibrated a fabrication refusal can still calibrate it, and an operator who could bypass a tenant's budget breaker can still bypass it. What does not survive is the spelling, and with it the property that makes each of the three a legacy mechanic: a regex list on a config object, a single grade doing the work of two, and an authority the caller grants itself. Each is a breaking change and each is stated as one -- :attr:~symfonic.agent.cutover.governance_surfaces.GovernanceSurface.adopter_break carries what an adopter loses per row, and every refusal message carries the replacement, so a migration is never inferred from a dead end.

Two mechanisms, because there are two kinds of row. The two configuration fields are ordinary :data:~symfonic.agent.cutover.retired_settings.RETIRED_SETTINGS rows and raise :class:~symfonic.agent.cutover.config_retirement.RetiredConfigurationError. is_admin is a per-call argument and cannot be one of those rows: that table's predicate is getattr(config, name) against a flat stock default, and a parameter has no attribute on the config object to read. It is refused here instead, by :func:refuse_self_asserted_admin, raising :class:SelfAssertedAuthorityError -- a subclass of :class:~symfonic.agent.cutover.retirement.RetiredArgumentError, on the same line, so an adopter's existing except keeps catching it and one who wants to tell an authority refusal from the three TA8.18 retirements can catch the narrower class. It is deliberately not folded into :data:~symfonic.agent.cutover.retirement.RETIRED_ARGUMENTS: those keys are exactly the names the envelope's per-call guard loop carries and TA8.4's reachability probe drives, and the admission inventory records is_admin as in_guard_loop: false with decided_by: null. Putting it in that table would make one vocabulary carry two kinds of fact, which is the reason RetiredArgumentError is named separately from RetiredLeverError in the first place.

Before dispatch, never from the verdict. The guard runs at the top of run/stream/stream_typed beside :func:~symfonic.agent.cutover.config_retirement.refuse_retired_setting, and for that function's reasons: stream does not consult the same verdict on every reported line, and a rolled-back switch would re-honour the keyword on the legacy body. There is no release-line branch; 9.12 and 10.4 keep the keyword in their own artefacts because this module is not backported.

What survives is bound, not passed. The point of the is_admin replacement is that the bit stops being a value the calling code chooses, so :func:bind_admin_authority will not accept a bare boolean. It takes an object that names the principal it authenticated -- an AuthenticatedPrincipal satisfies it structurally, and :class:AdminAuthority is the shipped adapter value for a host whose verifier is the legacy request-stashing kind -- and it refuses an admin claim whose principal_id is empty. "True" on its own is then not a sayable authorisation anywhere on this line, which is the whole difference between a replacement and a rename.

What this module does NOT claim. No cutover switch moved. The two configuration fields' surviving spellings live on a capability the composition root wires, which the migrated invocation envelope does not admit either -- so this changes which surface an adopter configures, not which body serves the turn. Reading a REPLACE as though the capability had reached the kernel is the mirror image of the error that collapses REPLACE into MIGRATE, and it would be just as wrong.

One boundary, stated rather than discovered. The configuration refusal covers the three public invocation entry points, which is the surface every guard in this package covers. SymfonicAgent.scrub_properties is public and a host may call it without invoking the agent; such a call still uses whatever credential_patterns compiled at construction and meets no refusal. That is a bound on this lane's claim, not a hole in the mechanism, and it is written here so nobody has to rediscover it from an absence.

AdminAuthority dataclass

AdminAuthority(principal_id: str, is_admin: bool = False)

An admin claim a host derived, and the principal it derived it for.

The adapter value for a deployment whose authentication is the legacy request-stashing verifier rather than a platform :class:~symfonic.platform.ports.ScopeResolver. A resolver's :class:~symfonic.platform.values.AuthenticatedPrincipal already satisfies what :func:bind_admin_authority reads, so it is bound directly and this class is not in its way.

principal_id is required and not defaulted. That is the type doing the contract's work: the legacy keyword's defect was that True said nothing about who, and a claim value with an optional identity would have reintroduced the same anonymous boolean at a longer address.

SelfAssertedAuthorityError

SelfAssertedAuthorityError(argument: str, entry_point: str, message: str)

Bases: RetiredArgumentError

is_admin=True was supplied to an entry point on the 11.0 line.

Subclasses :class:~symfonic.agent.cutover.retirement.RetiredArgumentError rather than standing alone, and the ancestry is the claim: this is a retired per-call argument, refused before dispatch with the same message shape, the same entry_point attribute and the same :data:~symfonic.agent.cutover.retirement.LEVER_RETIREMENT_LINE. Only the table it is not in differs, and the narrower class is what lets a handler tell an authority refusal from the three TA8.18 retirements without parsing prose.

Source code in src/symfonic/agent/cutover/retirement.py
def __init__(self, argument: str, entry_point: str, message: str) -> None:
    super().__init__(message)
    #: Which retired argument was supplied.
    self.argument = argument
    #: The entry point it was supplied to (``run`` / ``stream`` /
    #: ``stream_typed``), so a handler can tell a refused blocking turn
    #: from a refused stream without parsing prose.
    self.entry_point = entry_point
    #: The line that retired it. Read from :data:`LEVER_RETIREMENT_LINE`
    #: rather than re-spelled, so the attribute and the message cannot
    #: disagree.
    self.line = LEVER_RETIREMENT_LINE

UnauthenticatedAdminClaimError

Bases: CutoverPathError

An admin claim was bound without naming the principal it belongs to.

Subclasses :class:~symfonic.services.shadow.errors.CutoverPathError, and through it ConfigurationError and SymfonicError, for the reason every other error in this package gives for its ancestry: an adopter's existing except around the agent API keeps catching this. A widening, never a rename.

Named separately from :class:SelfAssertedAuthorityError because the two are different incidents. That one is "you asked for a keyword this line retired"; this one is "you used the replacement and it is not carrying an identity", which is a host wiring its own authentication wrongly and is the failure that would otherwise let the replacement decay back into the thing it replaced.

admin_authority

admin_authority() -> bool

The bound principal's admin bit, or False when nothing is bound.

The single place the engine asks "is this turn an administrator's?", so run, stream and stream_typed cannot answer it differently. It is read after :func:refuse_self_asserted_admin has run, so the keyword's value can never contribute: the two are one decision expressed as a refusal and a derivation rather than as a precedence rule, because a precedence rule between an authenticated claim and a self-asserted one is a rule that can be got backwards.

Source code in src/symfonic/agent/cutover/authority.py
def admin_authority() -> bool:
    """The bound principal's admin bit, or ``False`` when nothing is bound.

    The single place the engine asks "is this turn an administrator's?", so
    ``run``, ``stream`` and ``stream_typed`` cannot answer it differently. It is
    read *after* :func:`refuse_self_asserted_admin` has run, so the keyword's
    value can never contribute: the two are one decision expressed as a refusal
    and a derivation rather than as a precedence rule, because a precedence rule
    between an authenticated claim and a self-asserted one is a rule that can be
    got backwards.
    """
    return _ADMIN_AUTHORITY.get()

bind_admin_authority

bind_admin_authority(claim: Any) -> Iterator[bool]

Bind an authenticated principal's admin bit for the turns inside.

claim is anything carrying principal_id and is_admin -- :class:~symfonic.platform.values.AuthenticatedPrincipal structurally, or :class:AdminAuthority for a host whose verifier stashes facts on the request. None is not an error -- a host that authenticated nobody says so by having nothing to bind, and forcing it to construct an anonymous claim would be forcing it to write the sentence this contract exists to make unsayable -- and it binds False rather than binding nothing. That difference only shows inside an outer binding, and it is the point: None is what InvocationRequest.admin_claim returns for a non-admin request, so a host reusing that spelling to scope a nested turn down would otherwise keep the outer administrator's authority while this function handed it False. The value yielded and the value :func:admin_authority reads are one value in every case, which is the only version of this contract that cannot be read backwards.

Refuses an administrator claim with no principal_id. A non-admin claim with no id is merely uninformative and binds False, which is what it would have meant anyway; an admin claim with no id is the anonymous boolean wearing the replacement's clothes, and it is the one shape this function exists to reject.

A context manager rather than a parameter, because the value has to survive into an async generator the host returns and the caller drives later -- stream and stream_typed are consumed after their handler has returned. The reset tolerates a token minted in another context rather than raising: a generator driven from one task and closed from another would otherwise turn a tidy-up into the turn's exception, and that closing context never held this binding at all -- see the comment on the branch.

Source code in src/symfonic/agent/cutover/authority.py
@contextmanager
def bind_admin_authority(claim: Any) -> Iterator[bool]:
    """Bind an authenticated principal's admin bit for the turns inside.

    ``claim`` is anything carrying ``principal_id`` and ``is_admin`` --
    :class:`~symfonic.platform.values.AuthenticatedPrincipal` structurally, or
    :class:`AdminAuthority` for a host whose verifier stashes facts on the
    request. ``None`` is not an error -- a host that authenticated nobody says
    so by having nothing to bind, and forcing it to construct an anonymous
    claim would be forcing it to write the sentence this contract exists to
    make unsayable -- and it binds ``False`` rather than binding nothing. That
    difference only shows inside an outer binding, and it is the point:
    ``None`` is what ``InvocationRequest.admin_claim`` returns for a non-admin
    request, so a host reusing that spelling to scope a nested turn *down*
    would otherwise keep the outer administrator's authority while this
    function handed it ``False``. The value yielded and the value
    :func:`admin_authority` reads are one value in every case, which is the
    only version of this contract that cannot be read backwards.

    Refuses an *administrator* claim with no ``principal_id``. A non-admin claim
    with no id is merely uninformative and binds ``False``, which is what it
    would have meant anyway; an admin claim with no id is the anonymous boolean
    wearing the replacement's clothes, and it is the one shape this function
    exists to reject.

    A context manager rather than a parameter, because the value has to survive
    into an async generator the host returns and the caller drives later --
    ``stream`` and ``stream_typed`` are consumed after their handler has
    returned. The reset tolerates a token minted in another context rather than
    raising: a generator driven from one task and closed from another would
    otherwise turn a tidy-up into the turn's exception, and that closing
    context never held this binding at all -- see the comment on the branch.
    """
    if claim is None:
        is_admin = False
        principal_id = ""
    else:
        is_admin = bool(getattr(claim, "is_admin", False))
        principal_id = str(getattr(claim, "principal_id", "") or "").strip()
    if is_admin and not principal_id:
        raise UnauthenticatedAdminClaimError(
            "an admin claim must name the principal it was derived for: "
            f"{type(claim).__name__} carries is_admin=True and no "
            "principal_id. On the "
            f"{LEVER_RETIREMENT_LINE} line the admin bypass is a fact about an "
            "authenticated principal, so an anonymous claim is refused rather "
            "than honoured -- it is the retired is_admin=True keyword under a "
            "new name, and honouring it here would undo the replacement it is "
            "part of."
        )
    token = _ADMIN_AUTHORITY.set(is_admin)
    try:
        yield is_admin
    finally:
        # ``ValueError`` means the token was minted in another context -- the
        # shape ``_bound`` takes when sse-starlette drives the frames in one
        # task and closes the generator from another. Suppressed rather than
        # compensated for: the ``set`` landed in the *driving* task's context
        # and this one was never touched, so writing anything here (``False``,
        # or the value seen where the bind was entered, which in this shape is
        # the driving task's and not this context's) cannot reach the context
        # holding the binding and WOULD overwrite an outer binding held by the
        # closing task -- a silent de-escalation of somebody else's turn,
        # dressed as tidy-up. The driving task's copy dies with that task.
        with suppress(ValueError):
            _ADMIN_AUTHORITY.reset(token)

refuse_self_asserted_admin

refuse_self_asserted_admin(entry_point: str, *, is_admin: Any) -> None

Refuse is_admin=True on entry_point, or return.

Emptiness is not use, the rule TA8.26 states for the twenty-five retired fields: is_admin=False is what a caller who claims nothing passes and it is also the parameter's default, so only a truthy value is a request. The keyword stays in the signature for the reason :mod:~symfonic.agent.cutover.config_retirement gives for keeping the fields on the model -- the failure is then a named error citing the line rather than a bare TypeError.

There is no detail parameter, and its absence is the guarantee :func:~symfonic.agent.cutover.retirement.refuse_retired_lever buys by making one required: the next step comes from :data:~symfonic.agent.cutover.governance_surfaces.GOVERNANCE_SURFACES, so every refusal carries one and no call site can forget to pass it.

Source code in src/symfonic/agent/cutover/authority.py
def refuse_self_asserted_admin(entry_point: str, *, is_admin: Any) -> None:
    """Refuse ``is_admin=True`` on ``entry_point``, or return.

    Emptiness is not use, the rule TA8.26 states for the twenty-five retired
    fields: ``is_admin=False`` is what a caller who claims nothing passes and it
    is also the parameter's default, so only a truthy value is a request. The
    keyword stays in the signature for the reason
    :mod:`~symfonic.agent.cutover.config_retirement` gives for keeping the
    fields on the model -- the failure is then a named error citing the line
    rather than a bare ``TypeError``.

    There is no ``detail`` parameter, and its absence is the guarantee
    :func:`~symfonic.agent.cutover.retirement.refuse_retired_lever` buys by
    making one required: the next step comes from
    :data:`~symfonic.agent.cutover.governance_surfaces.GOVERNANCE_SURFACES`, so
    every refusal carries one and no call site can forget to pass it.
    """
    if not is_admin:
        return
    _refuse(entry_point)