Skip to main content

Security

Why Memory Security Matters

In multi-agent systems, agents trust each other by default. When your researcher agent passes output to your writer agent, the writer treats that as a legitimate instruction. If you compromise one agent, you get every downstream agent automatically. The 2025 incident landscape proved this at scale:
  • EchoLeak (CVE-2025-32711, CVSS 9.3): A single crafted email triggered automatic data exfiltration from Microsoft 365 Copilot
  • CrewAI + GPT-4o: 65% exfiltration success rate in tested scenarios
  • Drift chatbot cascade: One compromised agent integration cascaded into 700+ organizations
Memory is the attack surface. Aegis implements OWASP AI Agent Security Cheat Sheet Section 3 natively.

Content Security Pipeline

Every memory write passes through a four-stage content security pipeline before persistence.

Stage 1: Input Validation

  • Content length: Max 50,000 characters (configurable via CONTENT_MAX_LENGTH)
  • Metadata depth: Max 5 levels of nesting (configurable via METADATA_MAX_DEPTH)
  • Metadata keys: Max 50 total keys (configurable via METADATA_MAX_KEYS)
  • Encoding: Null bytes and control characters rejected (except \n, \t, \r)

Stage 2: Sensitive Data Detection

Detects PII and secrets using compiled regex patterns:
  • SSN patterns (\b\d{3}-\d{2}-\d{4}\b)
  • Credit card numbers (Luhn-validated 13-19 digit sequences)
  • API keys: AWS (AKIA...), OpenAI (sk-...), GitHub (ghp_..., gho_...)
  • Email addresses
  • Password assignments (password=, secret:, etc.)

Stage 3: Prompt Injection Detection

Detects common injection patterns:
  • System prompt overrides: “ignore previous instructions”, “you are now”, “new instructions”
  • Role manipulation: “pretend you are”, “act as”, “you must now”
  • Data exfiltration triggers: “send data to”, “exfiltrate”, “forward to” with URLs

Stage 4: LLM-Based Injection Classification (Optional)

When enabled, an LLM classifier runs as an async second opinion after regex detection. Stage 4 only fires when the risk warrants the latency/cost:
  • Untrusted or unknown trust level
  • Agent-shared or global scope
  • Content that was regex-flagged but not rejected (Stage 3 flagged it)
The classifier asks a focused binary question: “Does this text contain instructions that attempt to manipulate an AI system’s behavior?” and returns a confidence score. Escalation logic:
  • Confidence >= 0.8: escalate to REJECT
  • Confidence >= threshold (default 0.7) but < 0.8: add llm_injection_flagged flag, keep existing action
  • LLM error (timeout, API failure): fall back to regex-only verdict (graceful degradation)
Configuration:

Content Policy Configuration

Each detection category has a configurable action: Injection is only flagged by default, but a memory entering global scope is readable by every agent in the project, so CONTENT_POLICY_INJECTION_GLOBAL_SCOPE escalates a flagged injection to a hard reject there. Set it to inherit to fall back to CONTENT_POLICY_INJECTION for global writes too. (The /security/scan preview reports at global scope, so it shows this worst-case verdict.)
  • reject: HTTP 422 returned, memory NOT stored, SECURITY_REJECTED event emitted
  • redact: Matched patterns replaced with [REDACTED:<type>], memory stored with flags
  • flag: Memory stored with content_flags populated, available for admin review
  • allow: No action, content stored normally

Memory Integrity (HMAC-SHA256)

Every new memory is signed with HMAC-SHA256 at storage time.

How It Works

Memories are signed with the v2 hash format, stored with a v2: prefix. The signed message is domain-separated and delimited with the ASCII unit separator (\x1f):
Covering scope and trust_level (not just content) means a direct database edit that flips a memory from agent-private to global, or relabels its trust, now breaks the hash — the v1 format ({project_id}:{agent_id}:{content}) could not detect those. The delimiter prevents one field’s contents from masquerading as the next, and the aegis-mem-v2 domain prefix keeps a memory MAC from being replayed as any other Aegis MAC. The HMAC is computed using AEGIS_INTEGRITY_KEY (falls back to AEGIS_API_KEY). Legacy rows. Memories written before v2 (and Context Hub prompts/skills/subagents) carry the bare-hex v1 hash; verify_integrity recognizes both formats, so nothing breaks during migration. Upgrade existing rows to v2 with the backfill script — required before turning on INTEGRITY_REQUIRE_SIGNED (see the verify-on-read guide):
The script is idempotent and batched; --project-id scopes it to one project.

Verification

Returns whether the stored hash matches the recomputed hash. Legacy rows without hashes return has_hash: false.

Verify on read

