You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(multi-tenant-saas): model a real HR SaaS with EU vs US tenants (#54)
Replace the acme-corp/globex-financial placeholders with PeopleGraph, a
fictional HR/people-analytics SaaS whose two tenants differ by genuine data
processing agreement terms rather than by a flag.
- metzler-eu (EU employer, enforcing): GDPR guardrails enforced - declared
legal basis (Art. 6), EEA data residency (Art. 44-45), no special-category
processing (Art. 9).
- summit-us (US employer, advisory): permissive baseline; special-category
access is flagged for review but not blocked.
The same four calls (headcount, employee lookup, cross-region export,
special-category lookup) run against both tenants: the EU tenant blocks the
cross-border export and the special-category lookup, the US tenant allows the
export and only logs the special-category flag.
- New people_directory module holds employee fixtures and tool logic; the mock
server serves from it. Responses are free of scanner-tripping PII patterns.
- Add unit tests and a CI job; update the smoke matrix for the new tenants.
- Regenerate one signed TRACE record per tenant from real runs.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,7 +15,7 @@ End-to-end integration examples showing cMCP, Agent Manifest, and TRACE working
15
15
|`financial-services/`| Corporate credit risk agent: six-step assessment with CDD, exposure and IFRS 9 guardrails on the write | SEV-SNP / TDX | EU AI Act Art. 9/12, CRR Art. 395, EBA/GL/2020/06, EU AML, DORA Art. 9 |
16
16
|`healthcare/`| Clinical decision agent: EU AI Act Art. 14 HITL deny on high-risk treatment plans | SEV-SNP / TDX | EU AI Act Art. 14, HIPAA |
17
17
|`industrial-embodied-ai/`| Material-movement agent with cMCP authorization, an independent safety-controller boundary and offline-verifiable closed-session evidence | TEE / software-only development mode | OT security and industrial robot safety references |
18
-
|`multi-tenant-saas/`|Per-tenant Cedar policy bundles and enforcement modes (advisory vs enforcing) | TDX | GDPR Art. 6, customer contract SLA|
18
+
|`multi-tenant-saas/`|HR SaaS with an EU tenant (enforcing GDPR residency/Art. 9) and a US tenant (advisory) on one catalog | TDX | GDPR Art. 6/9/44, customer DPA|
19
19
|`startup-tpm/`| 15-minute quickstart on any cloud VM with Trusted Launch | TPM 2.0 | Development / staging |
20
20
21
21
Each example is fully runnable with no external dependencies: it ships a mock upstream MCP server, an agent script, an attested tool catalog, and a Cedar policy bundle, and ends by printing the signed TRACE Trust Record for the session. The `trace-output/` files in each example are captured from real runs.
Copy file name to clipboardExpand all lines: multi-tenant-saas/README.md
+90-64Lines changed: 90 additions & 64 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,51 +1,72 @@
1
1
# multi-tenant-saas: Per-Tenant Cedar Policy Isolation
2
2
3
-
End-to-end demo of a SaaS platform serving tenants with different compliance postures, each enforced by a separate Cedar policy bundle - and different *enforcement modes* - in the cMCP Runtime.
3
+
PeopleGraph is a fictional HR / people-analytics SaaS. It serves two tenants on the same platform and the same tool catalog, but each tenant has its own data processing agreement, its own Cedar policy bundle, and its own enforcement mode in the cMCP Runtime.
4
4
5
-
The same three tool calls produce different outcomes per tenant:
Each tenant has its own Cedar bundle under `tenants/<name>/policy/` and its own runtime config pointing at it. The `trace.policy.version` field in each TRACE record identifies exactly which tenant policy was enforced (`acme-corp-v1.0` vs `globex-financial-v3.2`).
26
+
Each tenant has its own Cedar bundle under `tenants/<name>/policy/` and its own runtime config pointing at it. The `trace.policy.version` field in each TRACE record identifies exactly which tenant policy was enforced.
19
27
20
-
**2. Progressive compliance posture via enforcement mode**
21
-
Acme Corp runs `enforcement_mode: advisory`: a matched forbid is logged in the audit chain and surfaced as `would_have_denied` + advice in the response metadata, but the call proceeds. Globex Financial runs `enforcing` with no catch-all permit - Cedar's default-deny blocks anything not explicitly permitted.
28
+
**2. The tenants differ by real contract terms, not just a flag**
29
+
Metzler's EU processing agreement is encoded as three GDPR guardrails: a declared legal basis for personal-data processing (Art. 6), EEA data residency (Art. 44-45), and no special-category processing through the agent (Art. 9). Summit's US agreement carries none of those; it only flags special-category access for review. The bundles genuinely differ.
22
30
23
-
**3. Structured advice on denies**
24
-
Both tenants' forbid rules carry `@annotation` metadata (GDPR article, required workflow) that the runtime returns to the caller - in `error.data.advice` for hard denies, in `_cmcp.advice` for advisory ones.
31
+
**3. Progressive posture via enforcement mode**
32
+
Metzler runs `enforcing`: a matched forbid blocks the call. Summit runs `advisory`: the same match is recorded in the audit chain and surfaced as `would_have_denied` + advice in the response metadata, but the call proceeds. The advisory record still counts the matched forbid in `gateway.call_summary.tool_calls_denied`, so an auditor sees what *would* have been blocked.
`people.data_export` and `people.employee_record_lookup` accept a `legal_basis` argument, `data_export` a `destination_region`, and the lookup an `include_special_category` flag. The Cedar guardrails act on those arguments.
67
+
68
+
---
69
+
49
70
## Run it
50
71
51
72
```bash
@@ -61,94 +82,99 @@ cd multi-tenant-saas
61
82
python server/mock_mcp_server.py
62
83
```
63
84
64
-
**Terminal 2 - runtime with Acme Corp's policy** (run from inside `multi-tenant-saas/`):
85
+
**Terminal 2 - runtime with Metzler's (EU) policy** (run from inside `multi-tenant-saas/`):
Each run ends by closing the session and printing the signed TRACE Trust Record. Compare `trace.policy.version` and `gateway.call_summary.tool_calls_denied` across the two captured examples in `trace-output/`.
132
+
Each run ends by closing the session and printing the signed TRACE Trust Record. Compare `trace.policy.version`, `trace.policy.enforcement_mode` and `gateway.call_summary.tool_calls_denied` across the two captured examples in `trace-output/`.
118
133
119
134
---
120
135
121
136
## How the policies differ
122
137
123
-
**Acme Corp** (`tenants/acme-corp/policy/allow.cedar`): a catch-all permit plus one annotated forbid - user data export without a `gdpr_justification` argument. Under advisory mode this logs and flags but does not block.
124
-
125
-
**Globex Financial** (`tenants/globex-financial/policy/allow.cedar`): no catch-all. Explicit permits per tool, gated on the workflow the agent declares via `_cmcp.workflow_id`:
138
+
**Metzler (`tenants/metzler-eu/policy/allow.cedar`, enforcing):** aggregate analytics are open; individual lookups and exports are gated on the `people-analytics` workflow; and three GDPR forbids block the call when a control fails:
The demo agent runs as `analytics-workflow`, so exports and config updates deny. Annotated forbid rules make those denies carry structured advice instead of being silent default-denies.
154
+
**Summit (`tenants/summit-us/policy/allow.cedar`, advisory):** a catch-all permit plus a single forbid that flags special-category access for review. Under advisory mode this logs and surfaces advice but does not block.
155
+
156
+
Action names follow the cMCP convention: `people.data_export` becomes `Action::"People.dataExport"` (the segment before the dot PascalCase, each underscore segment after it camelCase). Tool arguments are available under `context.arguments`; the `@annotation` values are returned in `error.data.advice` for hard denies and in `_cmcp.advice` for advisory ones.
139
157
140
158
---
141
159
142
160
## Running both tenants simultaneously
143
161
144
-
Run two runtimes on different ports (edit `listen_addr` in one config), one per tenant, against the same mock server:
162
+
This is the production topology: one attested runtime instance per tenant isolation boundary, each measuring its own policy bundle hash into its TRACE records. Run two runtimes on different ports (edit `listen_addr` in one config) against the same mock server:
This is the production topology: one attested runtime instance per tenant isolation boundary, each measuring its own policy bundle hash into its TRACE records.
169
+
---
170
+
171
+
## The tests
172
+
173
+
`tests/test_people_directory.py` checks that headcount output is aggregate-only, that special-category is a request flag rather than emitted data, and that EEA region classification and per-region currency are correct.
0 commit comments