Tenant policies — Cedar DSL with shadow rollout

Author tenant-specific permission policies in Cedar DSL, simulate against recent decisions, ship in shadow mode while TS gates remain authoritative.

Updated 2026-05-02

Most permission asks fit cleanly into Kodori's role + sensitivity tier + collection grant model. When a customer ask doesn't, the escape hatch is to author a Cedar DSL policy at `/policies`.

## Why Cedar, why shadow

Cedar is AWS's open-source policy DSL — declarative, statically analyzable, fast at evaluation time. Kodori's hand-rolled TypeScript permission gates remain authoritative. The Cedar evaluator runs in shadow alongside them; mismatches land on `policy-engine.divergence` audit events. TS stays authoritative until divergence telemetry shows zero mismatches over a soak window. Then — and only then — does Cedar flip authoritative for that policy.

The shadow rollout means a buggy policy can never lock a workspace out. The worst case is `policy-engine.divergence` events filling `/audit`, which is a clear signal for the policy author to revise.

## How divergence observation actually runs (D250)

Cedar evaluation is NOT inserted into the read-path SQL fragment that runs millions of times per day — that would double latency and produce divergence-event volume that drowns the real signal. Instead, an hourly Inngest cron (`cedar-divergence-observation`) replays the last hour of write-side audit events (document.tombstoned, document.classified, document.metadata-changed, document.legal-hold-applied, document.permission-changed, etc.) through Cedar and emits divergence events when Cedar disagrees with the TS gate the action already passed. Cap is 500 events per tenant per run.

The TS gate authoritatively allowed every replayed action — proof is the audit-chain row itself. So a divergence is exactly: "Cedar would have denied an action the TS gate let through." That's the signal a customer's ABAC policy is genuinely tighter than our default model, and worth a policy-author review.

## Status flow

- **draft** — saved, not running. Use Simulate to dry-run against the policy's own sample set; engine-construction failures surface here so invalid policies never make it to active. - **active** — shadow-evaluating against live traffic via the hourly cron. Divergence events fire on mismatches. - **archived** — retired. Kept for audit / compliance reference; not evaluating.

## The five event types

`tenant.policy-created`, `tenant.policy-activated`, `tenant.policy-archived`, `tenant.policy-simulated`, `policy-engine.divergence` — all on the hash-chained audit log alongside every other tenant action.

## The schema we evaluate against

The bundled v1 schema (Kodori namespace) declares: User / Agent / System principals; Document / Collection / Tenant resources; 9 standard actions (Read / Write / Delete / Share / ChangePermission / ChangeSensitivity / ChangeRetention / AddToHold / RemoveFromHold). Document attributes include `sensitivityLabel`, `retentionClassId`, `createdBy`, `onActiveHold`, `isTombstoned`. Collection parents are tracked via `memberOfTypes`. Future attribute additions are additive — your authored policies don't break when we extend the schema.

## Authoring + simulating

`/policies` admin list seeds a sample policy with one click so you can see the Cedar shape before authoring your own. Per-row Activate / Archive actions; `/policies/[id]` opens the editor. Simulate runs against the most recent N decisions in the same audit window — output is per-decision Allow/Deny + the difference vs the TS gate's verdict.