Skip to main content
Glama
README.md
# aegis-crew

[![ci](https://github.com/pvfOliveira/aegis-crew/actions/workflows/ci.yml/badge.svg)](https://github.com/pvfOliveira/aegis-crew/actions/workflows/ci.yml)

A multi-agent analyst crew — Researcher, Writer, Reviewer on LangGraph — wrapped
in a defence-in-depth security envelope. Agents cannot import their tools: every
tool call crosses a real MCP boundary and is capability-checked by a zero-trust
`CapabilityBroker`, input and output pass deterministic OWASP LLM Top-10
guardrails, and the one side-effecting action (publishing the report) is held at
a human-in-the-loop gate. The thesis: the security layer is constant and the
orchestrator is swappable — CrewAI, AutoGen, BeeAI, Microsoft Agent
Framework, and Google ADK all drive the same secured tools through the same broker.

On top of the always-on envelope sit **four opt-in security tiers**, each
independently switchable and composable:

| Tier | Flag | What it guarantees |
|---|---|---|
| **Rule of Two** (session policy) | `--rule-of-two` | No session combines untrusted inputs + sensitive-data access + external side effects without an explicit human grant. |
| **Dual-LLM quarantine** (semantic) | `--quarantine` | No provider call ever carries both tool access and untrusted bytes. |
| **Sandboxed tools** (runtime) | `--sandbox` | The tool server runs inside a deny-by-default macOS Seatbelt profile derived from the broker's capabilities. |
| **Behavioral baseline** (egress drift) | `--baseline` | Outgoing reports are scored against a profile fitted on known-good output; off-profile publishes warn or block. |

Everything is testable without an API key: **351 deterministic safety tests**
cover every control with negative-control discipline, and a separate live tier
sends real adversarial probes through a real model.

**Start with the design rationale:**
[docs/ARCHITECTURE-DEEP-DIVE.md](docs/ARCHITECTURE-DEEP-DIVE.md) — a full
walkthrough of the architecture, the trade-offs, and the threat model behind it.

---

## Quickstart

This project requires [uv](https://docs.astral.sh/uv/): `[tool.uv]
override-dependencies` resolves a metadata-only `json-repair` pin conflict that
plain pip cannot apply, so `pip install` fails where `uv sync` succeeds.

```bash
uv sync --extra dev
cp config.example.toml config.toml    # edit provider/security settings
cp .env.example .env                  # set ANTHROPIC_API_KEY for live runs

# Key-free demo — scripted provider, full pipeline, no network:
uv run aegis run "grid storage policy" --provider fake --auto-approve

# Deterministic safety suite (no API key):
uv run --extra dev pytest tests/safety -q
```

The report lands in `out/report.md`, written only after the HITL gate approves.

### Live interactive run

```bash
uv run aegis run "renewable energy subsidies" --provider anthropic
```

The crew runs against the real model. When the reviewer approves the draft, the
run **parks** at the HITL gate and prints the proposed report. You are prompted
to approve (`y`) or reject (`n`) before `out/report.md` is written.

Providers: `fake` (scripted, deterministic), `anthropic`, `ollama`, plus
first-party `openai`, `gemini`, `mistral`, and `watsonx` adapters.
`config.toml` keys of interest:

```toml
[provider]
default = "anthropic"   # anthropic | ollama | fake | openai | gemini | mistral | watsonx
anthropic_model = "claude-sonnet-4-6"

[security]
output_dir = "out"
corpus_dir = "corpus"
reflexion_max_retries = 2
```

The `kb_search` tool reads `.md`/`.txt` documents from `corpus/` (example
policy documents ship in the repo).

### Standalone MCP tool server (stdio)

```bash
uv run aegis serve-mcp
```

Starts the FastMCP tool server on stdio. Any MCP-compatible client (e.g. Claude
Desktop) can connect and call the three tools (`kb_search_tool`,
`read_doc_tool`, `publish_report_tool`) through the zero-trust broker.

> **Note on retrieval quality:** `MockEmbedding` (offline, deterministic) is the
> default so all tests and the `--provider fake` demo work without API keys.
> Retrieval is illustrative in this mode — pass a real embedding model via
> `build_index(embed_model=...)` for production-grade semantic search.

---

## Architecture

```
User topic
    |
    v
[scan_input / LLM01]          ← guardrail: prompt-injection / jailbreak
    |
    v
  StateGraph (LangGraph)
  ┌──────────────────────────────────────────────────────────┐
  │                                                          │
  │  START → researcher ──────────────────────────────────► │
  │              (ReAct loop)                                │
  │                  │ MCP boundary (MCPToolClient)          │
  │                  │   └── CapabilityBroker.authorize()    │
  │                  │   └── check_tool_args / LLM06        │
  │                  ▼                                       │
  │              writer                                      │
  │                  ▼                                       │
  │              reviewer  ──"retry"──► writer               │
  │              (Reflexion)                                 │
  │                  │ "ok"                                  │
  │                  ▼                                       │
  │           hitl_gate  ── reject ──► END                  │
  │           (interrupt())                                  │
  │                  │ approve                               │
  │                  ▼                                       │
  │           publisher  ── MCP ──► publish_report_tool      │
  │                  │   └── check_egress / LLM02           │
  │                  ▼                                       │
  │                 END                                      │
  └──────────────────────────────────────────────────────────┘
    |
    v
  out/report.md    (written only after HITL approval)
```

Key components:

| Layer | Artifact |
|---|---|
| Orchestration | `src/aegis/graph.py` — LangGraph `StateGraph` |
| MCP boundary | `src/aegis/mcp/server.py` + `src/aegis/mcp/client.py` |
| Zero-trust | `src/aegis/security/zero_trust.py` — `CapabilityBroker` + `AuditLog` |
| Guardrails | `src/aegis/security/guardrails.py` — LLM01/02/05/06 |
| HITL gate | `src/aegis/graph.py:257` — `interrupt()` before publish |
| Agents | `src/aegis/agents/{researcher,writer,reviewer}.py` |
| State | `src/aegis/state.py` — `CrewState` (Pydantic) |
| CLI | `src/aegis/cli.py` |

Full threat analysis: [`docs/THREAT_MODEL.md`](docs/THREAT_MODEL.md) (STRIDE
per component).

---

## The always-on envelope

- **Zero-trust `CapabilityBroker`** (`src/aegis/security/zero_trust.py:45`) —
  every tool call is explicitly authorized against a per-tool capability set;
  denials are recorded in the `AuditLog` (`zero_trust.py:26`). No tool runs
  without explicit allowance.

- **OWASP LLM Top-10 guardrails** (`src/aegis/security/guardrails.py`) — four
  guards mapped to four controls:
  - `scan_input` → LLM01 (Prompt Injection / Jailbreak)
  - `check_egress` → LLM02 (Sensitive Information Disclosure)
  - `validate_output` → LLM05 (Improper Output Handling)
  - `check_tool_args` → LLM06 (Excessive Agency)
  - Full mapping: [`docs/OWASP_MAPPING.md`](docs/OWASP_MAPPING.md)

- **Human-in-the-loop gate** (`src/aegis/graph.py:257`) — LangGraph
  `interrupt()` parks the run before the one side-effecting tool
  (`publish_report_tool`). A rejected decision routes to `END` without writing
  any file.

- **Audit log** (`src/aegis/security/zero_trust.py:26`) — all broker allow/deny
  decisions are recorded with tool, action, target, verdict, and reason.

- **MCP choke point** — tools are exclusively reachable via the `MCPToolClient`
  boundary; agents have no direct function references. The broker enforces
  authorization at the server side of that boundary.

The spine files behind these controls are **byte-frozen**:
`tests/safety/test_no_security_regression.py` guards them with a git-diff check
plus SHA-256 digest pins, so every later capability had to compose *on top of*
the controls instead of editing them. The frozen guardrails are also
mutation-tested — see [`docs/mutation-testing.md`](docs/mutation-testing.md).

---

## The four security tiers

### Rule of Two (session policy, `--rule-of-two`)

Meta's "Agents Rule of Two": an agent session may combine at most two of
**[A] untrustworthy inputs**, **[B] sensitive-system/private-data access**,
**[C] state-change/external communication**. Needing all three means the session
must not run autonomously — human approval is the mandated minimum.

aegis enforces this mechanically: a `SessionLedger` accumulates factors as the
broker authorizes calls (`read`→B, `write`→C, `network`→A+C, unknown→all three,
fail-closed; carve-outs are explicit declarations), and `RuleOfTwoBroker` (a
drop-in subclass of the frozen `CapabilityBroker`) denies, at authorize-time,
any call whose factors would complete the triple without a human grant. The
HITL gate's approval *is* the grant: approving the publish unlocks
`EXTERNAL_WRITE` for the session; rejecting leaves the third factor locked even
if graph routing were bypassed. Every decision lands in the same audit log.

Enforcement is in-process: in `aegis run` (in-memory MCP) the server and graph
share one broker and one ledger, so enforcement is end-to-end. In `serve-mcp`
stdio mode the ledger lives inside the server process: tool-side factor
accumulation and denial are enforced there, but graph-side marks and HITL
grants cannot reach it — a cross-process grant channel is future work, not
claimed. Classification is structural, not semantic: Rule of Two bounds blast
radius; it does not detect injection — the guardrails do that, and neither
subsumes the other.

### Dual-LLM quarantine (semantic tier, `--quarantine`)

"Design Patterns for Securing LLM Agents against Prompt Injections"
([arXiv:2506.08837](https://arxiv.org/abs/2506.08837)) states the principle: once
an agent has ingested untrusted input, it must be *impossible* for that input to
trigger consequential actions. `--quarantine` turns the crew into the paper's
**Dual LLM** pattern (with [CaMeL](https://arxiv.org/abs/2503.18813) as the
data-flow lineage):

- the **privileged** researcher keeps its tools but never sees untrusted bytes —
  tool results are parked in a `QuarantineStore` and appear to the model only as
  opaque refs (`⟦Q1⟧ from kb_search (1204 chars)`) plus validated signals;
- a **quarantined** model (never given tools) reads the untrusted text and
  answers only schema-validated primitives (bool / bounded int / fixed choice);
  a reply that fails strict validation is re-asked once, then rejected
  fail-closed — free text never crosses back;
- the **writer and reviewer** see the content but are quarantined roles:
  structurally tool-less (a regression-pinned property, not an accident);
- the **publisher** boundary is unchanged: HITL + broker + egress guards + Rule
  of Two when enabled.

The enforced, tested invariant: **no provider call ever carries both tool access
and untrusted bytes.** Default-off; `[security] quarantine_provider` optionally
routes the quarantined model to a cheaper local provider.

Honesty notes, verbatim from the design review: "Quarantine bounds capability and data
flow, not signal truthfulness: extracted signals are derived from untrusted
content and remain adversary-influenceable. An attacker can lie to the relevance
check; they cannot make the privileged model see their text or invoke a tool
with it." "The quarantined model can still be prompt-injected. The guarantee is
that injection there is inconsequential by construction: the quarantined call
carries no tool access, and its only output channel is a schema-validated
primitive — free text never crosses back to the privileged side." "Writer and
reviewer see untrusted content by design; they are quarantined roles —
structurally tool-less. The consequential boundary remains the publisher path
(HITL + broker + egress guards + Rule of Two when enabled). Detection
(guardrails) and structure (quarantine) are complementary; neither subsumes the
other."

**Stretch rung — `--quarantine-mode plan` (CTE-lite):** the privileged model
emits a full typed plan (pydantic-validated verbs: `search` / `read` /
`extract`) from the topic alone, *before any untrusted data exists*; a
deterministic executor walks it through the same broker-checked MCP path.
Honest label, verbatim: "This is Plan-Then-Execute with typed steps and
capability-checked execution (CTE-lite). Full CaMeL-style data-flow labels on
variables are future work, not claimed."

### Sandboxed tool execution (sandbox tier, `--sandbox`)

```bash
uv run aegis run "renewable energy subsidies" --sandbox --provider fake --auto-approve
```

OWASP ASI02 ("Tool Misuse") calls for two layers: the broker that authorizes a
tool call, and a runtime boundary that survives the broker being wrong. `aegis
run --sandbox` adds the second layer: the `serve-mcp` tool server is launched
inside a macOS `sandbox-exec` (Seatbelt) subprocess under a caps-derived
deny-by-default profile (`security/sandbox.py`, `SandboxPolicy.from_caps`) —
filesystem access is scoped to exactly the read/write paths the broker's
capabilities declare, and network is denied outright. The broker remains the
policy decision point inside that subprocess; the sandbox bounds the blast
radius if the broker is wrong or the tool runtime is compromised.

The launch writes two inspectable artifacts under `out/sandbox/`:
`profile.sb` (the actual Seatbelt profile text) and `config.toml` (the
secrets-free config the sandboxed server reads). Only the profile's
`sha256_16` enters the shared `AuditLog` as a `sandbox`/`launch` record
(`security/sandbox.py:launch_audit_record`) — the content lives on disk for
inspection, not duplicated into the audit trail.

`--sandbox` composes with `--quarantine` and `--auto-approve`. It refuses
`--rule-of-two`: with `--sandbox`, tool-side capability decisions are made
and audited *inside* the sandboxed server process, and Rule of Two's session
ledger — an in-process object — cannot span that process boundary, so the
combination is rejected at the CLI rather than silently downgraded to
graph-side marks only.

| Combination | Result |
|---|---|
| `--sandbox` + `--quarantine` + `--auto-approve` | valid |
| `--sandbox` + `--rule-of-two` | refused (`BadParameter`) |

Honesty guards, verbatim from the design review:

1. *"Broker authorizes, sandbox contains. The broker remains the policy decision
   point; the sandbox bounds the blast radius of a tool-runtime compromise or a
   broker misconfiguration. Neither subsumes the other, and neither makes tool
   OUTPUT trustworthy — untrusted-content handling remains the quarantine and
   guardrail tiers' job."*
2. *"`sandbox-exec` is deprecated as a public CLI but is the OS-shipped Seatbelt
   mechanism that still underpins macOS app sandboxing. This tier is best-effort
   local containment on a developer machine, not a certified security boundary and
   not E2B: no microVM, no snapshotting, no network allowlisting — egress is simply
   denied."*
3. *"The sandbox tier is darwin-only and fails closed: on any other platform
   `--sandbox` is an error, never a silent downgrade to unsandboxed execution. A
   Linux backend (namespaces/containers) is future work."*

### Behavioral baseline (egress drift tier, `--baseline`)

```bash
uv run aegis baseline-fit ./known-good-reports -o profile.json
# config.toml: [security] baseline_profile = "profile.json"
uv run aegis run "renewable energy subsidies" --baseline --provider fake --auto-approve --config config.toml
```

`security/baseline.py` wires [portcullis](https://github.com/pvfOliveira/portcullis)'s
`BaselineMonitor` into the publisher boundary as an optional, default-off egress
tier, run *after* `check_egress` and *before* the MCP publish call: the outgoing
report is embedded and scored against a profile fitted offline on known-good
reports. This is a behavioral tier, not a content tier — it complements the
existing guardrail and egress checks, it does not replace them.

**Fit → enforce lifecycle:** `aegis baseline-fit <known-good-dir> [-o profile.json]
[--threshold-percentile 95.0]` fits a profile on `*.md`/`*.txt` reports and saves it
as JSON; `[security] baseline_profile = "profile.json"` in the config points
`aegis run --baseline` at it. Needs the `[baseline]` extra
(`pip install 'aegis-crew[baseline]'`), lazy-imported inside `security/baseline.py`
so the core install and the deterministic safety suite never need it.

**Warn vs. block:** `--baseline` alone runs in `warn` mode — drift is recorded on
the audit trail and the run still publishes. `--baseline-mode block` refuses an
off-profile publish with a content-free `BLOCKED: ...` critique, same shape as an
egress denial. Audit records land on the same ledger as every other tier:
`baseline / egress / report.md / allow|deny`, with reasons carrying
distance/threshold/destination — never report text.

**Composition:** `--baseline` composes with `--rule-of-two`, `--quarantine`, and
`--sandbox` — verified end-to-end by
`tests/safety/test_baseline.py::test_baseline_composes_with_rule_of_two`,
`::test_baseline_composes_with_quarantine`, and `::test_baseline_composes_with_sandbox`
respectively (the monitor and embedder run in the graph process; `--sandbox`
contains a disjoint layer, the `serve-mcp` subprocess).

Honesty guards, verbatim from the design review:

1. *"A behavioral baseline is anomaly detection, not attack detection: it flags
   conversations that drift from the fitted profile. A novel-but-benign topic can
   flag; an attack phrased on-profile will not. It bounds drift — it does not
   detect prompt injection or PII. Run it alongside the content gate, never
   instead of it."*
2. *"Fail posture is fail-closed with no override exposed: an embedder failure
   blocks the publish even in warn mode. Errors never downgrade to a warning
   silently."*
3. *"HashingEmbedder is deterministic and content-sensitive but NOT semantic: it
   demonstrates the wiring and the fit/enforce lifecycle, not detection quality.
   Hand a real embedding model to LlamaIndexEmbedder for any real deployment, and
   fit and enforce with the SAME embedder — profiles are embedder-specific."*

The default no-key path uses `HashingEmbedder` (stdlib token-hash bag, unit
vector) — deterministic and content-sensitive, but not semantic (guard 3 above).
`LlamaIndexEmbedder` adapts any llama-index `BaseEmbedding` for a real
deployment. Fit and enforce **must** use the same embedder: profiles are
embedder-specific.

---

## Capabilities

Everything below composes on the frozen spine; each item names the module that
implements it and the test or CLI command that exercises it.

### Broker & policy

- **Zero-trust broker + audit log** (`security/zero_trust.py`) — deny-by-default
  per-tool capability sets; every allow/deny audited. Exercised by
  `tests/safety/test_zero_trust.py`.
- **Rule-of-Two session ledger** (`security/rule_of_two.py`, `security/policy.py`)
  — factor accumulation + authorize-time denial, HITL approval as the human
  grant. Exercised by `tests/safety/test_rule_of_two.py`.
- **Sandbox policy derivation** (`security/sandbox.py`) — a Seatbelt profile
  generated from the broker's capability set, deny-by-default, network denied.
  Exercised by `tests/safety/test_sandbox.py`.
- **HITL gate** (`graph.py`, `security/hitl.py`) — `interrupt()` before the one
  side effect; resume via `Command(resume=...)`. Exercised by
  `tests/safety/test_graph_hitl.py`.

### Guardrails & spine

- **OWASP LLM Top-10 guards** (`security/guardrails.py`) — LLM01/02/05/06,
  deterministic, mutation-tested
  ([`docs/mutation-testing.md`](docs/mutation-testing.md)). Exercised by
  `tests/safety/test_guardrails.py` with negative controls.
- **Dual-LLM quarantine** (`security/quarantine.py`, `security/plan_execute.py`)
  — the semantic tier above. Exercised by `tests/safety/test_quarantine.py`.
- **Behavioral baseline** (`security/baseline.py`) — the egress drift tier
  above. Exercised by `tests/safety/test_baseline*.py` (skips cleanly without
  the `[baseline]` extra).
- **NeMo Guardrails integration** (`security/nemo_rails.py`) — an `LLMRails`
  input rail delegating to the frozen `scan_input`; blocks injection with no
  model in the deterministic test, and with a live Ollama in the live test.
- **Adversarial hardening** (`adversarial/defense.py`) — prompt-space evasion
  bench against the LLM01 guardrail; measured attack success rate 0.78 bare →
  0.00 with `harden` (obfuscation-folding `normalize`), `guardrails.py`
  untouched. CLI: `aegis adversarial bench`.
- **MCP tool-poisoning defense** (`mcp/tool_scan.py`) — `vet_tools` scans
  advertised tool metadata for smuggled instructions; `ToolPin`/`detect_rugpull`
  catch post-approval description swaps. CLI: `aegis mcp scan`.
- **Data-poisoning attack + detector** (`adversarial/poison.py`) — plants a
  poisoned corpus doc and detects it by reusing the frozen LLM01 guardrail plus
  a duplication check. CLI: `aegis adversarial poison-scan`.
- **Model-extraction attack + defense** (`adversarial/extraction.py`) — a
  surrogate fit on victim queries; measured fidelity degradation 0.96 → 0.57
  under the perturbation/budget defense.

### Orchestration & interop

- **The crew** (`graph.py`, `agents/`) — ReAct researcher, writer, Reflexion
  reviewer with bounded retry, HITL publisher, on a LangGraph `StateGraph`.
- **Swappable orchestrators** (`orchestrators/`) — CrewAI, AutoGen, BeeAI,
  Microsoft Agent Framework, and Google ADK each drive the *same* secured MCP
  tools over the `serve-mcp` stdio contract via their native adapters; none
  re-implements a tool. Structural tests are model-free; live smoke tests run
  against local Ollama (`qwen3:8b`).

  ```bash
  uv run aegis run "grid storage policy" --orchestrator crewai   # | autogen | beeai
  ```

- **Five orchestration topologies** (`topologies/`) — supervisor,
  plan-and-execute (replan-on-failure), routing/handoffs, swarm
  (decentralized `Command(goto=...)`), and debate (judge sides with either
  debater), each a separate LangGraph over the same secured boundary. CLI:
  `aegis topo run --pattern <name> TOPIC`.
- **A2A protocol** (`protocols/`) — the crew is callable as a Google
  Agent2Agent agent (`aegis serve-a2a`) and can delegate to peers
  (`aegis a2a-call`); inbound messages are guardrail-scanned, outbound peer
  calls are deny-by-default and audited. Needs the `[a2a]` extra.
- **Computer use** (`agents/computer_use.py`, `mcp/tools/browser.py`) — a
  bounded perceive→decide→act loop over a gated Playwright `BrowserTool`;
  origins and action verbs are allowlisted, sensitive actions (e.g. `submit`)
  park at HITL. CLI: `aegis browse URL TASK`. Needs the `[browser]` extra.
- **Evaluation harness** (`eval/harness.py`) — `aegis eval` runs each task
  through the real crew and scores trajectory + safety-held + LLM-judge
  (deterministic `StubJudge` by default, `--live` for an Ollama judge);
  `--metric tool-accuracy` scores F1 over expected-vs-actual tool-call sets,
  and topology runs take a caller-supplied success predicate.

### Memory & privacy

- **Three-tier memory** (`memory/store.py`) — episodic/semantic/scratch over
  SQLite; `remember`/`recall` are capability-checked through the same broker
  (memory paths are scoped under the run's output dir). Exercised by
  `tests/safety/test_memory.py` and the crew-wiring tests.
- **Semantic fact store + consolidation** (`memory/semantic_store.py`,
  `memory/consolidate.py`) — fact/preference store with dedup on
  `(subject, predicate)`; `consolidate()` extracts durable facts from the
  episodic log and shrinks event volume while preserving knowledge. CLI:
  `aegis memory facts`.
- **Context engineering** (`context/assembler.py`) — `ContextAssembler` packs
  prioritized sections into a token budget, compressing lowest-priority first;
  counter and summarizer are dependency-injected, so it tests
  deterministically.
- **PII redaction** (`privacy/redaction.py`) — `Redactor` with `redact` and
  `block` policies. CLI: `aegis privacy redact PATH`.
- **Differential privacy** (`privacy/dp_train.py`) — a real Opacus DP-SGD
  micro-train reporting the spent budget (**ε≈2.25, δ=1e-5**; CPU — Opacus
  grad-sample hooks are MPS-incompatible). CLI: `aegis privacy dp-train`.
  Needs the `[privacy]` extra.
- **Federated learning simulation** (`privacy/federated.py`) — a labeled FedAvg
  simulation (honestly labeled: simulation, not a distributed deployment).

### Supply chain & governance

- **Artifact attestation** (`supplychain/provenance.py`, `supplychain/verify.py`)
  — Ed25519 in-toto-style sign + verify-on-load; a tampered artifact is blocked
  before use. In-house local signing, NOT keyless sigstore. CLI: `aegis attest`,
  `aegis verify`.
- **SBOM** (`supplychain/sbom.py`) — CycloneDX-style SBOM for installed
  packages. CLI: `aegis sbom`.
- **cosign signing** (`supplychain/cosign.py`) — key-based blob sign/verify via
  the cosign CLI (key-based, NOT keyless-OIDC). CLI:
  `aegis provenance cosign-sign`.
- **C2PA content provenance** (`provenance/c2pa.py`) — signs images with a
  local self-signed ES256 credential; `verify_image` performs real
  tamper-detection on the manifest. CLI: `aegis provenance c2pa-sign` /
  `c2pa-verify`. Needs the `[c2pa]` extra.
- **Governance gate** (`governance/policy_engine.py`) — deny-by-default
  `GovernanceGate` over a NIST/ISO/EU-AI-Act risk register mapped to real
  callables; exits 1 on violation. Scope: an executable **pre-flight / CI
  gate**, not a runtime interceptor wired into `graph.py`. CLI:
  `aegis govern check` / `report`. Mapping:
  [`docs/AI_GOVERNANCE.md`](docs/AI_GOVERNANCE.md).
- **ISO/IEC 42001** (`governance/aims.py`) — `AimsGate` binds 5 clauses to real
  callables and blocks on nonconformity. CLI: `aegis govern aims-check`.
  Mapping: [`docs/ISO_42001_AIMS.md`](docs/ISO_42001_AIMS.md).
- **OWASP Agentic Top-10 map** (`security/agentic_map.py`) — an executable
  threat→control map over 8 threats: 6 controlled, 2 documented gaps (T4
  Resource Overload, T7 Cascading). CLI: `aegis agentic map` / `check`.
  Mapping: [`docs/OWASP_AGENTIC_TOP10.md`](docs/OWASP_AGENTIC_TOP10.md).
- **Executable attack trees** (`threat/attack_tree.py`) — extends the STRIDE
  model; every leaf must resolve to a real aegis control or the test fails.
  CLI: `aegis threat tree`.
- **Confidential computing** (`confidential/`) — **a design study, not built: I
  have no TEE hardware.** An attestation-gated inference stage refuses to run
  without a TEE quote (`aegis confidential attest` exits nonzero here); nothing
  is simulated. Design doc:
  [`docs/CONFIDENTIAL_COMPUTING.md`](docs/CONFIDENTIAL_COMPUTING.md).

### Optional extras

Heavy dependencies are lazy-imported inside the functions that need them, so
the core install and the deterministic safety suite never require any of
these:

```bash
pip install 'aegis-crew[a2a]'       # a2a-sdk — serve-a2a / a2a-call
pip install 'aegis-crew[browser]'   # playwright — browse (then: python -m playwright install chromium)
pip install 'aegis-crew[privacy]'   # torch + opacus — privacy dp-train
pip install 'aegis-crew[agentic]'   # agent-framework-core — the MAF orchestrator
pip install 'aegis-crew[adk]'       # google-adk[mcp] + litellm — the Google ADK orchestrator
pip install 'aegis-crew[rails]'     # nemoguardrails — the NeMo rail layer
pip install 'aegis-crew[c2pa]'      # c2pa-python — provenance c2pa-sign / c2pa-verify
pip install 'aegis-crew[baseline]'  # portcullis — the behavioral-baseline tier
pip install 'aegis-crew[platform]'  # convenience bundle: a2a + browser
```

---

## Testing

Two tiers, intentionally separated:

### Deterministic safety suite (no API key)

```bash
uv run --extra dev --extra baseline --extra adk pytest tests/safety -q
```

**351 tests** cover every security control, the guardrail negative-control
discipline, HITL interrupt/resume, the MCP boundary, the zero-trust broker, all
four tiers and their compositions, and CLI smoke. **If a control is removed,
its corresponding test fails** — the suite is written as a regression harness,
not just coverage. `tests/safety/test_baseline*.py` skip cleanly (via
`pytest.importorskip`) when the `[baseline]` extra is not installed — the suite
stays green either way.

### Live adversarial red-team suite (requires `ANTHROPIC_API_KEY`)

```bash
ANTHROPIC_API_KEY=sk-... uv run --extra dev pytest -m redteam -q
```

Sends real adversarial probes through a live crew run:
- A corpus document with a planted prompt-injection comment must not cause scope escape.
- A jailbreak topic must be blocked at `scan_input` before the model or tools see it.

Further live markers: `-m live` (orchestrator + topology smoke against local
Ollama) and per-provider markers (`openai_live`, `gemini_live`, `mistral_live`,
`watsonx_live`).

---

## Honest limitations

- **Confidential computing is a design study** — no TEE hardware here, so it
  ships as a fail-closed attestation gate plus a design doc; TEE-backed
  deployment is not claimed.
- **The sandbox tier is darwin-only** best-effort local containment (Seatbelt),
  not a certified security boundary; it fails closed on other platforms.
- **Rule of Two in `serve-mcp` stdio mode** enforces tool-side only; the
  cross-process HITL grant channel is future work, not claimed.
- **Default retrieval and baseline embedders are illustrative** —
  `MockEmbedding` and `HashingEmbedder` are deterministic, not semantic; swap
  in real embedding models for deployment.
- **cosign signing is key-based**, not keyless-OIDC; the Ed25519 attestation is
  in-toto-style, not sigstore.
- **OWASP Agentic Top-10 coverage is 6 of 8 mapped threats**, with T4 (Resource
  Overload) and T7 (Cascading) as documented gaps.
- **The governance gates are pre-flight/CI gates**, not runtime interceptors.
- **FedAvg is a simulation**, not a distributed deployment.

---

## Project history

The wave-by-wave build ledger — what was added when, with the full
capability-to-evidence tables — lives in
[docs/BUILD-NOTES.md](docs/BUILD-NOTES.md).

TDQS

B3.2/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a distinct purpose: searching the knowledge base, reading a document, and publishing a report. There is no overlap or ambiguity.

Naming Consistency4/5

All tools use snake_case and include a verb, but the pattern varies: kb_search_tool follows noun_verb_tool, while read_doc_tool and publish_report_tool follow verb_noun_tool. This minor inconsistency is still clear.

Tool Count4/5

With three tools, the set is minimal but fits a focused knowledge base and report creation workflow. It is not overly small given the scope.

Completeness3/5

The tools cover search, read, and publish, but lack update, delete, or list operations. For a basic workflow it is functional, but gaps exist for lifecycle management.

Maintenance

ActivitySlowing
ResponsivenessNo issues