Skip to content

symfonic.capabilities.extensions.values

values

The vocabulary extension composition speaks (T4.2.2).

Everything here is frozen and everything here is a reading rather than a handle. That is what lets the composer be a pure function: two compositions of the same providers compare equal, so "installing this plugin changed nothing else" is an assertion rather than a claim.

Two design points worth stating rather than leaving to be inferred.

A policy verdict is tri-state. ALLOW, DENY, and ABSTAIN are three answers, not two. A plugin that raised, or that has no opinion about this action, has abstained; collapsing that into ALLOW is how a guard whose backend is down reads as a guard that examined the action and approved it.

A trust tier is a string here, not the prompt compiler's enum. The capability layer does not import another capability's internals (LAY-ADR §3.4), so the tier travels as its declared name and the composition root maps it. The names are deliberately the same names, so the mapping is an identity lookup and a divergence is a failing test rather than a silent re-tiering.

ContributionKind

Bases: StrEnum

The four things an extension may contribute, and there is no fifth.

ExtensionDiagnostic dataclass

ExtensionDiagnostic(extension: str, kind: ContributionKind, detail: str, subject: str = '', severity: Severity = Severity.WARNING)

One thing the composer decided, in names, for the record.

Diagnostics are how a dropped contribution stays visible. Nothing is discarded silently: every refusal, truncation, and remap emits one of these, and :class:~.composition.ComposedExtensions carries them alongside what survived.

LifecyclePhase

Bases: StrEnum

When a lifecycle hook runs relative to the composition that owns it.

PolicyDecision

Bases: StrEnum

One contributed policy's answer about one action.

PolicyRequest dataclass

PolicyRequest(action: str, context: Mapping[str, object], extension: str = '')

What a contributed policy is asked about: one action, once.

context is frozen at construction, all the way down: a policy answers a question, it does not edit the question. Freezing rather than copying is what makes that a property instead of a claim — a shallow copy leaves every nested value writable, so a policy handed the live tool arguments could rewrite the very call it was only allowed to veto (AS-INT-3), and the caller's own mapping would come back mutated.

The caller's mapping is never touched: the frozen view is built over a fresh dict, so whoever passed the context still holds their own mutable one.

PolicyVerdict dataclass

PolicyVerdict(decision: PolicyDecision, reason: str = '', policy: str = '')

One policy's answer, with the reason that makes it auditable.

Severity

Bases: StrEnum

How loudly a composition diagnostic should read.

ERROR is reserved for a refusal an operator must see — a shadowed tool name, a rejected payload — and never used for an extension declining to contribute, which is ordinary.

freeze_context

freeze_context(value: Any) -> Any

Return a recursively read-only view of value.

Mappings become :class:~types.MappingProxyType, sequences become tuples, sets become frozensets, and every other object is passed through by reference. Leaves are not copied on purpose: a policy context is the caller's live data — clients, connections, large payloads — and deep-copying it once per policy per action would be both expensive and, for anything holding a socket, wrong. Making the structure unwritable is what the invariant actually needs.

Source code in src/symfonic/capabilities/extensions/values.py
def freeze_context(value: Any) -> Any:
    """Return a recursively read-only view of ``value``.

    Mappings become :class:`~types.MappingProxyType`, sequences become tuples,
    sets become frozensets, and every other object is passed through by
    reference. Leaves are *not* copied on purpose: a policy context is the
    caller's live data — clients, connections, large payloads — and deep-copying
    it once per policy per action would be both expensive and, for anything
    holding a socket, wrong. Making the *structure* unwritable is what the
    invariant actually needs.
    """
    if isinstance(value, Mapping):
        return MappingProxyType(
            {key: freeze_context(item) for key, item in value.items()}
        )
    if isinstance(value, (str, bytes, bytearray)):
        return value
    if isinstance(value, (list, tuple)):
        return tuple(freeze_context(item) for item in value)
    if isinstance(value, (set, frozenset)):
        return frozenset(value)
    return value

layer_rank

layer_rank(layer: str) -> int

Position of layer in the stable-to-volatile order.

Unknown layers sort last rather than raising: ordering is not the place to discover an invalid layer, and the contract validator has already rejected one by the time anything is sorted.

Source code in src/symfonic/capabilities/extensions/values.py
def layer_rank(layer: str) -> int:
    """Position of ``layer`` in the stable-to-volatile order.

    Unknown layers sort last rather than raising: ordering is not the place to
    discover an invalid layer, and the contract validator has already rejected
    one by the time anything is sorted.
    """
    try:
        return CONTRIBUTED_LAYERS.index(layer)
    except ValueError:
        return len(CONTRIBUTED_LAYERS)