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'sis_admin, derived once per request by a :class:~symfonic.platform.ports.ScopeResolverand 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
¶
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 ¶
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
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 ¶
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
bind_admin_authority ¶
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
refuse_self_asserted_admin ¶
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.