Skip to content

symfonic.agent.cutover.container_semantics

container_semantics

The agent container row's own semantics, rejected by name (TA8.41).

TA8.25 filed config.agent as subgroup C1-A, one row, and recommended REJECT on the field's merits rather than for want of an owner: agent is a typed grouping container, the only value the inventory ever drove through it was agent.model, and every meaningful nested setting has a row and a disposition of its own. Nothing independent is left for the container row to mean.

What is rejected, and what is emphatically not. The agent object is not withdrawn and is not refused wholesale -- doing that would refuse every adopter who pins a model, which is most of them. What 11.0 rejects is the catch-all: semantics attributed to the container that no child row owns. The owned children continue under their own contracts, including :data:~symfonic.agent.cutover.policy.ALLOWED_AGENT_FIELDS' model and recursion_limit and C1-B's replacement for ask_user_pause_ttl_seconds.

Why this is a refusal and not a fallback. Today all three shapes below are already rejected by :func:~symfonic.agent.cutover.envelope._nested_refusal -- a substituted container class has no baseline to compare against, and an unenumerable one has no fields to compare -- but rejected into the legacy body, silently, because the envelope is default-deny. On a line that retires the legacy body that silence stops being a fallback and starts being a drop. This module says the same "no" out loud, before dispatch, on run, stream and stream_typed alike.

Emptiness is not use, the same rule TA8.26 states for the twenty-five retired fields. A child whose owner is not recorded here refuses only when the adopter actually moved it off the stock value; left alone it asks for nothing and passes through untouched. The two structural shapes -- a substituted class, an unenumerable container -- are use by construction, because an adopter does not accidentally supply a different type.

UnownedContainerError

UnownedContainerError(setting: str, entry_point: str, group: str, message: str)

Bases: RetiredConfigurationError

config.agent carried a semantic no child row owns.

Subclasses :class:~symfonic.agent.cutover.config_retirement.RetiredConfigurationError so except RetiredConfigurationError around the agent API keeps catching this -- the same widening-never-a-rename discipline that error states for its own ancestry -- while an adopter who wants to tell the container rejection from the twenty-five retired fields can catch the narrower class.

It is a subclass rather than a fourth sibling because it refuses the same kind of thing at the same moment: a fact about how this agent was built, named before dispatch. The vocabulary difference is the group, which is :data:UNOWNED_CONTAINER_GROUP and is not a retired-settings group.

Source code in src/symfonic/agent/cutover/config_retirement.py
def __init__(
    self, setting: str, entry_point: str, group: str, message: str
) -> None:
    super().__init__(message)
    #: Which retired field was set, spelled as ``FrameworkConfig`` spells it.
    self.setting = setting
    #: The entry point it reached (``run`` / ``stream`` / ``stream_typed``),
    #: so a handler can tell a refused blocking turn from a refused stream
    #: without parsing prose. Every entry point refuses; the attribute
    #: records which one was asked, not which ones would have refused.
    self.entry_point = entry_point
    #: The contractual group the field refuses with. Named on the exception
    #: because "this whole family of legacy consolidation dials is gone" is
    #: a different piece of news from "this one field is gone", and an
    #: adopter migrating twelve settings should be told once.
    self.group = group
    #: The line that retired it. Read from
    #: :data:`~symfonic.agent.cutover.retirement.LEVER_RETIREMENT_LINE`
    #: rather than re-spelled, so the attribute and the message cannot
    #: disagree with the levers or the arguments.
    self.line = LEVER_RETIREMENT_LINE

refuse_unowned_container

refuse_unowned_container(entry_point: str, config: Any) -> None

Reject the container's own semantics on entry_point, or return.

The whole of C1-A's REJECT, expressed once and called from all three public entry points -- the position, and the reasons for it, are :func:~symfonic.agent.cutover.config_retirement.refuse_retired_setting's: a verdict-driven rejection would be route-conditional, and a rolled-back switch would re-honour what this line rejects.

Source code in src/symfonic/agent/cutover/container_semantics.py
def refuse_unowned_container(entry_point: str, config: Any) -> None:
    """Reject the container's own semantics on ``entry_point``, or return.

    The whole of C1-A's REJECT, expressed once and called from all three public
    entry points -- the position, and the reasons for it, are
    :func:`~symfonic.agent.cutover.config_retirement.refuse_retired_setting`'s:
    a verdict-driven rejection would be route-conditional, and a rolled-back
    switch would re-honour what this line rejects.
    """
    reason = unowned_container_semantics(config)
    if reason is None:
        return
    raise UnownedContainerError(
        CONTAINER_ROW,
        entry_point,
        UNOWNED_CONTAINER_GROUP,
        f"{reason}. The agent container row was rejected on the "
        f"{LEVER_RETIREMENT_LINE} line, and {entry_point}() was asked for it. "
        f"It is rejected as part of the {UNOWNED_CONTAINER_GROUP!r} group, "
        "which names a value attributed to a typed grouping object rather than "
        f"to a capability any implementation on this line reads. {_INSTEAD}",
    )

unowned_container_semantics

unowned_container_semantics(config: Any) -> str | None

Why config.agent carries unowned semantics, or None.

Answers None -- "nothing attributed to the container itself" -- for the ordinary case, which is every configuration whose agent is a stock-class container carrying only owned children.

It also answers None when the question cannot be put: a config object with no agent attribute at all, or one whose class cannot be default-constructed to supply a stock container to compare against. That is the one place this rule does not fail closed, and it is deliberate and bounded: those objects are refused by :func:~symfonic.agent.cutover.envelope._config_refusal on their own terms, so answering "refuse" here would name the container for a defect that is not the container's.

Source code in src/symfonic/agent/cutover/container_semantics.py
def unowned_container_semantics(config: Any) -> str | None:
    """Why ``config.agent`` carries unowned semantics, or ``None``.

    Answers ``None`` -- "nothing attributed to the container itself" -- for the
    ordinary case, which is every configuration whose ``agent`` is a stock-class
    container carrying only owned children.

    It also answers ``None`` when the question cannot be put: a config object
    with no ``agent`` attribute at all, or one whose class cannot be
    default-constructed to supply a stock container to compare against. That is
    the one place this rule does not fail closed, and it is deliberate and
    bounded: those objects are refused by
    :func:`~symfonic.agent.cutover.envelope._config_refusal` on their own terms,
    so answering "refuse" here would name the container for a defect that is not
    the container's.
    """
    container = getattr(config, "agent", MISSING)
    if container is MISSING:
        return None
    stock = stock_instance(config)
    if stock is None:
        return None
    expected = getattr(stock, "agent", MISSING)
    if expected is MISSING:
        return None

    if type(container) is not type(expected):
        return (
            f"config.agent is a {type(container).__name__}, not the stock "
            f"{type(expected).__name__}. A substituted container class carries "
            "semantics attributed to the agent grouping itself rather than to "
            "any of its children"
        )

    declared = field_names(container)
    if declared is None:
        return (
            "config.agent cannot be enumerated, so which of its children a "
            "value belongs to is unknowable and the value is attributable only "
            "to the container"
        )

    attached = _extra_attributes(container, declared)
    if attached:
        return (
            f"config.agent carries the attached attribute "
            f"{attached[0]!r}, which no child row owns"
        )

    for name in declared:
        if name in AGENT_CONTAINER_CHILDREN:
            continue
        actual = getattr(container, name, MISSING)
        if not equivalent(actual, getattr(expected, name, MISSING)):
            return (
                f"config.agent.{name}={brief(actual)} was set, and no child row "
                "owns that name; it is a semantic of the container and of "
                "nothing else"
            )
    return None