Skip to content

symfonic.capabilities.delegation.lockdown

lockdown

No delegated child holds a prompt-block write surface, on any path.

A prompt block has exactly one writer. That is the whole guarantee the block layer offers an operator: the rules the agent runs under are written by a human (authored tiers) or by the consolidation timeline (learned tiers), and the agent reads them. A delegated child that could write one breaks it twice over — it is a second writer, and a writer the parent never sees, because the child runs its own loop with its own palette.

Two paths, and only one of them can be fixed

:func:deny_child_self_edit covers configs the parent builds: it returns a config with the request cleared. :func:assert_no_write_surface covers children the parent is handed: those cannot be sanitised — already constructed, palette already frozen — so it refuses. Continuing quietly would leave the caller believing in a lock that did not hold.

Both run at construction, never per run. The palette is advertised to the model when the graph compiles; a check that fired at delegation time would fire after the thing it was meant to restrict had already been offered.

The vocabulary is declared here, not imported

The reserved names live in the framework's block taxonomy, which a capability may not import. Duplicating a security vocabulary is a real risk, so it is duplicated and pinned: a contract test compares this predicate against the shipped one over the whole near-miss corpus, in the test tree, where importing both is legal. Alignment asserted from outside beats either a forbidden import or a silent divergence.

SELF_EDIT_FIELD module-attribute

SELF_EDIT_FIELD = 'prompt_block_self_edit'

The config field naming the (always denied) child write surface.

WRITE_SURFACE_NAMESPACE module-attribute

WRITE_SURFACE_NAMESPACE = 'memory_block_'

Reserved tool-name prefix. Fail-closed on the whole prefix, not a fixed set: a verb added under it tomorrow is caught without editing this module.

WRITE_SURFACE_TOOL_NAMES module-attribute

WRITE_SURFACE_TOOL_NAMES: frozenset[str] = frozenset({'memory_block_append', 'memory_block_replace', 'memory_block_rewrite'})

The named write tools. Documentation, not the check — see the pattern.

assert_no_write_surface

assert_no_write_surface(name: str, agent: Any) -> Any

Refuse pre-built child agent if it can write a prompt block.

Returns the child, so a caller can register in one expression and a reader can see that registration passed through the lock rather than around it.

Raises:

Type Description
ChildLockdownError

If the child carries a write tool, or a config requesting one.

Source code in src/symfonic/capabilities/delegation/lockdown.py
def assert_no_write_surface(name: str, agent: Any) -> Any:
    """Refuse pre-built child ``agent`` if it can write a prompt block.

    Returns the child, so a caller can register in one expression and a reader
    can see that registration passed through the lock rather than around it.

    Raises:
        ChildLockdownError: If the child carries a write tool, or a config
            requesting one.
    """
    tools = registered_write_surface_tools(agent)
    if tools:
        raise ChildLockdownError(
            f"sub-agent {name!r} was built with the block-edit tool(s) "
            f"{list(tools)!r} registered. A delegated child holds no block-edit "
            "tool on any construction path: a prompt block has one writer, and "
            "a child that can write one is a second writer the parent never "
            "sees. Remove the tool from the child, or declare the child as a "
            "ChildSpec -- specs are built by the parent, which clears the write "
            "surface for you."
        )
    if wants_block_self_edit(getattr(agent, "_config", None)):
        raise ChildLockdownError(
            f"sub-agent {name!r} was built with a config setting "
            f"{SELF_EDIT_FIELD}=True. A delegated child never receives the "
            "block self-edit surface, and a pre-built child cannot be sanitised "
            "after the fact -- its tool registry is already built. Set "
            f"{SELF_EDIT_FIELD}=False on the child's config, or declare the "
            "child as a ChildSpec so the parent builds it with the flag cleared."
        )
    return agent

deny_child_self_edit

deny_child_self_edit(config: Any) -> Any

Return config with the write-surface request cleared.

The common path allocates nothing and preserves object identity: a config that never asked is returned unchanged, which matters for callers that compare configs by identity.

Raises:

Type Description
UnsanitisableChildConfigError

If the flag is set and the object exposes no model_copy to clear it through. Also a TypeError, matching what the shipped engine raised.

