Status: implemented on main (2026-07-03). Delivers assessment requirement P3 (agent
identity & permissions — only Salesforce/Microsoft were GREEN). Built on the
Trust Membrane (Tier 5.2) foundation: the agent is a
first-class governed principal. Design: LEAPFROG_ROADMAP.md Epic 3.3,
TIER_5.2_TRUST_MEMBRANE.md §1.5.
Renamed 2026-07-04 — this document was formerly "Agent Identity Architecture" and the module was
core/agent_identity.py. It was renamed to Agent Authority (core/agent_authority.py) because in Uderia an agent's identity is its Profile; this subsystem governs the agent's authority. See §0. The stored config key movedidentityConfig → authorityConfig(the legacy key is still read). The internal classAgentPrincipalkeeps its name — it is the correct IAM term.
0. What "Agent Identity" means in Uderia — read this first
"Agent identity" in the market is an umbrella term. In Uderia it is not a single feature — it is a composite realised across four subsystems on two orthogonal axes. Keeping them distinct is the whole point of this rename.
| Concept | The question it answers | Where it lives in Uderia | Axis |
|---|---|---|---|
| Agent identity | Who is this agent? | The Profile — its IFOC class, its LLM, its MCP "senses", its Memory, its Components. The @TAG profile is the agent. |
who |
| Agent assurance | Is this agent verified / trustworthy? | The Trust Membrane (Tier 5.2) — signed, compositional attestation. | trust |
| Agent authority ← this document | What may it do, on whose behalf, when it acts autonomously? | core/agent_authority.py — a delegated, subset-of-owner scope. |
access |
| Agent audit | What did it actually do? | The Execution Provenance Chain — every autonomous action attributed to the agent. | audit |
Visual companion: The Uderia Agent — An Anatomy of Trust — the whole-agent diagram this taxonomy comes from (the five faculties inside the Trust Membrane, on the Grounding & Guardrails foundations). Its center — Agent Identity = who it is = its Profile — is exactly the "identity" row above.
Two rules follow, and they are load-bearing:
- Identity ≠ authority. The Profile says who the agent is; Agent Authority says what it is permitted to do. A trusted, well-defined agent (identity + assurance) can still be given a deliberately narrow authority. This is the platform's phrasing of charter #8, trust ≠ access.
- Authority is delegated and can only shrink. An agent has no authority of its own — when it acts autonomously it borrows a strict subset of its owner's permissions. The model can only remove; it can never grant or elevate.
If you came here looking for "the agent's identity," you want the Profile and, for how it's
surfaced as an estate-wide map, the Platform Ontology. This
document is specifically the access/authority face.
1. The idea
When an agent acts autonomously — a genie expert delegated by a coordinator, a scheduled
task firing on a timer, a flow node, an inbound MCP ask_profile call — it is not a human
issuing that request in the moment. Until now such actions ran under the owner's raw
authority (the parent's auth_token, the task's user_uuid). P3 makes each autonomous
action run as an Agent Principal derived from the owner whose authority is a strict
subset of the owner's — and attributes every autonomous action to the agent in the audit
trail.
Honest scope. This is the attainable form of P3: a scoped principal + audit attribution + fail-closed enforcement over capabilities (tools, writes, connectors, budget). The fuller vision — an agent confined to a graph sub-segment via ontology-mediated data access ("cannot query outside its slice") — depends on OBDA, which does not yet exist (Tier 5 Step 5). The shipped form scopes what the agent can do, not yet what data it can see structurally.
2. The model — pure, deterministic, subset-only
core/agent_authority.py is pure: the model + the enforcement decision. It never grants —
a scope only ever removes the owner's authority (subset invariant by construction, so an
agent with no config behaves exactly as today — byte-identical baseline, charter #15).
| Piece | Meaning |
|---|---|
AgentScope |
The restriction. autonomous (may it act at all?) · deny_writes (block mutating tools) · denied_tools (deny-list) · allowed_connectors (None = all; else an allow-list) · max_cost_per_run (USD cap). All defaults = unrestricted. |
AgentPrincipal |
owner_uuid + profile_id + scope. principal_id = agent:<profile_id>. actor() = the audit stamp attributing an action to the agent, not the user. (Kept its name — "principal" is the correct IAM term for an entity that holds authority.) |
parse_scope(authorityConfig) |
Build a scope from a config dict (absent/empty → baseline). |
derive_principal(profile, owner_uuid) |
The principal for a profile, read from its stored authorityConfig — falling back to the legacy identityConfig key for profiles saved before the rename (same mechanism as every other *Config block). |
is_tool_allowed(scope, tool, …) |
The decision: (allowed, reason). Autonomous-off → deny all; denied-list → deny; connector not in allow-list → deny; write under deny_writes → deny. TDA_* framework tools are never write-gated (they are the agent's own reporting/orchestration). |
is_run_within_budget(scope, cost) |
Per-run USD cap (post-hoc; the cost engine owns the numbers). |
3. Enforcement — a per-turn security context, fail-closed at every chokepoint
The design problem: one gate misses most engines, and threading a principal through
every call site is fragile. The solution is a per-turn security context via contextvars
+ layered enforcement at the execution chokepoints.
-
contextvars.ContextVar(async-task-local, non-racy). The principal is set once at the autonomous-turn boundary and read at every tool chokepoint.contextvarspropagate into awaited coroutines and child tasks (asyncio copies the context at task creation), so concurrent sessions stay isolated and no signature plumbing is needed. An interactive user turn leaves it unset → baseline (no gating). (A CI test proves the propagation acrossawaitand the concurrent-turn isolation.) -
Set/reset at the turn boundary —
agent/execution_service.py:run_agent_execution. When_effective_origin ∈ AUTONOMOUS_ORIGINS(genie_parent·genie_child·flow·scheduler·mcp) it derives the principal from the active profile and sets the ambient context; on convergence it resets and stamps the turn payload with the actor (agent vs user). A scheduled task'ssource="scheduler"is mapped to theschedulerorigin so the principal applies. Genie slaves run as separate HTTP requests (their own task/context) → each gets its own principal, no cross-task leak. -
Four chokepoints read the ambient principal and deny before executing (all-engine, because every tool call from any engine funnels through one of these):
| Chokepoint | File | Scoped by |
|---|---|---|
| MCP data-source tools (Optimize / genie-slave / FASTPATH) | mcp_adapter/adapter.py:invoke_mcp_tool |
denied_tools, deny_writes (heuristic). Genuine TDA_* framework tools (final report, logging, orchestration) exempt — but component tools are not (enforced in the component-routing branch, see below). |
| MCP data-source tools (Conversation / ReAct) | agent/tool_enforcement.py:wrap_langchain_mcp_tools |
Same decision as invoke_mcp_tool. The ReAct engine executes MCP tools through the LangChain session, NOT invoke_mcp_tool, so each tool's coroutine is wrapped at ConversationAgentExecutor.__init__ (the one seam where the tools + session_id are both known) to reach the same fail-closed chokepoint. Also runs preflight SQL grounding there. |
| Component tools | components/manager.py:_run_component (LangChain path) + mcp_adapter/adapter.py component-routing branch (Optimize path) |
denied_tools by component id OR TDA_ tool name (enforce_tool(..., aliases=…)), so denied_tools:["scheduler"] and denied_tools:["TDA_Scheduler"] both work on both paths. Capabilities, not write-gated. |
| Platform connectors | core/platform_connector_registry.py:invoke_connector_tool |
allowed_connectors allow-list. |
The single shared decision module
agent/tool_enforcement.pybacks both MCP paths so the "fail-closed at every tool chokepoint" guarantee holds on every engine, including the ReAct engine that bypassesinvoke_mcp_tool.
- Fail-closed (charter #8):
enforce_tool()returns deny on any enforcement error; a denial returns a terminal, non-retryable result (denied_tool_result) stampedagent_deniedso it is auditable and the LLM incorporates it as a definitive outcome (no self-correction loop). No principal (interactive) → allowed (baseline).
4. Persistence & UX
- Storage: the profile's
authorityConfigblock (inuser_preferencesprofile JSON, same asguardrailConfig/groundingConfig— no schema change). The legacyidentityConfigkey is still read on load, so profiles saved before the rename keep their scope; the next save rewrites them underauthorityConfig. - Profile editor: an Agent Authority card (
configurationHandler.js+index.html) — process-focused: it frames the model ("when this agent runs autonomously it acts as a scoped identity derived from you — its identity is the Profile itself; leave untouched to inherit your full authority") then exposes allow-autonomous · read-only · denied-tools · allowed-connectors · per-run budget. Theme-aware, monochrome icon, collapsed by default.
5. Guarantees & proof
- Byte-identical baseline — no
authorityConfig/identityConfig(or all-default) → unrestricted scope,is_restrictedFalse, nothing gated. - Subset-only — the scope model has no grant/elevate field; it can only remove.
- Backward-compatible — legacy
identityConfigis still honored;authorityConfigwins when both are present (asserted in CI). - Proof —
test/test_agent_authority.py(46 assertions: decision · subset invariant · config back-compat · contextvar set/get/reset · async propagation-across-awaits + concurrent-turn isolation · fail-closed enforce · denial shape) + a live integration proof through the realinvoke_mcp_toolchokepoint (owner/no-principal passes; the autonomous agent is deniedbase_writeQuerybefore touching MCP; a permitted read tool passes) + the real stored-profile → scope loop (config_manager load of@OPTIM→derive_principal→ denial).
5a. The trust-budget circuit breaker writes here (Jul 2026)
The Behavioral Trust Score's opt-in circuit breaker
(trust_behavior.breaker_mode = "freeze_autonomy" — see
AGENT_OPS_EVAL_ARCHITECTURE.md §6c) acts
through this model, not around it: when an agent's trailing conduct enters
at_risk, the breaker sets the profile's authorityConfig.autonomous = False
and stamps frozen_by_breaker: true + frozen_reason — suspending only the
agent's unattended use via the exact same scope the Agent Authority card
edits (interactive use untouched; visible and reversible by clearing the flag /
re-enabling autonomous in the card). The stamps are plain authorityConfig
keys, so export/import and the editor treat them like any other scope field.
Implementation: eval/online.py:_run_trust_breaker (the freeze_autonomy branch
calls config_manager.update_profile).
6. What's deferred (charter #19)
- Structural data-scoping (OBDA). The agent confined to a graph sub-segment — the fuller P3 vision — is Tier 5 Step 5. The Agent Principal is its on-ramp: the same principal that scopes capabilities today will carry the data-scope when OBDA exists, extending — never forking — this model.
- Per-run budget is post-hoc (
is_run_within_budget, enforced at theexecution_service.run_agent_executionend-of-turn seam after the turn's cost is resolved): an overrun stampsauthority_budget_exceededon the payload + emits anagent_budget_exceededaudit event so the caller (scheduler, coordinator) can react — it is recorded, not a mid-run kill switch. (max_cost_per_run: 0is honored as a real deny-all cap, not "no cap".) - MCP origin is in
AUTONOMOUS_ORIGINSbut an inbound MCPask_profileis not yet stamped with a distinct origin at the execution layer (it currently arrives as a user REST call); genie/flow/scheduler are the live autonomous triggers today. - Frontier "agent identity" facets the platform does not yet claim (see §0): the agent authenticating to external systems as itself (its own workload credential), its own vaulted short-lived secrets distinct from the owner's, and formal OAuth token-exchange / on-behalf-of delegation downstream. Authority (this doc), assurance (Trust Membrane), and audit (provenance) are in place; those three are the open edge of the identity umbrella.
7. Files
| File | Role |
|---|---|
src/trusted_data_agent/core/agent_authority.py |
The model + decision + contextvar context + enforce_tool + denied_tool_result (pure). |
src/trusted_data_agent/agent/execution_service.py |
Derive/set the principal at the autonomous-turn boundary; reset + actor stamp on convergence; source=scheduler → origin. |
src/trusted_data_agent/mcp_adapter/adapter.py |
MCP-tool chokepoint enforcement. |
src/trusted_data_agent/components/manager.py |
Component-tool chokepoint enforcement. |
src/trusted_data_agent/core/platform_connector_registry.py |
Connector chokepoint enforcement. |
static/js/handlers/configurationHandler.js + templates/index.html |
The Agent Authority profile-editor card. |
test/test_agent_authority.py |
46 CI assertions incl. the async architectural proof and config back-compat. |