Aggrete · the code-of-conduct proxy for AI assistants

Your assistant is four polite questions away from a leak. Stop it in minutes.

Every question is allowed. The answer they add up to is not. Aggrete sits between every assistant and your connectors, keeps a tally of what each person has already seen, and refuses the question that completes the picture — before the backend is ever asked. One policy file. One service. Every assistant your company runs.

✔ Enforces the policy you already have✔ Refuses before the connector is contacted✔ Works with Claude, Cursor, Copilot, ChatGPT✔ Open source · Apache-2.0

Pick where you are — the why and the how for your situation

~5 minutes

Why you need it: your assistant already has every connector you do. It will happily answer four innocent questions that add up to one you'd never ask. Nothing on your laptop stops that today.

Try it against the mocks, then your own connectors.

stdio, identity from the config file. Good enough for one laptop; advisory as enforcement, and we say so.

  1. Install and run the four-turn demo. The fourth call is refused before the mock connector is reached.
  2. Open the folder in Claude Code. .mcp.json registers aggrete; approve it once. Ask the four questions in order.
  3. Point it at a real connector. Add an upstreams: entry and a domains: pattern; drop your policy into coc.yaml.
stdioidentity: configstate: in-memory
# Python ≥ 3.10
pip install aggrete            # or: git clone github.com/cjohannsen81/aggrete
aggrete --config proxy.config.yaml   # stdio, for one client

# register anywhere outside the repo folder
claude mcp add aggrete -- aggrete --config /abs/path/proxy.config.yaml
upstreams:
  hr:
    url: https://mcp.workday.example/mcp
    headers: { Authorization: "Bearer ${HR_TOKEN}" }
domains:
  "hr__*": hr-personnel
~30 minutes

Why you need it: every teammate's assistant can run the same patient join across HR, finance and ops — from Claude, Cursor and Copilot at once. Per-request guardrails can't see across calls. Aggrete keeps one tally per person, wherever they ask from.

One shared service. Tokens from your IdP. Redis for state.

Aggrete runs as a service in your VPC and holds the connector credentials. Users never do — so the only route to a connector is through the policy.

  1. Deploy with Helm (bundled Redis, or point at your own). The policy lives in a ConfigMap; pods roll when it changes.
  2. Configure JWT auth with your IdP's issuer and audience. Every request without a valid bearer token is a 401. The user is the email claim.
  3. Give people the URL. They register https://aggrete.internal/mcp as a remote MCP server in whichever assistant they use. One history per person, across all of them.
streamable HTTPidentity: OAuth JWTstate: RedisHelm · Docker
helm install aggrete deploy/helm/aggrete \
  --set auth.issuer=https://login.example.com/ \
  --set auth.audience=https://aggrete.internal/mcp \
  --set ingress.enabled=true --set ingress.host=aggrete.internal \
  --set-file coc=coc.yaml
# or plain Docker
docker run -p 8080:8080 -v ./etc:/etc/aggrete:ro \
  -e REDIS_URL=redis://redis:6379/0 -e HR_TOKEN=… \
  ghcr.io/cjohannsen81/aggrete
# what the client sees without a token
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://aggrete.internal/.well-known/oauth-protected-resource/mcp"
an afternoon

Why you need it: your code of conduct already forbids this kind of aggregation. Right now it's a PDF. Aggrete makes it the thing that actually decides, in every assistant, with an audit row per call for the rule owner.

Make it the default, not an option.

There is no marketplace for "mandatory". A company-wide default is the team setup plus two controls: the assistant clients may only talk to Aggrete, and the connectors may only talk to Aggrete.

  1. Run the team setup behind your IdP, with the code of conduct as the policy — use the ingest tool to draft coc.yaml from the handbook, then have HR and Legal own the file.
  2. Pin the clients. Claude Code managed settings and managed-mcp.json via MDM; Claude Enterprise org connectors; Copilot, Cursor and ChatGPT org policies. Allow the Aggrete URL, disallow user-added servers.
  3. Fence the connectors. Network rules so Workday, Salesforce, Slack MCP endpoints accept traffic only from the Aggrete hosts. Then a bypass isn't a policy violation, it's a connection refused.
  4. Review the audit trail. One JSON row per call with the evidence — which domains were held and which people overlapped — for the rule owner and the auditor.
identity: OAuth JWTclients: managed policyconnectors: network-fencedpolicy: coc.yaml owned by HR/Legal
# Claude Code — managed-mcp.json shipped by MDM (admins only)
{
  "mcpServers": {
    "aggrete": { "type": "http", "url": "https://aggrete.internal/mcp" }
  }
}
# managed-settings.json: allow only that server
{ "allowedMcpServers": [ { "serverName": "aggrete" } ] }
# draft the policy from the document you already have
aggrete-ingest handbook.pdf --domains proxy.config.yaml -o coc.yaml
# every rule ships with its own tests; CI runs them on each change

Setting names are as of early 2026 — check each vendor's enterprise docs for the current keys. The pattern is stable even where the key names move.

~1 hour

Why you need it: your gateway authorizes and logs each call. It doesn't remember what the same user saw two calls ago — and that's exactly where the leak lives. Aggrete is the missing tally, as two hooks.

