Skip to main content

AI agent data access control — control what agents can see

Project description

aegis-trust

AI agents should not read everything.

aegis-trust is a developer-first trust layer for AI agents. It controls what data an agent can access, why it can access it, and how that access is audited — before sensitive data reaches the model.

For startup engineers building AI agents for enterprise customers (LangChain, CrewAI, OpenAI SDK, MCP, n8n). When the enterprise procurement team asks "will your AI read our customer data?", the answer is in the decorator above the tool.

pip install aegis-trust

PyPI version Python versions License: MIT


10-Second Sandbox

pip install aegis-trust
aegis sandbox

…or with Docker:

git clone https://github.com/Incierge3789/aegis-shield
cd aegis-shield/examples/docker
docker compose -f docker-compose.dev.yml up --build
# In another shell:
curl -s http://localhost:8080/demo/agent-request | jq
# Stop + clean up: docker compose -f docker-compose.dev.yml down -v

Output (excerpt):

3. Aegis decision:
     ┌────────────────────────────────────────────────────────┐
     │ Agent:        support_agent
     │ Purpose:      customer_support
     │ ✓ Allowed:    last_login, plan, user_id
     │ ✗ Blocked:    credit_card, email, internal_notes, phone, ssn
     │ Decision:     filtered
     │ Audit:        ./aegis-sandbox-audit.jsonl
     └────────────────────────────────────────────────────────┘

No code. No config. No infra. Run it, see what aegis-trust does, then write the same in your own code.


30-Second Quickstart

from aegis import shield

@shield(purpose="customer_support", scope=["name", "issue"])
def get_customer(id):
    return {
        "name": "Tanaka Taro",
        "email": "tanaka@example.com",   # hidden
        "card":  "4242-****-****-1234",  # hidden
        "issue": "Login problem",
    }

get_customer(1)
# → {"name": "Tanaka Taro", "issue": "Login problem"}

The agent never sees email or card. No config files. No middleware. One line.

5-Minute Verification

pip install aegis-trust
python -c "from aegis import shield
f = shield(purpose='support', scope=['name'])(lambda: {'name': 'Aria', 'ssn': '123-45-6789'})
print(f())"
# → {'name': 'Aria'}

If you see {'name': 'Aria'} (no ssn), the install works and field-level filtering is active.


Why this exists

LLM-driven agents see whatever a tool returns. A "look up customer" tool that returns 30 fields hands all 30 to the model on every call. PII, payment data, internal notes — all of it ends up in the prompt window, the logs, and (often) the model provider's training pipeline.

@shield collapses the answer down to the fields the declared purpose actually needs, before the agent sees the result. The purpose is a contract: the function says what it is for, and the SDK enforces what it is allowed to return.

  • Whitelist (scope): the agent sees only the listed fields.
  • Blacklist (deny_fields): the agent sees everything except the listed fields.
  • Fail-closed: on any error, return empty. The decorator never leaks unfiltered data, exceptions, or tracebacks.

Agent Framework Examples

LangChain — filter PII out of tool returns

from aegis import shield

@shield(purpose="customer_support", scope=["name", "plan", "issue"])
def get_customer(customer_id: str) -> dict:
    return db.fetch(customer_id)  # may return 10+ fields including PII

# Register `get_customer` as a LangChain tool. The agent — and the model
# context, the model logs, and the provider's training pipeline — only
# ever see {name, plan, issue}.

Runnable end-to-end example: examples/langchain_example.py.

CrewAI — different agents, different scopes, same data source

@shield(purpose="customer_support", scope=["name", "plan", "issue"])
def get_for_support(customer_id: str) -> dict:
    return db.fetch(customer_id)

@shield(purpose="billing", scope=["name", "plan", "balance_due", "billing_address"])
def get_for_billing(customer_id: str) -> dict:
    return db.fetch(customer_id)

Support and Billing agents share one db.fetch() but never see each other's fields. Runnable example: examples/crewai_example.py.

MCP / FastMCP

@mcp.tool()
@shield(purpose="customer_support", scope=["name", "issue"])
def get_customer(customer_id: str) -> dict:
    return db.fetch(customer_id)