Source code in src/symfonic/capabilities/delegation/lockdown.py
def deny_child_self_edit(config: Any) -> Any:
    """Return ``config`` with the write-surface request cleared.

    The common path allocates nothing and preserves object identity: a config
    that never asked is returned unchanged, which matters for callers that
    compare configs by identity.

    Raises:
        UnsanitisableChildConfigError: If the flag is set and the object
            exposes no ``model_copy`` to clear it through. Also a
            ``TypeError``, matching what the shipped engine raised.
    """
    if not wants_block_self_edit(config):
        return config
    copier = getattr(config, "model_copy", None)
    if not callable(copier):
        raise UnsanitisableChildConfigError(
            f"a delegated child was given a config with {SELF_EDIT_FIELD}=True "
            f"of type {type(config).__name__!r}, which exposes no model_copy to "
            "clear it through. A child never holds a block-edit tool, and a "
            "config that cannot be sanitised cannot be used for one -- pass a "
            "framework config, or set the flag False yourself."
        )
    return copier(update={SELF_EDIT_FIELD: False})

is_write_surface_tool_name

is_write_surface_tool_name(name: Any) -> bool

True if a tool called name may write a prompt block.

Matched case-insensitively against a stripped name. Nothing on the path from a registered tool to this predicate normalises either, so a guard that a shift key defeats would be no guard: MEMORY_BLOCK_APPEND reaches a child exactly as easily as the lowercase spelling.

This is a name heuristic and says so. It cannot detect a write tool given a name outside the vocabulary entirely (grant_edit). No name check can — the reserved namespace exists so that the framework's own tools are always inside it, and the verb family covers the near-misses an adopter wrapping the write API would plausibly reach for.

Source code in src/symfonic/capabilities/delegation/lockdown.py
def is_write_surface_tool_name(name: Any) -> bool:
    """``True`` if a tool called ``name`` may write a prompt block.

    Matched case-insensitively against a stripped name. Nothing on the path
    from a registered tool to this predicate normalises either, so a guard that
    a shift key defeats would be no guard: ``MEMORY_BLOCK_APPEND`` reaches a
    child exactly as easily as the lowercase spelling.

    This is a name heuristic and says so. It cannot detect a write tool given a
    name outside the vocabulary entirely (``grant_edit``). No name check can —
    the reserved namespace exists so that the framework's own tools are always
    inside it, and the verb family covers the near-misses an adopter wrapping
    the write API would plausibly reach for.
    """
    if not isinstance(name, str):
        return False
    return bool(_WRITE_SURFACE_PATTERN.match(name.strip()))

registered_write_surface_tools

registered_write_surface_tools(agent: Any) -> tuple[str, ...]

Names of prompt-block write tools registered on agent.

The authoritative check, because it reads what the model will actually be offered. A tool handed straight to a child's constructor never passed through :data:SELF_EDIT_FIELD, so a config-only guard waves it through.

Source code in src/symfonic/capabilities/delegation/lockdown.py
def registered_write_surface_tools(agent: Any) -> tuple[str, ...]:
    """Names of prompt-block write tools registered on ``agent``.

    The authoritative check, because it reads what the model will actually be
    offered. A tool handed straight to a child's constructor never passed
    through :data:`SELF_EDIT_FIELD`, so a config-only guard waves it through.
    """
    direct = _direct_write_surface_tools(agent)
    if direct is not None:
        return direct
    return _delegated_write_surface_tools(agent)

wants_block_self_edit

wants_block_self_edit(config: Any) -> bool

True if config explicitly requests the block write surface.

Compared with is True rather than coerced. A duck-typed child that synthesises attributes on access — a mock, a lazy or remote proxy — answers any attribute read with a truthy object, and coercion reported such a child as requesting a surface it had never heard of.

Source code in src/symfonic/capabilities/delegation/lockdown.py
def wants_block_self_edit(config: Any) -> bool:
    """``True`` if ``config`` explicitly requests the block write surface.

    Compared with ``is True`` rather than coerced. A duck-typed child that
    synthesises attributes on access — a mock, a lazy or remote proxy — answers
    any attribute read with a truthy object, and coercion reported such a child
    as requesting a surface it had never heard of.
    """
    return getattr(config, SELF_EDIT_FIELD, False) is True