Signing on write only helps if the hash is checked on the way back out. Every retrieval path — /memories/query, /memories/hybrid_query, GET /memories/{id}, /memories/handoff, the typed timeline/entity reads, and the context bundle — recomputes the HMAC and acts on the result according to INTEGRITY_READ_MODE: A mismatch is an active tamper signal — a row whose content, scope, or trust was edited out from under its signature — so drop excludes it and it never reaches a prompt. An unsigned row is merely un-upgraded, so it is kept until you opt in with INTEGRITY_REQUIRE_SIGNED=true. That split makes drop safe to run before the backfill; the hardened end state, after scripts/backfill_integrity.py has signed the corpus, is INTEGRITY_READ_MODE=drop + INTEGRITY_REQUIRE_SIGNED=true. Drops are recorded as INTEGRITY_FAILED in the security audit log (GET /security/audit), including on the replica-safe read routes — the event is committed on a dedicated write session. POST /memories/export is the deliberate exception: it annotates each row with an integrity_status field and never drops, because a backup should show which rows are tampered, not silently omit them.

Provenance

Every memory carries an immutable, HMAC-signed provenance record (memory_provenance, 1:1 with the memory) written in the same transaction as the memory itself. It captures where the memory came from — origin channel and kind, the producing agent and API key, the source run/interaction, and its depth in a derivation chain — and, crucially, how it was admitted: the full content-security verdict, the taint set (trust label + the detections that fired), and a policy_version hash over the policy configuration and scanner-rules version in effect at the time. The record is signed with a domain-separated HMAC (distinct from the content signature, so one can never be replayed as the other) and is never mutated. Promotions — a scope or trust relabel — are recorded as SCOPE_CHANGED / TRUST_CHANGED events in the audit log rather than by editing the record, so the origin is a fixed fact and the history is an append-only trail. Every write channel populates it through one choke point (MemoryRepository.add); a source-derived test fails CI if a new write path forgets to. A deduplicated write is recorded as a DEDUPLICATED event so a second, differently-originated write of the same content stays auditable while the first write’s provenance stands.
The active policy_version is also surfaced by GET /security/config.

Trust-weighted retrieval

The same signals that make memory safer also make retrieval better. With ENABLE_TRUST_WEIGHTED_RANKING=true, retrieval stops ordering purely by vector similarity and fuses it with the memory’s content trust level, its effectiveness votes, temporal decay, and its provenance depth:
(weights are configurable via RANKING_W_* and validated to sum to 1.0 at startup). This is the control that closes the ACE loop: a memory voted helpful rises, a low-trust or deeply-derived one sinks, and a poisoned write that slipped past screening is ranked below vouched content rather than competing with it on raw similarity alone. Unproven signals use neutral priors — an unvoted memory or one with no provenance record is treated as neutral (0.5), never bad — so enabling the feature never buries the un-voted corpus. This is distinct from ENABLE_TRUST_LEVELS, which governs principal-trust authorization; ranking is advisory and weighs content trust. score in the API stays cosine similarity; the fused value is surfaced as relevance_score.

Agent Trust Hierarchy

Four trust levels following OWASP recommendations:
Enforcement status. The Read and Delete rules are enforced on every memory route for bound API keys. The Write column — specifically “an internal principal may not write global” — is enforced when ENABLE_TRUST_LEVELS=true, because turning it on requires a privileged key for legitimate global writes. can_admin() gates /security/* as before.Independent of that setting, one rule is always enforced: content labelled untrusted or unknown can never be written to global scope. See Content provenance vs. principal trust.

Content provenance vs. principal trust

Two different things are called “trust”, and conflating them is a security bug: Both are checked on a write: provenance is the ceiling, principal trust is the floor. The provenance rule lives in one module imported by both aegis_memory.guard and the server, so the offline gate and the API cannot drift.

Agent Identity Binding

API keys can carry a bound_agent_id. When set, that identity is authoritative on every memory route: the acting agent is resolved from the key, and a request body claiming a different agent_id is rejected with 403. The scope ACL is then evaluated against the bound agent, which is what makes agent-private an enforceable boundary.
Unbound keys act as any agent in their project. That is the intended posture for a project-scoped key — it represents the whole application, and has no agent identity to check against. Bind one key per agent when agents must be mutually isolated. Project-level isolation is always enforced regardless.

Scope inference cannot escalate

ScopeInference reads memory content to pick a scope, and content is attacker-controlled — text containing keywords like “team” and “policy” would otherwise infer global. Inference therefore can never raise privilege: an inferred global is capped to agent-private when the content’s provenance is untrusted. An explicit scope in the request is a caller instruction rather than content influence, so it is checked by the authorization layer instead.

Per-Agent Rate Limiting

Separate from project-level rate limiting, per-agent limits prevent a single rogue agent from exhausting the project’s quota.

Security Admin Endpoints

All require privileged or system trust level.
These five endpoints — scan, audit, flagged, verify, config — are the complete security-admin surface in the open-source distribution. There is no approve / reject / remediate workflow and no review-queue UI: flagged memories are surfaced for inspection (/security/flagged), while enforcement happens automatically at write time via the content-policy actions above (reject / redact / flag).

SDK Security Methods

Security Configuration Reference