@shield stacks inside (closer to the function than) any framework decorator. Same pattern works for FastAPI, Litestar, n8n custom nodes, or any other agent surface that calls Python functions.


Use Cases

Quickstart (lite mode, no infrastructure)

from aegis import shield

@shield(purpose="support", scope=["name", "issue"])
def get_customer(customer_id: str) -> dict:
    return db.get_customer(customer_id)

FastAPI

@shield stacks with any framework decorator. Put @shield directly above the function (closest to it):

from fastapi import FastAPI
from aegis import shield

app = FastAPI()

@app.get("/customer/{customer_id}")
@shield(purpose="support", scope=["name", "issue"])
def get_customer(customer_id: str) -> dict:
    return db.get_customer(customer_id)

The HTTP response now contains only name and issue, regardless of what db.get_customer returns.

FastMCP / MCP server tools

from fastmcp import FastMCP
from aegis import shield

mcp = FastMCP("customer-service")

@mcp.tool()
@shield(purpose="customer_support", scope=["name", "issue"])
def get_customer(customer_id: str) -> dict:
    """Look up a customer by ID."""
    return db.get(customer_id)

Every MCP tool call now respects purpose-based access control.

aegis.yaml (centralized policies)

For multi-purpose deployments, define policies once in aegis.yaml:

# aegis.yaml
purposes:
  support:
    scope: ["name", "issue", "profile.age"]
  billing:
    deny_fields: ["card", "ssn", "profile.ssn"]
from aegis import shield

# scope/deny_fields pulled from aegis.yaml
@shield(purpose="support")
def get_customer(id: int) -> dict:
    return db.get(id)

Requires the optional YAML extra:

pip install aegis-trust[yaml]

async functions

@shield works transparently with async def:

from aegis import shield

@shield(purpose="support", scope=["name", "issue"])
async def get_customer(customer_id: str) -> dict:
    return await db.get(customer_id)

Supported return types

@shield normalizes common Python return shapes to dict before filtering, so the wrapped function can return objects directly:

Return type How it's handled
dict filtered directly
list[dict] each element filtered
None passes through
@dataclass instance dataclasses.asdict() → filtered
Pydantic v2 BaseModel .model_dump() → filtered
Pydantic v1 BaseModel .dict() → filtered
SQLAlchemy Declarative instance __table__.columns → filtered
Anything else (int, str, opaque obj) empty value (fail-closed)

Pydantic and SQLAlchemy are detected by duck typing — neither is a dependency of aegis-trust. If the conversion raises, @shield returns empty. Hybrid objects that look like both (Pydantic v2 + SQLAlchemy Declarative, such as SQLModel) resolve via the Pydantic v2 branch so serializer customization is preserved.

from dataclasses import dataclass
from aegis import shield

@dataclass
class Customer:
    name: str
    ssn: str

@shield(purpose="support", scope=["name"])
def get_customer():
    return Customer(name="Aria", ssn="111-22-3333")

get_customer()
# → {"name": "Aria"}

Filtering inside lists

Dot-notation drills into each element when the value is a list of dicts:

from aegis import shield

@shield(purpose="support", scope=["users.name"])   # filter each element
def list_users() -> dict:
    return {"users": [
        {"name": "Aria", "ssn": "111-22-3333"},
        {"name": "Ben",  "ssn": "444-55-6666"},
    ]}

list_users()
# → {"users": [{"name": "Aria"}, {"name": "Ben"}]}

A bare scope=["users"] over a list-of-dicts is ambiguous — it whitelists the key but not the inner fields, so the ssn values would pass through. @shield treats that as fail-closed: the key is dropped and a warning points at the dot-notation fix.

@shield(purpose="support", scope=["users"])    # fail-closed drop
def list_users():
    return {"users": [{"name": "Aria", "ssn": "111"}]}

list_users()
# → {}            # users dropped, warning logged: use 'users.<field>'

Empty lists ([]) and lists of primitives (["red", "blue"]) are released as-is — no inner dicts, no leak path, no warning.

The same contract applies to deny_fields: use deny_fields=["users.ssn"] to remove ssn from each element; a bare deny_fields=["ssn"] removes only the top-level ssn key and does not recurse.

deny_fields (blacklist with dot-notation)

