aggrete 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aggrete-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,315 @@
1
+ Metadata-Version: 2.4
2
+ Name: aggrete
3
+ Version: 0.1.0
4
+ Summary: An MCP proxy that enforces your code of conduct across connectors. Permission is not need-to-know.
5
+ Author: Christian Johannsen
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://aggrete.com
8
+ Project-URL: Repository, https://github.com/cjohannsen81/aggrete
9
+ Keywords: mcp,model-context-protocol,policy,proxy,gateway,security,aggregation
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Security
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: mcp>=2.0
18
+ Requires-Dist: pyyaml>=6
19
+ Requires-Dist: pyjwt[crypto]>=2.8
20
+ Requires-Dist: starlette>=0.40
21
+ Requires-Dist: uvicorn>=0.30
22
+ Requires-Dist: python-multipart>=0.0.9
23
+ Provides-Extra: redis
24
+ Requires-Dist: redis>=5; extra == "redis"
25
+ Provides-Extra: ingest
26
+ Requires-Dist: anthropic>=1.0; extra == "ingest"
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest; extra == "dev"
29
+ Requires-Dist: redis>=5; extra == "dev"
30
+ Requires-Dist: anthropic>=1.0; extra == "dev"
31
+
32
+ # Aggrete
33
+
34
+ The open-source proxy. Product site: https://aggrete.com. This repo is the proxy and nothing else: engine, accumulator, ingest CLI, Helm chart.
35
+
36
+ An MCP proxy that enforces a **code of conduct document** across connectors, with
37
+ state that accumulates per user.
38
+
39
+ Every MCP gateway on the market authorizes tool calls and logs them. None of them
40
+ answer the question that actually matters once an assistant can reach Glean,
41
+ Salesforce, Slack and Drive at once: *is this call, combined with everything this
42
+ person has already pulled today, something the code of conduct forbids?*
43
+
44
+ Four individually-authorized questions can assemble a layoff list. No guardrail
45
+ fires, because no single question was sensitive. This proxy is the missing layer.
46
+
47
+ ## Run it
48
+
49
+ ```bash
50
+ python -m venv .venv && .venv/bin/pip install mcp pyyaml pytest
51
+ .venv/bin/python -m pytest tests -q # tests generated from coc.yaml
52
+ .venv/bin/python demo/run_demo.py # the four-prompt sequence, end to end
53
+ ```
54
+
55
+ Output:
56
+
57
+ ```
58
+ turn 1 finance__headcount_plan allowed
59
+ turn 2 finance__budget_roles allowed
60
+ turn 3 hr__recent_joiners allowed (alert: COC-HR-011, 20 distinct > 8)
61
+ turn 4 ops__oncall_draft DENIED COC-HR-004
62
+ ```
63
+
64
+ Turn 4 is denied **before the upstream call**, so the on-call data is never
65
+ fetched. The two domains already held overlap on the same people, and this call
66
+ would complete the forbidden set.
67
+
68
+ ## The document is the source of truth
69
+
70
+ `coc.yaml` holds clause text written by the clause owner, its enforcement, and its
71
+ tests. Engineering owns the compiler, not the policy.
72
+
73
+ ```yaml
74
+ - rule_id: COC-HR-004
75
+ clause: >
76
+ Personnel records, compensation or budget records, and operational rosters
77
+ may not be combined to derive the employment status, performance, or
78
+ planned departure of identifiable individuals.
79
+ owner: hr-privacy@example.com
80
+ enforce:
81
+ - layer: accumulation
82
+ action: deny
83
+ type: domain_join
84
+ domains: [hr-personnel, finance-comp, ops-rota]
85
+ require_entity_overlap: true
86
+ scope: user
87
+ window: 4h
88
+ tests:
89
+ - {name: four_prompt_layoff_list, expect: deny, sequence: [...]}
90
+ ```
91
+
92
+ CI fails any rule without both an allow and a deny test. Clauses that compile to
93
+ nothing are worth finding. Those are the parts of your code of conduct that were
94
+ never enforceable.
95
+
96
+ Rule types: `domain_join`, `entity_budget`, `domain_block`, `self_comparison`,
97
+ `min_group` (a result about fewer than k people is one person's data; pay
98
+ transparency), `wall` (a domain open only to `allowed_users`, or closed to
99
+ `blocked_users`, optionally `until` a date; privilege, embargoes, investigation
100
+ subjects). `domain_join` and `domain_block` accept the same `allowed_users`,
101
+ `blocked_users`, `since`, `until` scoping (quiet periods). `self_comparison`
102
+ (the requester's own record plus colleagues' records in one domain. The
103
+ precondition for "how do I compare"; decided post-call, since the colleague
104
+ records have to be seen to be counted). Actions: `deny`, `alert`. Start everything at `alert`, tune against real traffic, then flip.
105
+
106
+ ## How it works
107
+
108
+ ```
109
+ client ──MCP──▶ proxy ──MCP──▶ hr / finance / ops connectors
110
+
111
+ ├─ pre_call deny before fetching where already decidable
112
+ ├─ post_call extract entities, record, re-evaluate, redact
113
+ └─ audit what was handed over, not just what was asked
114
+ ```
115
+
116
+ - `aggrete/policy.py`. Deterministic evaluation. No model in this path.
117
+ - `aggrete/accumulator.py`. Per-user state, TTL'd. `MemoryStore` for tests,
118
+ `RedisStore` for deployment, because state must be shared across clients.
119
+ - `aggrete/entities.py`. Pulls stable person IDs out of tool results.
120
+ - `proxy.config.yaml`. Maps tool name patterns to the domains clauses refer to.
121
+
122
+ ### Remote connectors
123
+
124
+ Upstreams are either local stdio processes (`command:`) or remote MCP
125
+ servers over streamable HTTP (`url:`). The proxy holds the credential for the
126
+ upstream; header values may reference `${ENV_VARS}` so tokens never sit in
127
+ the YAML. Because the end user never holds that token, the only path to the
128
+ connector is through the proxy.
129
+
130
+ ```yaml
131
+ upstreams:
132
+ ops:
133
+ url: https://mcp.example.com/ops/mcp
134
+ headers:
135
+ Authorization: "Bearer ${OPS_MCP_TOKEN}"
136
+ ```
137
+
138
+ `tests/test_http_upstream.py` runs the mock `ops` connector over HTTP
139
+ (`demo/mock_server.py --transport streamable-http`) behind the proxy end to end.
140
+
141
+
142
+ ## Architecture: where the proxy lives and how the pieces connect
143
+
144
+ ```
145
+ people's assistants your network your systems
146
+ (Claude, Copilot, Cursor) | |
147
+ | mcp.example.com (this proxy) | HR system (Workday)
148
+ ── HTTPS + OAuth ────────► | Starlette, streamable HTTP | ─► Finance (budget lines)
149
+ | identity from the token | ─► On-call rotations
150
+ | policy: coc.yaml | ─► Drive, Slack, CRM ...
151
+ | state: Redis (or memory) | (reachable only from the proxy)
152
+ | │ writes |
153
+ | ▼ |
154
+ | audit.jsonl ◄── read only ── Aggrete Console (live.example.com)
155
+ | coc.yaml HR / Legal / IT, behind SSO or basic auth
156
+ ```
157
+
158
+ Three rules make this safe:
159
+
160
+ 1. **Only the proxy holds connector credentials.** People sign in to the proxy
161
+ (your IdP via `mode: jwt`, or the built-in sign-in via `mode: builtin` when
162
+ you have no IdP yet); the proxy signs in to the connectors. Fence the
163
+ connectors so they accept traffic only from the proxy host.
164
+ 2. **The console never touches the connectors.** It reads two files the proxy
165
+ writes, `audit.jsonl` and `coc.yaml`, on the same host or a shared volume,
166
+ and it changes nothing the proxy enforces. Put it behind your SSO or, at
167
+ minimum, HTTP basic auth; it shows who asked what.
168
+ 3. **The assistants may only talk to the proxy.** Managed client policy
169
+ (Claude Code managed settings, Claude Enterprise connectors, Copilot and
170
+ Cursor org policies) allow-lists `https://mcp.example.com/mcp` and nothing
171
+ else.
172
+
173
+ Connecting Claude (claude.ai): Settings → Connectors → Add custom connector →
174
+ URL `https://mcp.example.com/mcp`. Claude discovers the sign-in from the
175
+ proxy's OAuth metadata, registers itself, and sends you to `/signin`. From then
176
+ on every question Claude asks on your behalf passes the policy.
177
+
178
+ Sample handbook: `samples/northwind-handbook.docx`; `coc.yaml` maps to its
179
+ clauses 7.1 to 7.11 one to one (7.4 and 7.12 are not enforceable at a data
180
+ proxy). `aggrete-ingest samples/northwind-handbook.docx` reproduces it.
181
+
182
+ ## Serving it to a whole company: streamable HTTP + OAuth
183
+
184
+ stdio is for one laptop. For everyone else, run Aggrete as a service and let
185
+ identity come from the token:
186
+
187
+ ```bash
188
+ python -m aggrete.proxy --config proxy.config.yaml --transport streamable-http --host 0.0.0.0 --port 8080
189
+ ```
190
+
191
+ HTTP mode refuses to start without an `auth:` block. In `jwt` mode it validates
192
+ bearer JWTs from your IdP (issuer, audience, expiry, signature via JWKS,
193
+ required scopes) and derives the user from the `email` claim. Configurable
194
+ with `identity_claim`. Every request without a valid token is a 401 with an
195
+ RFC 9728 `WWW-Authenticate` pointer, and the `user:` line in the config is
196
+ ignored entirely. `builtin` mode is a small OAuth server inside the proxy (dynamic client
197
+ registration, a sign-in page, passcodes from the environment) for teams with
198
+ no IdP yet. `static` mode (fixed tokens) exists for development and the
199
+ test-suite. The accumulator keys state on the token identity, so the same
200
+ person hitting Aggrete from Claude Code, Claude.ai and Cursor shares one
201
+ history. Which is the point.
202
+
203
+ Register it in a client as a remote MCP server at `https://<host>/mcp` with
204
+ the bearer token your IdP issues; keep the connectors themselves reachable
205
+ only from the Aggrete host.
206
+
207
+ ## Inside a gateway you already run
208
+
209
+ If agentgateway, IBM ContextForge, Kong or your own gateway is already the
210
+ control plane, don't add a second one. Embed Aggrete:
211
+
212
+ ```python
213
+ from aggrete.plugin import PolicyHook, AggreteMiddleware
214
+
215
+ hook = PolicyHook("coc.yaml", domains={"hr__*": "hr-personnel", "ops__*": "ops-rota"},
216
+ store=RedisStore(redis_client))
217
+ # as two calls from your plugin system
218
+ v = hook.before(user, tool) # v.allow, v.message (clause + remediation)
219
+ v = hook.after(user, tool, result_text) # records entities, re-evaluates
220
+ # or as ASGI middleware around any MCP server that answers in JSON
221
+ app = AggreteMiddleware(app, hook, identity=lambda scope: scope["state"]["user"])
222
+ ```
223
+
224
+ Identity is a callable over the request, so it composes with whatever auth
225
+ the host performs. The middleware refuses at pre-call without forwarding and
226
+ inspects JSON tools/call results for post-call recording.
227
+
228
+ ## Ways to deploy
229
+
230
+ | Who | How |
231
+ |---|---|
232
+ | One developer | `uvx aggrete --config proxy.config.yaml` (PyPI) or the `.mcp.json` in this repo |
233
+ | A team | `docker run ghcr.io/cjohannsen81/aggrete` with `/etc/aggrete` mounted, or `helm install aggrete deploy/helm/aggrete` (bundled Redis, JWT auth, Ingress) |
234
+ | 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 |
235
+ | Existing gateway | `aggrete.plugin` (above) |
236
+
237
+
238
+ ## Putting a real system behind the proxy: Google Drive
239
+
240
+ `aggrete/connectors/drive.py` is a Drive upstream the proxy runs itself. How
241
+ it is done, in the order you do it:
242
+
243
+ 1. **A service account, not a person.** In Google Cloud: enable the Drive API,
244
+ create a service account (say `aggrete-drive`), download its JSON key. The
245
+ proxy holds the key; nobody's personal Google login is involved, which is
246
+ what makes the proxy the only road.
247
+ 2. **Share the folders, read only.** In Drive, create a root folder (say
248
+ `Northwind`) with one subfolder per kind of material (`Restructuring plan`,
249
+ `Legal hold`, `Team documents`) and share the root with the service account
250
+ email as **Viewer**. Service accounts own nothing; they only see what is
251
+ shared with them.
252
+ 3. **One tool pair per folder.** The connector lists the root's subfolders and
253
+ exposes `search_<folder>` and `read_<folder>` for each, so the policy can
254
+ name folders:
255
+ ```yaml
256
+ upstreams:
257
+ drive: {command: python3, args: [-m, aggrete.connectors.drive, --credentials, /opt/aggrete/drive-sa.json, --root, Northwind]}
258
+ domains:
259
+ "drive__*_restructuring_plan": restructuring-plan # clause 7.9: embargo until announced
260
+ "drive__*_legal_hold": legal-hold # clause 7.3: never for assistants
261
+ "drive__*": drive-general
262
+ ```
263
+ 4. **Results name people.** Every file comes back with `owner_email` and
264
+ `editor_email`, so the policy's tallies and joins work on Drive results
265
+ like on HR records.
266
+ 5. **Remove the direct road.** Disable the assistant's native Drive connector
267
+ for governed accounts (Claude Enterprise: managed connectors; personal
268
+ accounts: remove it). Otherwise the assistant has two ways to Drive and the
269
+ policy only sees one.
270
+
271
+ `python -m aggrete.connectors.drive --credentials sa.json --root Northwind --list`
272
+ prints the tools that will be exposed. If the root is not shared yet the
273
+ connector still starts and exposes a single `status` tool that says what is
274
+ missing, so the proxy never fails to boot because of Drive.
275
+
276
+ ## Starting from the document you already have
277
+
278
+ `aggrete/ingest.py` turns a code-of-conduct document into a draft `coc.yaml`:
279
+
280
+ ```bash
281
+ python -m aggrete.ingest handbook.pdf --domains proxy.config.yaml -o coc.draft.yaml
282
+ ```
283
+
284
+ PDFs go to the model as native document blocks; DOCX, Markdown and text as
285
+ text. The model proposes rules in the exact `coc.yaml` schema with clause text
286
+ verbatim, every action forced to `alert`, and each rule's own tests are run
287
+ through the real `Engine` before the file is written. A draft that fails its
288
+ tests is rejected. Clauses no data proxy can enforce (tone, harassment,
289
+ expenses) are listed separately with the reason. Model set by `AGGRETE_INGEST_MODEL`. Needs `ANTHROPIC_API_KEY`
290
+ or an `ant auth login` profile.
291
+
292
+ ## Purpose binding
293
+
294
+ A permanent block gets routed around. `engine.grant_purpose(user, rule_id,
295
+ purpose, ttl_s)` opens a scoped window and stamps every retrieval made under it
296
+ with the stated purpose. Wire it to an approval workflow owned by the clause
297
+ owner named in the rule.
298
+
299
+ ## Honest limitations
300
+
301
+ - **Entity extraction is the weak point.** `entities.py` works on stable IDs and
302
+ emails. Tune `IDENTIFIER_KEYS` against your own connectors before trusting any
303
+ threshold, or Layer 4 will either never fire or fire constantly.
304
+ - **Post-call denial redacts, it does not un-fetch.** The data left the upstream.
305
+ Prefer rules that can be decided pre-call.
306
+ - **stdio identity is advisory.** The user is whoever launched the process and the
307
+ config is user-editable. Real enforcement needs streamable HTTP with OAuth, the
308
+ subject taken from the token, and IdP-level blocking of direct connector grants
309
+ so this proxy is the only path.
310
+ - **Aggregation cannot be solved, only narrowed.** A user who spaces requests
311
+ beyond the window, or paraphrases across systems this proxy doesn't front, gets
312
+ through. This raises the cost and creates the audit trail; it is not a ceiling.
313
+ - **Not a gateway.** No multi-tenancy, no token vault, no HA. For production,
314
+ port this policy engine onto agentgateway or IBM ContextForge as a plugin
315
+ rather than running it as your control plane.
@@ -0,0 +1,284 @@
1
+ # Aggrete
2
+
3
+ The open-source proxy. Product site: https://aggrete.com. This repo is the proxy and nothing else: engine, accumulator, ingest CLI, Helm chart.
4
+
5
+ An MCP proxy that enforces a **code of conduct document** across connectors, with
6
+ state that accumulates per user.
7
+
8
+ Every MCP gateway on the market authorizes tool calls and logs them. None of them
9
+ answer the question that actually matters once an assistant can reach Glean,
10
+ Salesforce, Slack and Drive at once: *is this call, combined with everything this
11
+ person has already pulled today, something the code of conduct forbids?*
12
+
13
+ Four individually-authorized questions can assemble a layoff list. No guardrail
14
+ fires, because no single question was sensitive. This proxy is the missing layer.
15
+
16
+ ## Run it
17
+
18
+ ```bash
19
+ python -m venv .venv && .venv/bin/pip install mcp pyyaml pytest
20
+ .venv/bin/python -m pytest tests -q # tests generated from coc.yaml
21
+ .venv/bin/python demo/run_demo.py # the four-prompt sequence, end to end
22
+ ```
23
+
24
+ Output:
25
+
26
+ ```
27
+ turn 1 finance__headcount_plan allowed
28
+ turn 2 finance__budget_roles allowed
29
+ turn 3 hr__recent_joiners allowed (alert: COC-HR-011, 20 distinct > 8)
30
+ turn 4 ops__oncall_draft DENIED COC-HR-004
31
+ ```
32
+
33
+ Turn 4 is denied **before the upstream call**, so the on-call data is never
34
+ fetched. The two domains already held overlap on the same people, and this call
35
+ would complete the forbidden set.
36
+
37
+ ## The document is the source of truth
38
+
39
+ `coc.yaml` holds clause text written by the clause owner, its enforcement, and its
40
+ tests. Engineering owns the compiler, not the policy.
41
+
42
+ ```yaml
43
+ - rule_id: COC-HR-004
44
+ clause: >
45
+ Personnel records, compensation or budget records, and operational rosters
46
+ may not be combined to derive the employment status, performance, or
47
+ planned departure of identifiable individuals.
48
+ owner: hr-privacy@example.com
49
+ enforce:
50
+ - layer: accumulation
51
+ action: deny
52
+ type: domain_join
53
+ domains: [hr-personnel, finance-comp, ops-rota]
54
+ require_entity_overlap: true
55
+ scope: user
56
+ window: 4h
57
+ tests:
58
+ - {name: four_prompt_layoff_list, expect: deny, sequence: [...]}
59
+ ```
60
+
61
+ CI fails any rule without both an allow and a deny test. Clauses that compile to
62
+ nothing are worth finding. Those are the parts of your code of conduct that were
63
+ never enforceable.
64
+
65
+ Rule types: `domain_join`, `entity_budget`, `domain_block`, `self_comparison`,
66
+ `min_group` (a result about fewer than k people is one person's data; pay
67
+ transparency), `wall` (a domain open only to `allowed_users`, or closed to
68
+ `blocked_users`, optionally `until` a date; privilege, embargoes, investigation
69
+ subjects). `domain_join` and `domain_block` accept the same `allowed_users`,
70
+ `blocked_users`, `since`, `until` scoping (quiet periods). `self_comparison`
71
+ (the requester's own record plus colleagues' records in one domain. The
72
+ precondition for "how do I compare"; decided post-call, since the colleague
73
+ records have to be seen to be counted). Actions: `deny`, `alert`. Start everything at `alert`, tune against real traffic, then flip.
74
+
75
+ ## How it works
76
+
77
+ ```
78
+ client ──MCP──▶ proxy ──MCP──▶ hr / finance / ops connectors
79
+
80
+ ├─ pre_call deny before fetching where already decidable
81
+ ├─ post_call extract entities, record, re-evaluate, redact
82
+ └─ audit what was handed over, not just what was asked
83
+ ```
84
+
85
+ - `aggrete/policy.py`. Deterministic evaluation. No model in this path.
86
+ - `aggrete/accumulator.py`. Per-user state, TTL'd. `MemoryStore` for tests,
87
+ `RedisStore` for deployment, because state must be shared across clients.
88
+ - `aggrete/entities.py`. Pulls stable person IDs out of tool results.
89
+ - `proxy.config.yaml`. Maps tool name patterns to the domains clauses refer to.
90
+
91
+ ### Remote connectors
92
+
93
+ Upstreams are either local stdio processes (`command:`) or remote MCP
94
+ servers over streamable HTTP (`url:`). The proxy holds the credential for the
95
+ upstream; header values may reference `${ENV_VARS}` so tokens never sit in
96
+ the YAML. Because the end user never holds that token, the only path to the
97
+ connector is through the proxy.
98
+
99
+ ```yaml
100
+ upstreams:
101
+ ops:
102
+ url: https://mcp.example.com/ops/mcp
103
+ headers:
104
+ Authorization: "Bearer ${OPS_MCP_TOKEN}"
105
+ ```
106
+
107
+ `tests/test_http_upstream.py` runs the mock `ops` connector over HTTP
108
+ (`demo/mock_server.py --transport streamable-http`) behind the proxy end to end.
109
+
110
+
111
+ ## Architecture: where the proxy lives and how the pieces connect
112
+
113
+ ```
114
+ people's assistants your network your systems
115
+ (Claude, Copilot, Cursor) | |
116
+ | mcp.example.com (this proxy) | HR system (Workday)
117
+ ── HTTPS + OAuth ────────► | Starlette, streamable HTTP | ─► Finance (budget lines)
118
+ | identity from the token | ─► On-call rotations
119
+ | policy: coc.yaml | ─► Drive, Slack, CRM ...
120
+ | state: Redis (or memory) | (reachable only from the proxy)
121
+ | │ writes |
122
+ | ▼ |
123
+ | audit.jsonl ◄── read only ── Aggrete Console (live.example.com)
124
+ | coc.yaml HR / Legal / IT, behind SSO or basic auth
125
+ ```
126
+
127
+ Three rules make this safe:
128
+
129
+ 1. **Only the proxy holds connector credentials.** People sign in to the proxy
130
+ (your IdP via `mode: jwt`, or the built-in sign-in via `mode: builtin` when
131
+ you have no IdP yet); the proxy signs in to the connectors. Fence the
132
+ connectors so they accept traffic only from the proxy host.
133
+ 2. **The console never touches the connectors.** It reads two files the proxy
134
+ writes, `audit.jsonl` and `coc.yaml`, on the same host or a shared volume,
135
+ and it changes nothing the proxy enforces. Put it behind your SSO or, at
136
+ minimum, HTTP basic auth; it shows who asked what.
137
+ 3. **The assistants may only talk to the proxy.** Managed client policy
138
+ (Claude Code managed settings, Claude Enterprise connectors, Copilot and
139
+ Cursor org policies) allow-lists `https://mcp.example.com/mcp` and nothing
140
+ else.
141
+
142
+ Connecting Claude (claude.ai): Settings → Connectors → Add custom connector →
143
+ URL `https://mcp.example.com/mcp`. Claude discovers the sign-in from the
144
+ proxy's OAuth metadata, registers itself, and sends you to `/signin`. From then
145
+ on every question Claude asks on your behalf passes the policy.
146
+
147
+ Sample handbook: `samples/northwind-handbook.docx`; `coc.yaml` maps to its
148
+ clauses 7.1 to 7.11 one to one (7.4 and 7.12 are not enforceable at a data
149
+ proxy). `aggrete-ingest samples/northwind-handbook.docx` reproduces it.
150
+
151
+ ## Serving it to a whole company: streamable HTTP + OAuth
152
+
153
+ stdio is for one laptop. For everyone else, run Aggrete as a service and let
154
+ identity come from the token:
155
+
156
+ ```bash
157
+ python -m aggrete.proxy --config proxy.config.yaml --transport streamable-http --host 0.0.0.0 --port 8080
158
+ ```
159
+
160
+ HTTP mode refuses to start without an `auth:` block. In `jwt` mode it validates
161
+ bearer JWTs from your IdP (issuer, audience, expiry, signature via JWKS,
162
+ required scopes) and derives the user from the `email` claim. Configurable
163
+ with `identity_claim`. Every request without a valid token is a 401 with an
164
+ RFC 9728 `WWW-Authenticate` pointer, and the `user:` line in the config is
165
+ ignored entirely. `builtin` mode is a small OAuth server inside the proxy (dynamic client
166
+ registration, a sign-in page, passcodes from the environment) for teams with
167
+ no IdP yet. `static` mode (fixed tokens) exists for development and the
168
+ test-suite. The accumulator keys state on the token identity, so the same
169
+ person hitting Aggrete from Claude Code, Claude.ai and Cursor shares one
170
+ history. Which is the point.
171
+
172
+ Register it in a client as a remote MCP server at `https://<host>/mcp` with
173
+ the bearer token your IdP issues; keep the connectors themselves reachable
174
+ only from the Aggrete host.
175
+
176
+ ## Inside a gateway you already run
177
+
178
+ If agentgateway, IBM ContextForge, Kong or your own gateway is already the
179
+ control plane, don't add a second one. Embed Aggrete:
180
+
181
+ ```python
182
+ from aggrete.plugin import PolicyHook, AggreteMiddleware
183
+
184
+ hook = PolicyHook("coc.yaml", domains={"hr__*": "hr-personnel", "ops__*": "ops-rota"},
185
+ store=RedisStore(redis_client))
186
+ # as two calls from your plugin system
187
+ v = hook.before(user, tool) # v.allow, v.message (clause + remediation)
188
+ v = hook.after(user, tool, result_text) # records entities, re-evaluates
189
+ # or as ASGI middleware around any MCP server that answers in JSON
190
+ app = AggreteMiddleware(app, hook, identity=lambda scope: scope["state"]["user"])
191
+ ```
192
+
193
+ Identity is a callable over the request, so it composes with whatever auth
194
+ the host performs. The middleware refuses at pre-call without forwarding and
195
+ inspects JSON tools/call results for post-call recording.
196
+
197
+ ## Ways to deploy
198
+
199
+ | Who | How |
200
+ |---|---|
201
+ | One developer | `uvx aggrete --config proxy.config.yaml` (PyPI) or the `.mcp.json` in this repo |
202
+ | A team | `docker run ghcr.io/cjohannsen81/aggrete` with `/etc/aggrete` mounted, or `helm install aggrete deploy/helm/aggrete` (bundled Redis, JWT auth, Ingress) |
203
+ | 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 |
204
+ | Existing gateway | `aggrete.plugin` (above) |
205
+
206
+
207
+ ## Putting a real system behind the proxy: Google Drive
208
+
209
+ `aggrete/connectors/drive.py` is a Drive upstream the proxy runs itself. How
210
+ it is done, in the order you do it:
211
+
212
+ 1. **A service account, not a person.** In Google Cloud: enable the Drive API,
213
+ create a service account (say `aggrete-drive`), download its JSON key. The
214
+ proxy holds the key; nobody's personal Google login is involved, which is
215
+ what makes the proxy the only road.
216
+ 2. **Share the folders, read only.** In Drive, create a root folder (say
217
+ `Northwind`) with one subfolder per kind of material (`Restructuring plan`,
218
+ `Legal hold`, `Team documents`) and share the root with the service account
219
+ email as **Viewer**. Service accounts own nothing; they only see what is
220
+ shared with them.
221
+ 3. **One tool pair per folder.** The connector lists the root's subfolders and
222
+ exposes `search_<folder>` and `read_<folder>` for each, so the policy can
223
+ name folders:
224
+ ```yaml
225
+ upstreams:
226
+ drive: {command: python3, args: [-m, aggrete.connectors.drive, --credentials, /opt/aggrete/drive-sa.json, --root, Northwind]}
227
+ domains:
228
+ "drive__*_restructuring_plan": restructuring-plan # clause 7.9: embargo until announced
229
+ "drive__*_legal_hold": legal-hold # clause 7.3: never for assistants
230
+ "drive__*": drive-general
231
+ ```
232
+ 4. **Results name people.** Every file comes back with `owner_email` and
233
+ `editor_email`, so the policy's tallies and joins work on Drive results
234
+ like on HR records.
235
+ 5. **Remove the direct road.** Disable the assistant's native Drive connector
236
+ for governed accounts (Claude Enterprise: managed connectors; personal
237
+ accounts: remove it). Otherwise the assistant has two ways to Drive and the
238
+ policy only sees one.
239
+
240
+ `python -m aggrete.connectors.drive --credentials sa.json --root Northwind --list`
241
+ prints the tools that will be exposed. If the root is not shared yet the
242
+ connector still starts and exposes a single `status` tool that says what is
243
+ missing, so the proxy never fails to boot because of Drive.
244
+
245
+ ## Starting from the document you already have
246
+
247
+ `aggrete/ingest.py` turns a code-of-conduct document into a draft `coc.yaml`:
248
+
249
+ ```bash
250
+ python -m aggrete.ingest handbook.pdf --domains proxy.config.yaml -o coc.draft.yaml
251
+ ```
252
+
253
+ PDFs go to the model as native document blocks; DOCX, Markdown and text as
254
+ text. The model proposes rules in the exact `coc.yaml` schema with clause text
255
+ verbatim, every action forced to `alert`, and each rule's own tests are run
256
+ through the real `Engine` before the file is written. A draft that fails its
257
+ tests is rejected. Clauses no data proxy can enforce (tone, harassment,
258
+ expenses) are listed separately with the reason. Model set by `AGGRETE_INGEST_MODEL`. Needs `ANTHROPIC_API_KEY`
259
+ or an `ant auth login` profile.
260
+
261
+ ## Purpose binding
262
+
263
+ A permanent block gets routed around. `engine.grant_purpose(user, rule_id,
264
+ purpose, ttl_s)` opens a scoped window and stamps every retrieval made under it
265
+ with the stated purpose. Wire it to an approval workflow owned by the clause
266
+ owner named in the rule.
267
+
268
+ ## Honest limitations
269
+
270
+ - **Entity extraction is the weak point.** `entities.py` works on stable IDs and
271
+ emails. Tune `IDENTIFIER_KEYS` against your own connectors before trusting any
272
+ threshold, or Layer 4 will either never fire or fire constantly.
273
+ - **Post-call denial redacts, it does not un-fetch.** The data left the upstream.
274
+ Prefer rules that can be decided pre-call.
275
+ - **stdio identity is advisory.** The user is whoever launched the process and the
276
+ config is user-editable. Real enforcement needs streamable HTTP with OAuth, the
277
+ subject taken from the token, and IdP-level blocking of direct connector grants
278
+ so this proxy is the only path.
279
+ - **Aggregation cannot be solved, only narrowed.** A user who spaces requests
280
+ beyond the window, or paraphrases across systems this proxy doesn't front, gets
281
+ through. This raises the cost and creates the audit trail; it is not a ceiling.
282
+ - **Not a gateway.** No multi-tenancy, no token vault, no HA. For production,
283
+ port this policy engine onto agentgateway or IBM ContextForge as a plugin
284
+ rather than running it as your control plane.
File without changes