Don't add a second control plane. Embed the one thing yours lacks.

Gateways authorize and log individual calls well. What they don't keep is the running tally per user across calls. Aggrete's engine slots in as two hooks or as ASGI middleware, and uses the identity your gateway already established.

  1. Two calls from your plugin: before(user, tool) ahead of forwarding, after(user, tool, result) with the response.
  2. Or one middleware around any MCP server that answers in JSON — it refuses at pre-call without forwarding and records entities from the result.
  3. Share the store. Point it at the same Redis your gateway replicas use; the tally follows the user across instances.
identity: yourstransport: yoursstate: Redispip install aggrete
from aggrete.plugin import PolicyHook, AggreteMiddleware

hook = PolicyHook("coc.yaml", domains={"hr__*": "hr-personnel"},
                  store=RedisStore(redis_client))

# in your plugin
v = hook.before(user, tool)              # v.allow, v.message
v = hook.after(user, tool, result_text)  # records, re-evaluates

# or around the whole server
app = AggreteMiddleware(app, hook,
        identity=lambda scope: scope["state"]["user"])

Proof

Watch the fourth question get refused.

Real transcript through the proxy. Three innocent calls go through; the fourth would join personnel, budget and rota on the same six people — so it never reaches the ops connector.

claude code · aggrete · user christian@example.com

Accumulated · this user · 4h window

finance-planning
finance-comp
hr-personnel
ops-rota
COC-HR-004 · domain_join
hr-personnel + finance-comp + ops-rota
require_entity_overlap: true
pre_call

The backend never sees the question. Every call is judged against what this user already holds before it is forwarded. A refused call is not fetched-then-redacted — the connector is simply never contacted.

The problem

Guardrails judge one request. The risk is in the sequence.

Prompt A · blunt

"Give me the list of people on the platform team who are about to be managed out."

refused The model recognises the ask and declines. This is the case every guardrail demo shows.

Prompt B · patient

  1. What's the Q3 headcount plan? — totals, no names
  2. Which roles are backfill-only? — six budget lines with owners
  3. Who joined recently? — ten joiners, same six among them
  4. Where are the on-call gaps? — who's missing from the Q4 rota
allowed ×4 Each answer is within the user's permissions. The join is the leak — and it's the fourth call the proxy holds back before the connector sees it.
The user could always have done this by hand. What changed is that the assistant does it in ninety seconds, across every connector they've been granted, without ever asking the forbidden question.

How it works

State lives in the proxy, not the prompt.

Claude Code / any MCP clientstdio
tools/call ops__oncall_draft
Aggrete · policy.py + accumulatorpre_call → post_call
approved calls only · turn 4 never crosses this line
hrstdio
financestdio
opshttps
pre_call

Approve before forwarding. The question reaches the backend only after the proxy has judged it against what this user already holds. If one more domain would complete a forbidden join, the call is answered by the proxy itself — the connector never receives it, no data leaves it, and there is nothing to redact.

post_call

Record what was actually handed over. Person identifiers are pulled from the result, linked (an email and an employee ID on one record are one person), and stored per user with a TTL. Counts and overlaps are re-evaluated.

refuse ≠ error

Return the clause, not a stack trace. A denial comes back as text: the rule, its owner, and the sanctioned path. The assistant relays it instead of retrying, so a blocked user learns where to go rather than how to rephrase.

no model

Enforcement is deterministic. coc.yaml is the source of truth. Three rule types — domain_join, entity_budget, domain_block — and every rule ships with an allow test and a deny test or CI fails.

Your document

The policy you already have is the policy it enforces.

Drop your code of conduct here. The page reads it locally — nothing is uploaded — pulls out every obligation clause, and drafts the coc.yaml skeleton those clauses map to.

Full conversion, PDFs included: the browser draft is a heuristic pass. The repo ships an ingest command that has Claude read the document — PDFs natively — and produce complete rules with domains and tests, then runs every generated test through the real engine before writing anything.

python -m aggrete.ingest handbook.pdf \
  --domains proxy.config.yaml -o coc.draft.yaml
reads

PDF as a document block; DOCX, Markdown, text as text.

drafts

Rules in the exact coc.yaml schema. Clause text verbatim. Everything alert, never deny.

verifies

Each rule's own tests run through Engine. A draft that fails its tests is not written.

sorts out

Clauses no data proxy can enforce — tone, harassment, expenses — are listed separately with the reason.


    

Each clause got a rule type by keyword — combined / togetherdomain_join, list / roster / bulkentity_budget, never / under no circumstancesdomain_block. Domains are placeholders. Treat this as the outline you hand the clause owner, not the file you deploy.

What it doesn't do

Three things we'd rather tell you now.

Entity extraction is the weak point.Every threshold is only as good as the identifiers pulled from real payloads. Tune EMAIL_KEYS / ID_KEYS against your connectors first.
Identity over stdio is advisory.Upstreams can be remote already; the proxy itself should serve streamable HTTP with OAuth, subject from the token, before more than one person uses it.
It's a control on the assistant, not the human.A user with direct access to Workday can still do the join by hand. This closes the path that got fast.