When the safe set is large and the unsafe set is small, blacklist is clearer:

from aegis import shield

@shield(purpose="billing", deny_fields=["ssn", "profile.ssn", "profile.internal_notes"])
def get_customer(id: int) -> dict:
    return db.get(id)

scope and deny_fields are mutually exclusive. Specifying both raises ValueError.


API Summary

@shield(purpose, scope=None, *, deny_fields=None)

Decorator that controls data access based on declared purpose.

  • purpose (str): why the agent needs this data (e.g. "customer_support")
  • scope (list[str]): whitelist — fields the agent is allowed to see
  • deny_fields (list[str]): blacklist — fields to hide; everything else passes

Either scope or deny_fields is required (not both). Both accept dot-notation: ["profile.age"].

On any internal error, the decorated function returns an empty value rather than leaking unfiltered data, exceptions, or tracebacks.

Testing helpers

from aegis.pytest_plugin import assert_shield_blocked, assert_shield_passed

def test_support_agent_cannot_see_ssn(shield_history):
    get_customer("id-1")
    records = shield_history()
    assert_shield_blocked(records, "ssn")
    assert_shield_passed(records, "name")

The shield_history fixture is auto-registered via the pytest11 entry point.

Local history (optional)

Set AEGIS_HISTORY=1 to record every @shield call to a local SQLite store at ~/.aegis/history.db:

AEGIS_HISTORY=1 python my_app.py
aegis history       # show recent calls
aegis stats         # aggregate by purpose / blocked field

Migration from aegis-shield

If you were using the TestPyPI distribution aegis-shield (versions through 0.6.5.1), migrate to aegis-trust:

pip uninstall aegis-shield
pip install aegis-trust

The import path is unchanged (from aegis import shield). No source-code changes are required.

The package was renamed to aegis-trust because aegis-shield was already registered on PyPI by an unrelated party.


Security and cryptographic posture

aegis-trust is fail-closed by design. On any error inside @shield (filtering exception, scope mismatch, internal failure), the decorator returns an empty value rather than leaking unfiltered data, exceptions, or tracebacks.

Release evidence is anchored to the Bitcoin blockchain via OpenTimestamps (OTS) for tamper-evident chronology. As of v0.6.4, attestation hashes use SHA-3-512 (NIST FIPS 202) as a pre-PQC bridging measure. OTS is not a post-quantum cryptography substitute; full PQC migration is on the roadmap.

Vulnerability reports: contact@aegisagentcontrol.com. See SECURITY.md for the full policy.

Beyond local filtering

aegis-trust is the open-source entry point to a broader trust platform. For production deployments with enterprise controls and platform-managed policy orchestration, email contact@aegisagentcontrol.com.

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aegis_trust-0.9.0rc1.tar.gz (88.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aegis_trust-0.9.0rc1-py3-none-any.whl (167.4 kB view details)

Uploaded Python 3

File details

Details for the file aegis_trust-0.9.0rc1.tar.gz.

File metadata

  • Download URL: aegis_trust-0.9.0rc1.tar.gz
  • Upload date:
  • Size: 88.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.12

File hashes

Hashes for aegis_trust-0.9.0rc1.tar.gz
Algorithm Hash digest
SHA256 2bae129825a5fca6e0ad3330ec3470975eec4dae33c10051265fdddc33c3c080
MD5 f1b90dca9aa46ac4f0599a4f9b3aeae7
BLAKE2b-256 d310976f6fcf7b56b73cf436f3f2ced1ad2012ea5b098dfc628e02a66e91c077

See more details on using hashes here.

File details

Details for the file aegis_trust-0.9.0rc1-py3-none-any.whl.

File metadata

  • Download URL: aegis_trust-0.9.0rc1-py3-none-any.whl
  • Upload date:
  • Size: 167.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.12

File hashes

Hashes for aegis_trust-0.9.0rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 1e0a192cb6a35b3190b4cd66b341d7cf18bdbc9de60cdf19ed8d968ae4c508cb
MD5 cf0bd97f140bbf63c433bac283edb72d
BLAKE2b-256 03535189a3015a04efe32ace61f2cde3bd45f58f1955aba25989a55f84de7a51

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page