AI agent data access control — control what agents can see
Project description
aegis-trust
The trust layer for AI agents. One decorator declares the purpose; the SDK enforces what data the agent is allowed to see. Local-first AI agent data access control — no infrastructure, no telemetry.
pip install aegis-trust
You declare what each purpose is allowed to see. Everything else is filtered out before the agent gets it.
30-Second Quickstart
from aegis_trust 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_trust 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.
Use Cases
Quickstart (lite mode, no infrastructure)
from aegis_trust 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_trust 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_trust 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_trust 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_trust 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_trust 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_trust 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_trust 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 seedeny_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_trust.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
Mode (LITE / FULL / AUTO)
| Mode | Behaviour | Requires |
|---|---|---|
LITE |
In-process filter only. Deterministic, no I/O. | nothing |
FULL |
Filter + audit chain ingest + central policy sync via aegis-core. | aegis-core running + AEGIS_TOKEN |
AUTO |
Probe-first detection. See AUTO behaviour matrix below. | nothing |
AUTO behaviour matrix (rc4+)
AEGIS_MODE=auto (the default) probes the backend FIRST (re-probe TTL = 60 s) and consults the Full-intent heuristic only when the probe fails. Behaviour:
AEGIS_MODE=lite→ Lite.AEGIS_MODE=full→ Full (calls fail-closed at the gateway until the backend recovers).AEGIS_MODE=auto+ no Full intent (noAEGIS_TOKENAND no non-dev URL) → Lite.AEGIS_MODE=auto+ Full intent + reachable backend → Full (opportunistic upgrade).AEGIS_MODE=auto+ Full intent + unreachable backend → fail-closed Full + onelogger.warning. Silent LITE degrade is suppressed because it would skip the user-visible warning and provide weaker semantics than the user asked for.
Full mode env vars
| Variable | Default | Meaning |
|---|---|---|
AEGIS_URL |
https://localhost:8443/api/v1 |
aegis-core REST endpoint (rc4+ canonical; parity with npm). |
AEGIS_BASE_URL |
— | npm-parity deprecation alias for AEGIS_URL. Read only when AEGIS_URL is unset; emits one logger.warning per process the first time it is read (re-armed by reset()). Removed in v1.0.0. |
AEGIS_TOKEN |
(empty) | Bearer token for auth |
AEGIS_MODE |
auto |
Override mode detection (full / lite) — see matrix above. |
AEGIS_HISTORY |
(unset) | 1 to enable local audit log (~/.aegis/history.db). |
AEGIS_HISTORY_PATH |
~/.aegis/history.db |
Local audit file. |
FULL mode — gateway trust-boundary guarantees
When shield() runs in FULL mode it calls the aegis-core gateway's /check-access endpoint before filtering. As of the Core Security Remediation track (CSR 4/4, landed in aegis-core 2026-05-21) that ingress provides four scoped guarantees:
- Identity binding —
/check-accesstreats the identity established by the gateway's auth middleware as the sole authoritative requester identity (the JWTsubfor Bearer-JWT auth; the literalapi-keyfor API-key auth). A request body that claims a differentrequester_idis denied (HTTP 403) with anidentity_mismatchaudit record. - Ingress denial of unknown inputs — an unknown
purpose, an unknownscope, or a malformed / path-traversalcapsule_idis denied (HTTP 403) at the/check-accessingress, each with an audit DENY record. (The unknown-purpose denial is RBAC-pathed; unknown-scope and malformed-capsule carry dedicatedpolicy.*audit reasons.) - Audit-or-deny — a
/check-accessdecision fails closed if its audit record cannot be written: the gateway returns HTTP 503 rather than a silently-unaudited 200 ALLOW or 403 DENY. - Boot-time config validation — started with
AEGIS_PROFILE=production, the gateway fails its own boot (exit(2)) on missing critical config keys, disabled security controls, an enabled legacy dashboard socket, or an unparseable / zeroAEGIS_REST_PORT, instead of degrading silently.AEGIS_PROFILEunset ordevelopmentkeeps the pre-existing permissive behaviour.
Scope of these guarantees — read before relying on them:
- The audit-or-deny guarantee (#3) applies to the
/check-accessendpoint only. It is not a gateway-wide audit fail-closed guarantee; other gateway endpoints are not yet swept. - The
/check-accessscope check (#2) validatesscopeagainst a known registry. It is not purpose × scope field-level minimum-disclosure enforcement; field-level redaction by purpose × scope is not wired. AEGIS_PROFILE=productionvalidation (#4) is operator opt-in. The gateway is not production-ready out of the box; the default profile keeps silent config fallbacks.- These four guarantees are
/check-access-scoped and do not amount to an all-gateway-operations audit-complete claim.
Known follow-ups — tracked, not yet shipped:
- A missing
AEGIS_CAPSULE_ROOTcan still produce a runtime HTTP 500 with no audit record; that 500 path is evaluated after the identity check (#1) but before the unknown-purpose / scope / capsule checks (#2), so it pre-empts guarantee #2. - Gateway-wide audit-append fail-closed sweep (beyond
/check-access). - Debug-log redaction (
RUST_LOG=debugoutput hygiene). - Wiring validated
scopethrough to RBAC / Reflex / field-level enforcement. - SDK access-cache TTL:
authorize()caches an allow decision for 30 s (_ACCESS_CACHE_TTL_S). Deny decisions are never cached (fail-closed). A same-token server-side policy change is invisible to this SDK process for ≤ 30 s after the last allow; a token rotation (set_token(...)) invalidates the entire allow-cache immediately by epoch bump. Parity with the TypeScript SDKACCESS_CACHE_TTL_MS = 30_000(same value, same fail-closed deny semantics). A bounded TTL window for allow decisions is the explicit trade-off; operators that require zero allow-staleness should callset_token()on policy change.
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 was renamed to match the package: use from aegis_trust import shield (v0.9.0-rc2+). The legacy from aegis import shield continues to work via a back-compat shim that emits a DeprecationWarning and is slated for removal in v2.0.0.
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aegis_trust-0.9.0rc6.tar.gz.
File metadata
- Download URL: aegis_trust-0.9.0rc6.tar.gz
- Upload date:
- Size: 84.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ad9fcc2e15108c12da6eb1e36bd12778a9382b539021a2238ef3f472d9a66fe
|
|
| MD5 |
19cd42626acb535a8d64c5fd27da9924
|
|
| BLAKE2b-256 |
b5a1b632a852939853102dea014f85daa050781e737e5dea1ce29e42b6714594
|
File details
Details for the file aegis_trust-0.9.0rc6-py3-none-any.whl.
File metadata
- Download URL: aegis_trust-0.9.0rc6-py3-none-any.whl
- Upload date:
- Size: 119.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e9e79e1f8dd3aab0ed53cb447012703718c100f0d04a43eaf2b1effc108bd7c
|
|
| MD5 |
477a4170fb636694a0b40d877785ad52
|
|
| BLAKE2b-256 |
47ddf367e91466982241fe5f939574572066ac439ff7e6af08c34422f95bd5e9
|