aeat.entrypoints.mcp._persona_scope module¶
Persona-scoped tool boundary over the operator-surface manifest.
Implements decision D1 of the 2026-07-01-agent-harness-adr: a runtime
PreToolUse-layer partition that filters the exposed MCP tool set by the
active operator persona’s declared (family, mutability) ceiling. The
per-persona declaration in this module is a typed mapping from
AgentPersona to a coarse set of mounted-command-family child
tokens plus an OperatorMutability
ceiling - derived from each persona document’s “Tool scope” section under
src/aeat/_data/agent/personas/. It is deliberately NOT a per-tool
allowlist: per D1, a second tool-shaped artifact would duplicate the
manifest’s own (family, mutability) data and could itself drift between
builds, contrary to aeat-registry-authority-flow’s single-authority
discipline.
is_tool_in_persona_scope() reads the live
build_operator_surface_manifest()
on every call - never a frozen snapshot - so a manifest change (a family
added, removed, or re-mounted by the in-flight Track-1 #1 manifest-
completeness brief) is picked up automatically. Correctness of the
per-persona declaration against the live manifest is proven separately by
the build-time pinning test in tests/test_persona_scope.py, not by this
module.
This complements the existing global confirmation_for_tool()
HITL policy: that gate decides how a call is approved (auto/confirm/block)
irrespective of persona; this gate decides whether a tool is in the active
persona’s boundary at all. Both run in the PreToolUse layer.
active_persona() resolves the runtime session’s persona from the
AEAT_MCP_PERSONA environment variable; _server.py calls it once at
serve() startup and threads the result through _list_tools (filters
the advertised tool set) and _call_tool (refuses an out-of-scope call
before the global HITL gate runs).
Family granularity and the handoff deny rules: the manifest’s own boundary is
the mounted-command-family child token, not the individual verb, and the
scope filter stays at that granularity (three personas - modelo-preparer,
verifier, and reconciler - all declare families={"modelo"}).
What IS verb-granular is the irreversible boundary: per decision R6(iii) of
the 2026-07-02-agent-harness-refoundation-adr, PERSONA_HANDOFF_DENIALS
structurally denies the filing-handoff leaves (export, the record marker
file) to every modelo-lifecycle persona except the verifier, which
D3 of the 2026-07-01-agent-harness-adr made the sole owner of the
irreversible artefact. The preparer/verifier split for NON-handoff verbs
(e.g. the verifier calling modelo.work.create) remains prose-level
persona discipline; the deny rules close exactly the boundary whose breach
is irreversible.
- PERSONA_ENV_VAR¶
The environment variable that selects the active MCP session persona. Absent or empty means the session is un-personified and keeps the full, unscoped tool surface - the pre-D1 behaviour is preserved for any caller that does not opt into a persona boundary.
- class AgentPersona(*values)[source]¶
Bases:
StrEnumThe seven operator-harness personas, named per their persona document stem.
Mirrors the file stems under
src/aeat/_data/agent/personas/exactly (kebab-case), so a persona’s runtime identity and its shipped document are the same token.- COORDINATOR¶
- ONBOARDING¶
- LEDGER_GROOMER¶
- CLASSIFIER¶
- MODELO_PREPARER¶
- VERIFIER¶
- RECONCILER¶
- class PersonaToolScope(**data)[source]¶
Bases:
BaseModelOne persona’s declared mounted-command-family boundary.
familiesnames the mounted-command-familychildtokens (e.g."ledger","modelo") the persona document’s Tool-scope section grants it; a family absent from this set is out of scope regardless of mutability.mutability_ceilingis the highestOperatorMutabilitythe persona may invoke within its scoped families - a family whose own manifest-declared mutability exceeds the ceiling is refused even when the family child is listed, which is defence in depth for a future manifest edit that raises a scoped family’s mutability without an explicit persona-boundary review.- Parameters:
persona (AgentPersona)
mutability_ceiling (OperatorMutability)
- persona: AgentPersona¶
- families: frozenset[str]¶
- mutability_ceiling: OperatorMutability¶
- scope_for_persona(persona)[source]¶
Return the declared
PersonaToolScopefor one persona.Every
AgentPersonamember has exactly one declared scope (proven by the pinning test), so this never falls back to a default.- Return type:
- Parameters:
persona (AgentPersona)
- live_family_mutability()[source]¶
Read the live operator-surface manifest and map each family to its mutability.
Reads
build_operator_surface_manifest()fresh on every call rather than caching a snapshot, so a manifest change (a family added, removed, or re-mounted) is observed immediately - the single-authority discipline D1 requires.- Return type:
- Returns:
A mapping of family child token to its
OperatorMutability.
- is_tool_in_persona_scope(*, persona, command_key)[source]¶
Return whether
command_keyis inpersona’s live-manifest-checked scope.The
PreToolUse-layer partition D1 mandates: a command key is in scope only when (1) its projected family child token is one of the persona’s declaredfamilies, and (2) that family’s live manifest mutability is at or below the persona’s declaredmutability_ceiling. A family absent from the live manifest altogether (not yet mounted) fails closed - it is never treated as in scope.This is independent of, and complements, the global
confirmation_for_tool()HITL confirmation tier: that gate decides how an in-scope call is approved; this gate decides whether the call is in the persona’s boundary at all.- Return type:
- Parameters:
persona (AgentPersona)
command_key (str)
- is_handoff_denied(*, persona, command_key)[source]¶
True when
command_keyis a handoff leaf this persona is structurally denied.The denial is the irreversible modelo filing handoff, so it fires only for a command in the
modelofamily whose leaf is a denied handoff verb - a same-namedexport/fileleaf in another family is never caught by this rule (it is out of a modelo persona’s scope anyway; the family guard keeps the rule precise rather than relying on scope to mask it).Runs in the same
PreToolUselayer asis_tool_in_persona_scope()and is checked by both_list_tools(the denied tool is not even advertised to the persona) and_call_tool(a direct call refuses).- Return type:
- Parameters:
persona (AgentPersona)
command_key (str)
- handoff_denial_message(*, persona, command_key)[source]¶
The instructive refusal for a denied handoff call, naming the owning persona (client-relayed, localized).
- Return type:
- Parameters:
persona (AgentPersona)
command_key (str)
- active_persona(env=None)[source]¶
Resolve the active MCP session persona from
PERSONA_ENV_VAR.Reads
env(an injectable mapping so the resolution is unit-tested against real dict inputs without touching process environment state; defaults toos.environfor the live server). An absent or blank value returnsNone, which callers MUST treat as “unscoped” - the full tool surface, matching pre-D1 behaviour, so an un-personified session (a direct developer connection, a session that predates the persona boundary) is never accidentally narrowed.- Raises:
ValueError – the environment value does not name a declared
AgentPersonamember. The message enumerates the accepted set peraeat-architecture-boundaries’s CLI-boundary instructive-refusal discipline.- Return type:
- Parameters: