symfonic.core.prompt.blocks.taxonomy¶
taxonomy ¶
The block taxonomy: tiers, permissions, and the canonical matrix.
This module is the vocabulary :class:~symfonic.core.prompt.blocks.spec.PromptBlockSpec
is written in, and the single source of truth for what each well-known
block is. It is deliberately separate from the spec model: the matrix is
read by the validator, the resolver and the renderer, and a table that
several components agree on should not live inside one of them.
Tiers, and what "learned" means¶
Four tiers, in descending authority: platform, operating,
profile, session. The first two are authored -- a human wrote
that text and the agent may never write it. The last two are learned:
their content is aggregated from facts the system recorded about the
user, so it is attacker-influenced input and the renderer wraps it
accordingly.
"Learned" is therefore a classification over tiers
(:data:LEARNED_TIERS), not a fifth tier value. Downstream code asks
:attr:~symfonic.core.prompt.blocks.spec.PromptBlockSpec.is_learned
rather than comparing tier strings, so adding a tier later cannot
silently place new content on the authored side of the trust boundary.
Failure policy defaults follow that split¶
:func:default_on_source_failure returns fail_closed for the
authored tiers and omit for the learned ones. An unreachable
BOUNDARIES or IDENTITY source means the agent's rules are missing, and
running without them is worse than not running; an unreachable
USER_PROFILE means the assistant is less personal for a turn.
No destructive verb, anywhere¶
:data:AgentPermission has exactly four members and none of them is
clear or delete. That is the point: a destructive verb the agent
can reach is one hallucinated tool call away from erasing a user's
profile, and no confirmation prompt exists in an agent loop to stop it.
Removal happens through the memory layer's soft-retract path on the
consolidation timeline, never through a block permission.
:func:reject_destructive_permissions re-checks every permission set --
including :data:AgentPermission itself, at import -- so widening the
literal later cannot quietly reintroduce one.
AUTHORED_TIERS
module-attribute
¶
Tiers whose content a human authored. The agent may never write these.
AgentPermission
module-attribute
¶
What the agent may do to a block.
Four verbs, and deliberately no fifth. There is no clear and no
delete: see the module docstring.
BLOCK_EDIT_TOOL_NAMES
module-attribute
¶
BLOCK_EDIT_TOOL_NAMES: frozenset[str] = frozenset(
{
"memory_block_append",
"memory_block_replace",
"memory_block_rewrite",
}
)
The tools that let an agent write a block.
Named here rather than next to their (not yet written) implementations because the lockdown needs the names before the tools exist: a delegated child must be rejected for carrying one on the day the tool ships, not on the day someone remembers to update the guard. Stage 3 registers none of these on any construction path, so the set is currently a list of things nothing may hold -- which is exactly when it is cheap to install.
BLOCK_EDIT_TOOL_NAMESPACE
module-attribute
¶
Tool-name prefix reserved for block writes. See :func:is_block_edit_tool_name.
BlockLayer
module-attribute
¶
Which cached prompt region the block renders into.
BlockScope
module-attribute
¶
How widely one block's content is shared.
deployment is one value for the whole install and is the only scope a
scope_aware=False source can honour; that pairing rule is enforced by
the config-level validator, which holds every spec at once.
BlockTier
module-attribute
¶
Authority tiers, highest first. See :data:LEARNED_TIERS.
CANONICAL_BLOCKS
module-attribute
¶
CANONICAL_BLOCKS: MappingProxyType[str, CanonicalBlock] = (
MappingProxyType(
{
(b.name): b
for b in (
_canonical(
"BOUNDARIES",
"platform",
("read",),
"L1",
10,
),
_canonical(
"IDENTITY",
"platform",
("read",),
"L1",
20,
),
_canonical(
"RULES",
"operating",
("read",),
"L1",
30,
),
_canonical(
"ENVIRONMENT",
"operating",
("read",),
"L1",
40,
),
_canonical(
"USER_PROFILE",
"profile",
("read", "append", "replace"),
"L1",
50,
label_prefix="SOUL:",
),
_canonical(
"PREFERENCES",
"profile",
("read", "append", "replace"),
"L1",
60,
label_prefix="PREFERENCE:",
),
_canonical(
"FOCUS",
"session",
("read", "append", "replace"),
"L2",
70,
label_prefix="FOCUS:",
),
_canonical(
"ONBOARDING",
"operating",
("read",),
"L1",
80,
inherit=False,
),
)
}
)
)
The canonical block matrix, keyed by block name. Read-only at runtime.
DESTRUCTIVE_VERBS
module-attribute
¶
DESTRUCTIVE_VERBS: frozenset[str] = frozenset(
{
"clear",
"delete",
"drop",
"erase",
"purge",
"remove",
"reset",
"truncate",
"wipe",
}
)
Verbs that may never appear in any permission set, at any tier.
LEARNED_TIERS
module-attribute
¶
Tiers whose content is aggregated from recorded facts.
The renderer treats these as untrusted data; the authored tiers render verbatim.
MEMORY_SOURCE
module-attribute
¶
Sentinel block source selecting the pinned memory lane.
SourceFailurePolicy
module-attribute
¶
What the resolver does when a block's source cannot be read.
CanonicalBlock
dataclass
¶
CanonicalBlock(
name: str,
tier: BlockTier,
agent_permissions: frozenset[str],
layer: BlockLayer,
order: int,
inherit: bool = True,
label_prefix: str = "",
)
One row of the canonical block matrix.
A :class:~symfonic.core.prompt.blocks.spec.PromptBlockSpec naming
one of these blocks inherits the row for every field it does not
state itself, so an operator declaring ONBOARDING cannot forget
inherit=False and an operator declaring BOUNDARIES cannot
forget that it is platform tier.
default_on_source_failure ¶
Return the failure policy a block of tier gets when it declares none.
fail_closed for the authored tiers (platform, operating):
losing BOUNDARIES or RULES removes the agent's constraints, and an
agent running without its constraints is worse than an agent that did
not run.
omit for the learned tiers: losing USER_PROFILE costs
personalisation for a turn, which does not justify failing the turn.
Source code in src/symfonic/core/prompt/blocks/taxonomy.py
is_block_edit_tool_name ¶
Return True if a tool named name may write a prompt block.
Fail-closed on the whole :data:BLOCK_EDIT_TOOL_NAMESPACE prefix,
not only the known names in :data:BLOCK_EDIT_TOOL_NAMES -- a later
verb added under that namespace without updating the set is still
caught. But the reserved namespace is core's own; an adopter tool
that wraps
:meth:~symfonic.core.prompt.blocks.protocol.WritableBlockSource.append_revision
under an unrelated name (edit_block, write_block_custom) is
invisible to a bare prefix check even though it is exactly the
capability the lockdown exists to keep off a delegated child. This
also matches the wider block_edit/edit_block/write_block/
update_block verb family the source-tree sweep enforces, so the
two checks cannot silently diverge.
Matched case-insensitively, and against a name stripped of
surrounding whitespace. Nothing on the path from a registered tool to
this predicate normalises either: lockdown reads tool.name and
passes it straight through, so MEMORY_BLOCK_APPEND and
Edit_Block reached a delegated child while the lowercase spelling
of the same tool was rejected. Block names already treat wrong case
as a near-miss hazard and reject it by charset
(:mod:~symfonic.core.prompt.blocks.spec); the tool-name path is the
same hazard and gets the same answer. A guard that a shift key
defeats is not a guard.
This remains a name heuristic, not a behavioural guarantee: it
cannot detect a write tool given a name outside this vocabulary
entirely (grant_edit, a translated or obfuscated name). No name
check can -- see the module docstring on the reserved namespace.
Source code in src/symfonic/core/prompt/blocks/taxonomy.py
reject_destructive_permissions ¶
Raise :class:ValueError if permissions holds a destructive verb.
:data:AgentPermission already excludes them, so this can only fire
if the literal is widened later. That is exactly when it is worth
having: the review that adds a verb sees a named failure instead of a
silently-granted delete.