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: StrEnum

The 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: BaseModel

One persona’s declared mounted-command-family boundary.

families names the mounted-command-family child tokens (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_ceiling is the highest OperatorMutability the 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
families: frozenset[str]
mutability_ceiling: OperatorMutability
scope_for_persona(persona)[source]

Return the declared PersonaToolScope for one persona.

Every AgentPersona member has exactly one declared scope (proven by the pinning test), so this never falls back to a default.

Return type:

PersonaToolScope

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:

dict[str, OperatorMutability]

Returns:

A mapping of family child token to its OperatorMutability.

is_tool_in_persona_scope(*, persona, command_key)[source]

Return whether command_key is in persona’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 declared families, and (2) that family’s live manifest mutability is at or below the persona’s declared mutability_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:

bool

Parameters:
is_handoff_denied(*, persona, command_key)[source]

True when command_key is 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 modelo family whose leaf is a denied handoff verb - a same-named export / file leaf 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 PreToolUse layer as is_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:

bool

Parameters:
handoff_denial_message(*, persona, command_key)[source]

The instructive refusal for a denied handoff call, naming the owning persona (client-relayed, localized).

Return type:

str

Parameters:
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 to os.environ for the live server). An absent or blank value returns None, 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 AgentPersona member. The message enumerates the accepted set per aeat-architecture-boundaries’s CLI-boundary instructive-refusal discipline.

Return type:

AgentPersona | None

Parameters:

env (Mapping[str, str] | None)