Skip to content

symfonic.agent.cutover.switchboard

switchboard

Per-capability routing and metadata-only fallback accounting.

CutoverSwitchboard

CutoverSwitchboard(*, criteria: Mapping[str, SwitchCriteria] | None = None, fallback_sink: FallbackTelemetrySink | None = None, recorder: CutoverCriteriaRecorder | None = None, pin: LegacyPin | None = None)

Per-capability routing between the migrated and the dormant paths.

Source code in src/symfonic/agent/cutover/switchboard.py
def __init__(
    self,
    *,
    criteria: Mapping[str, SwitchCriteria] | None = None,
    fallback_sink: FallbackTelemetrySink | None = None,
    recorder: CutoverCriteriaRecorder | None = None,
    pin: LegacyPin | None = None,
) -> None:
    self._pin: LegacyPin = NO_LEGACY_PIN if pin is None else pin
    self._fallback_sink = fallback_sink
    self._criteria: dict[str, SwitchCriteria] = default_criteria()
    for capability, record in (criteria or {}).items():
        switch_for(capability)
        self._criteria[capability] = record
    self._recorder = recorder or CutoverCriteriaRecorder(ExtensionTrustRegistry())
    self._rolled_back: dict[str, str] = {}
    self._fallbacks: dict[str, dict[str, int]] = {}
    for capability, record in self._criteria.items():
        if not record.complete:
            refuse_retired_lever(
                "unfiled-criteria",
                capability,
                detail=(
                    "Its cutover criteria are unfiled (missing "
                    f"{', '.join(record.missing)}), and an unfiled switch "
                    f"no longer degrades to legacy on the "
                    f"{LEVER_RETIREMENT_LINE} line -- it refuses to build "
                    "the board. Nobody asked for the legacy body here: "
                    "this is what used to happen when the evidence was "
                    "simply missing, which is the one route change that "
                    "was never anyone's decision. File the citation, or "
                    "remove the capability from the ledger."
                ),
            )
        self._file(capability, record)

legacy_pin property

legacy_pin: LegacyPin

The build's pin, as a value. NO_LEGACY_PIN when none was set.

describe

describe() -> str

One line per switch. Names and citations only โ€” never a credential.

Source code in src/symfonic/agent/cutover/switchboard.py
def describe(self) -> str:
    """One line per switch. Names and citations only โ€” never a credential."""
    return "\n".join(
        f"{decision.capability}: {decision.route.value} ({decision.reason})"
        for decision in self.decisions()
    )

file

file(capability: str, criteria: SwitchCriteria) -> CutoverEvidence

File this capability's criteria; flips the switch when complete.

Source code in src/symfonic/agent/cutover/switchboard.py
def file(self, capability: str, criteria: SwitchCriteria) -> CutoverEvidence:
    """File this capability's criteria; flips the switch when complete."""
    switch_for(capability)
    if not criteria.complete:
        raise CutoverPathError(
            f"{capability!r} is missing non-shadow cutover criteria: "
            + ", ".join(criteria.missing)
        )
    evidence = self._file(capability, criteria)
    self._criteria[capability] = criteria
    self._rolled_back.pop(capability, None)
    return evidence

restore

restore(capability: str) -> None

Undo this board's rollback. Refused while the criteria are incomplete.

Also refused while a process-wide override is in force: clearing the instance flag would return LEGACY anyway, and a restore that reports success without moving the route is how an operator concludes the rollback lever is broken.

Source code in src/symfonic/agent/cutover/switchboard.py
def restore(self, capability: str) -> None:
    """Undo this board's rollback. Refused while the criteria are incomplete.

    Also refused while a process-wide override is in force: clearing the
    instance flag would return ``LEGACY`` anyway, and a restore that
    reports success without moving the route is how an operator concludes
    the rollback lever is broken.
    """
    switch_for(capability)
    record = self._criteria[capability]
    if not record.complete:
        raise CutoverPathError(
            f"{capability!r} cannot be restored to the migrated path: "
            "missing " + ", ".join(record.missing)
        )
    held = process_rollback_reason(capability)
    if held is not None:
        raise CutoverPathError(
            f"{capability!r} is held on legacy process-wide ({held}); lift "
            "it with symfonic.agent.cutover.restore_process_wide before "
            "restoring this agent"
        )
    if self._pin.holds(capability):
        # Same argument as the process-wide hold above: clearing the
        # instance flag would return LEGACY anyway, and a restore that
        # reports success without moving the route is how an operator
        # concludes the lever is broken. A pin is unset by rebuilding the
        # agent, because that is where it was set.
        raise CutoverPathError(
            f"{capability!r} is pinned to legacy by this build "
            f"({self._pin.describe()}); a pin is not a lever and cannot be "
            "lifted from a running process. Rebuild the agent without it."
        )
    self._rolled_back.pop(capability, None)

rollback

rollback(capability: str, *, reason: str | None = None) -> None

Refused since 11.0: the board-rollback lever is retired.

Still present, still accepting the arguments it always took, and it raises :class:~symfonic.agent.cutover.retirement.RetiredLeverError naming the line and the capability. reason is optional only so that a call written from muscle memory refuses by name instead of raising TypeError at the operator โ€” it is not read.

switch_for runs first on purpose. A mistyped capability is a different mistake from a retired lever, and answering "that lever is gone" to rollback("invocaton.run") would send the operator looking for a migration note about a capability that does not exist.

Source code in src/symfonic/agent/cutover/switchboard.py
def rollback(self, capability: str, *, reason: str | None = None) -> None:
    """Refused since ``11.0``: the board-rollback lever is retired.

    Still present, still accepting the arguments it always took, and it
    raises :class:`~symfonic.agent.cutover.retirement.RetiredLeverError`
    naming the line and the capability. ``reason`` is optional *only* so
    that a call written from muscle memory refuses by name instead of
    raising ``TypeError`` at the operator โ€” it is not read.

    ``switch_for`` runs first on purpose. A mistyped capability is a
    different mistake from a retired lever, and answering "that lever is
    gone" to ``rollback("invocaton.run")`` would send the operator looking
    for a migration note about a capability that does not exist.
    """
    switch_for(capability)
    refuse_retired_lever(
        "board-rollback",
        capability,
        detail=(
            "It cannot be put back on its legacy body: the route is "
            "derived from filed evidence and there is no "
            "supported way to move it by hand. If the migrated path is "
            "serving turns wrongly, that is a defect to fix or a release "
            "to roll back -- not a route to flip. Installs on 10.4 and "
            "below keep this lever for the length of the compatibility "
            "window; it is retired on this line and not backported."
        ),
    )

rollback_reason

rollback_reason(capability: str) -> str | None

Why this capability is on legacy: process-wide reason wins.

Source code in src/symfonic/agent/cutover/switchboard.py
def rollback_reason(self, capability: str) -> str | None:
    """Why this capability is on legacy: process-wide reason wins."""
    switch_for(capability)
    return process_rollback_reason(capability) or self._rolled_back.get(capability)

route_for

route_for(capability: str) -> Route

The implementation that serves capability in this process.

The process-wide override is read first and cannot be outvoted by an instance: a host that builds one agent per request would otherwise answer KERNEL on every board constructed after the operator rolled the capability back, which is the failure the lever exists to prevent.

Source code in src/symfonic/agent/cutover/switchboard.py
def route_for(self, capability: str) -> Route:
    """The implementation that serves ``capability`` in this process.

    The process-wide override is read first and cannot be outvoted by an
    instance: a host that builds one agent per request would otherwise
    answer ``KERNEL`` on every board constructed after the operator rolled
    the capability back, which is the failure the lever exists to prevent.
    """
    switch_for(capability)
    if process_rollback_reason(capability) is not None:
        return Route.LEGACY
    if capability in self._rolled_back:
        return Route.LEGACY
    if self._pin.holds(capability):
        return Route.LEGACY
    return Route.KERNEL if self._criteria[capability].complete else Route.LEGACY

FallbackTelemetryEvent dataclass

FallbackTelemetryEvent(capability: str, reason_code: str, entrypoint: str = 'unknown')

Safe projection of one fallback; never carries its raw reason.

SwitchDecision dataclass

SwitchDecision(capability: str, route: Route, reason: str, switch: CapabilitySwitch)

Why one capability is on the route it is on.