The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Aggrete listing page.

The open-source proxy. Product site: https://aggrete.com. This repo is the proxy and nothing else: engine, accumulator, ingest CLI, Helm chart.
An MCP proxy that enforces a code of conduct document across connectors, with state that accumulates per user.
Every MCP gateway on the market authorizes tool calls and logs them. None of them answer the question that actually matters once an assistant can reach Glean, Salesforce, Slack and Drive at once: is this call, combined with everything this person has already pulled today, something the code of conduct forbids?
Four individually-authorized questions can assemble a layoff list. No guardrail fires, because no single question was sensitive. This proxy is the missing layer.
Or clone this repo to get the demo, sample policy and Helm chart.
aggrete --demo (or docker run --rm ghcr.io/aggrete/aggrete --demo) runs the four-question walkthrough with no config, auth, or network, then drops into an interactive menu so you can try scenarios (a forbidden combination, individual pay, comparing colleagues, the prompt-injection shield, a wall, a blocked store) and watch each decision. Add the same command to an MCP client ({"command": "uvx", "args": ["aggrete", "--demo"]}) and it runs as a real, self-contained demo server: bundled mock hr/finance/ops tools governed by a bundled policy, plus the check and scenarios tools. Point it at your own servers with --config for the real thing.check tool dry-runs a proposed sequence of
calls and returns the decision, the rule, the clause and the remediation without
fetching anything, and scenarios lists things to try. Both are answered by the
proxy itself (disable with builtin_tools: false).aggrete-audit audit.jsonl (breaks are reported by line number). Optionally
forward each row to a SIEM (Splunk/Elastic/Datadog over HTTP, or syslog) as it is
written, off the hot path, with audit_forward:.redact: masks emails, SSNs, card numbers, API keys and
bearer tokens in results before they reach the model; hits are counted in the audit.per_user: true and each caller
reaches it with their own resolved credential (from a pluggable vault or
token-exchange hook), so a person's individual access is carried end to end
instead of everyone sharing one master token. The upstream sees Sam, not a
shared robot account.tool_integrity:. Deterministic, no model in the path.rate_limit:),
shared across replicas via Redis. A denial-of-wallet and abuse control.scan_inbound:), so a
leaked key never leaves through a tool call.Governing writes (egress). A tool that acts on the world (create, update,
upload, post, send, share) is classified as a write and governed as egress: any
write after a session has read untrusted content is refused (the prompt-injection
shield), and a rule can target writes only with applies: write. This is generic
across connectors, not Drive-specific. The Google Drive connector exposes governed
create_<folder> tools with --allow-write; writes are fenced to the folder like
reads. Classify your own connectors' write tools with write_tools: in the config.
See ROADMAP.md for what is shipped, in progress, and planned,
with the community requests behind each item.
It first previews the plan with the built-in check tool, then runs it for real:
Turn 4 is denied before the upstream call, so the on-call data is never
fetched. The three domains overlap on the same people, and this call would
complete the forbidden set. check reached the same verdict without fetching
anything. Call aggrete__scenarios through the proxy for more to try: individual
pay (min_group), comparing colleagues (self_comparison), the prompt-injection
shield (flow), and tools hidden behind a wall or block.
coc.yaml holds clause text written by the clause owner, its enforcement, and its
tests. Engineering owns the compiler, not the policy.
CI fails any rule without both an allow and a deny test. Clauses that compile to nothing are worth finding. Those are the parts of your code of conduct that were never enforceable.
aggrete-lint coc.yaml --config proxy.config.yaml catches the fail-open cases the
tests do not: a high-severity rule that only alerts, a wall whose until date has
passed, an enforce block missing a required field, and rules whose domains no tool
is mapped to (so the rule can never fire). It exits non-zero on errors, for CI.
Rule types: domain_join, entity_budget, domain_block, self_comparison,
min_group (a result about fewer than k people is one person's data; pay
transparency), wall (a domain open only to allowed_users, or closed to
blocked_users, optionally until a date; privilege, embargoes, investigation
subjects). domain_join and domain_block accept the same allowed_users,
blocked_users, since, until scoping (quiet periods). self_comparison
(the requester's own record plus colleagues' records in one domain. The
precondition for "how do I compare"; decided post-call, since the colleague
records have to be seen to be counted). arg_match decides a call from its
arguments, not just its type: the same tool is fine or forbidden depending on
what it is asked to do. Name tool globs in tools: and the argument conditions
that must all hold in deny_when: (operators: equals, in, regex, gt,
lt, exists, missing).
The regex operator runs your pattern against model-supplied argument values, so
keep patterns simple and anchored (avoid nested quantifiers) to sidestep
catastrophic backtracking.
The built-in check tool previews arg_match rules too: pass an object instead
of a bare tool name, e.g. {"tool": "crm__export", "args": {"scope": "all"}}, and
the dry run reports the decision without fetching anything.
Actions: deny, alert. Start everything at alert, tune against real traffic, then flip.
aggrete/policy.py. Deterministic evaluation. No model in this path.aggrete/accumulator.py. Per-user state, TTL'd. MemoryStore for tests,
RedisStore for deployment, because state must be shared across clients.aggrete/entities.py. Pulls stable person IDs out of tool results.proxy.config.yaml. Maps tool name patterns to the domains clauses refer to.Upstreams are either local stdio processes (command:) or remote MCP
servers over streamable HTTP (url:). The proxy holds the credential for the
upstream; header values may reference ${ENV_VARS} so tokens never sit in
the YAML. Because the end user never holds that token, the only path to the
connector is through the proxy.
tests/test_http_upstream.py runs the mock ops connector over HTTP
(demo/mock_server.py --transport streamable-http) behind the proxy end to end.
Three rules make this safe:
mode: jwt, or the built-in sign-in via mode: builtin when
you have no IdP yet); the proxy signs in to the connectors. Fence the
connectors so they accept traffic only from the proxy host.audit.jsonl and coc.yaml, on the same host or a shared volume,
and it changes nothing the proxy enforces. Put it behind your SSO or, at
minimum, HTTP basic auth; it shows who asked what.https://mcp.example.com/mcp and nothing
else.Connecting Claude (claude.ai): Settings → Connectors → Add custom connector →
URL https://mcp.example.com/mcp. Claude discovers the sign-in from the
proxy's OAuth metadata, registers itself, and sends you to /signin. From then
on every question Claude asks on your behalf passes the policy.
Sample handbook: samples/northwind-handbook.docx (synthetic, tailored to the
rule types); coc.yaml maps to its clauses 7.1 to 7.11 one to one (7.4 and
7.12 are not enforceable at a data proxy). aggrete-ingest samples/northwind-handbook.docx reproduces it. The samples/ directory also
has real public-domain examples (GSA/TTS code of conduct, Indiana state
employee handbook); see samples/README.md.
stdio is for one laptop. For everyone else, run Aggrete as a service and let identity come from the token:
HTTP mode refuses to start without an auth: block. In jwt mode it validates
bearer JWTs from your IdP (issuer, audience, expiry, signature via JWKS,
required scopes) and derives the user from the email claim. Configurable
with identity_claim. Every request without a valid token is a 401 with an
RFC 9728 WWW-Authenticate pointer, and the user: line in the config is
ignored entirely. builtin mode is a small OAuth server inside the proxy (dynamic client
registration, a sign-in page, passcodes from the environment) for teams with
no IdP yet. static mode (fixed tokens) exists for development and the
test-suite. The accumulator keys state on the token identity, so the same
person hitting Aggrete from Claude Code, Claude.ai and Cursor shares one
history. Which is the point.
Register it in a client as a remote MCP server at https://<host>/mcp with
the bearer token your IdP issues; keep the connectors themselves reachable
only from the Aggrete host.
By default the proxy holds one credential per upstream and every caller shares
it. Mark an upstream per_user: true and each caller instead reaches it with
their own credential, resolved per request, so the upstream sees the actual
person and their individual permissions, not a shared robot account. The proxy
still never puts the caller's own token on the wire; it resolves a separate
credential through a hook you control.
The resolved env is merged into a stdio connector's environment; headers are
merged into an HTTP upstream's request headers (the per-user value wins). With no
obo block, a per_user upstream defaults to passing the identity as
AGGRETE_ACTING_USER, so a delegation-aware connector can act as them. A per-user
upstream opens a fresh connection per call for isolation (connection pooling is a
planned optimization); shared upstreams keep the one long-lived session. Every
decision still records who the call acted as.
If agentgateway, IBM ContextForge, Kong or your own gateway is already the control plane, don't add a second one. Embed Aggrete:
Identity is a callable over the request, so it composes with whatever auth the host performs. The middleware refuses at pre-call without forwarding and inspects JSON tools/call results for post-call recording.
| Who | How |
|---|---|
| One developer | uvx aggrete --config proxy.config.yaml (PyPI) or the .mcp.json in this repo |
| A team | docker run ghcr.io/aggrete/aggrete with /etc/aggrete mounted, or helm install aggrete deploy/helm/aggrete (bundled Redis, JWT auth, Ingress) |
| A company | Helm/Docker behind your IdP, then make https://aggrete.<corp>/mcp the only MCP server your assistant policies allow (Claude Code managed settings, Claude Enterprise connectors, Copilot/Cursor org policies), with connectors network-restricted to the Aggrete hosts |
| Existing gateway | aggrete.plugin (above) |
aggrete/connectors/drive.py is a Drive upstream the proxy runs itself. How
it is done, in the order you do it:
aggrete-drive), download its JSON key. The
proxy holds the key; nobody's personal Google login is involved, which is
what makes the proxy the only road.Northwind) with one subfolder per kind of material (Restructuring plan,
Legal hold, Team documents) and share the root with the service account
email as Viewer. Service accounts own nothing; they only see what is
shared with them.search_<folder> and read_<folder> for each, so the policy can
name folders:
owner_email and
editor_email, so the policy's tallies and joins work on Drive results
like on HR records.python -m aggrete.connectors.drive --credentials sa.json --root Northwind --list
prints the tools that will be exposed. If the root is not shared yet the
connector still starts and exposes a single status tool that says what is
missing, so the proxy never fails to boot because of Drive.
Drive is the reference; the pattern is general. A connector is just an MCP server, and the proxy governs any MCP server, so putting a new system behind the proxy is: expose read tools, name write tools with a write verb, and map the tools to a policy domain.
aggrete/connectors/base.py removes the boilerplate:
c.write(...) refuses a tool name with no write verb, because a mis-named write
would slip past egress governance. Full guide with the folder-fencing pattern
and a copy-paste template: docs/CONNECTORS.md and
examples/connectors/knowledgebase_connector.py.
For teams that would rather not build and maintain their own, Aggrete for teams is where supported, certified connectors live: maintained and covered by support, with Drive shipping and Slack, GitHub, Jira, Salesforce and Workday on the roadmap. The proxy and this SDK stay Apache-2.0.
aggrete/ingest.py turns a code-of-conduct document into a draft coc.yaml:
PDFs go to the model as native document blocks; DOCX, Markdown and text as
text. The model proposes rules in the exact coc.yaml schema with clause text
verbatim, every action forced to alert, and each rule's own tests are run
through the real Engine before the file is written. A draft that fails its
tests is rejected. Clauses no data proxy can enforce (tone, harassment,
expenses) are listed separately with the reason. Model set by AGGRETE_INGEST_MODEL. Needs ANTHROPIC_API_KEY
or an ant auth login profile.
A permanent block gets routed around. engine.grant_purpose(user, rule_id, purpose, ttl_s) opens a scoped window and stamps every retrieval made under it
with the stated purpose. Wire it to an approval workflow owned by the clause
owner named in the rule.
entities.py works on stable IDs and
emails. Tune IDENTIFIER_KEYS against your own connectors before trusting any
threshold, or Layer 4 will either never fire or fire constantly.mcp-name: io.github.Aggrete/aggrete