Layered Policy & Admin-Granted Exemptions
How Prismor policy applies at global / project / repo levels, and how a developer gets an exemption for a specific repo without being able to silently bypass security — with full telemetry visibility. Companion to the scoped-agent design (docs/scoped-agent.md) and docs/live-telemetry.md.
The problem
A developer working on a company repo sometimes legitimately needs a rule
relaxed for that one repo (a deploy script that uses curl | sh, a sandbox
where a strict rule gets in the way). But:
- They must not be able to turn it off themselves — that's a silent bypass.
- The org must still see it — a relaxed repo can't become a blind spot.
So: the developer requests, the admin grants, the relaxation is scoped + signed + time-boxed, and telemetry shows it.
Policy layers (precedence; the floor always wins)
floor ── destructive cmd · secret exfil · RCE · priv-esc · DoS (ALWAYS on, every layer)
│
org ── global policy, applies to all managed repos
│
project ─ applies to a Project's repos (e.g. "Client A") ← expanded to repo patterns server-side
│
repo ── a repo-scoped exemption: relaxes specific NON-floor rules for one repo
Most-specific wins for non-floor rules. The floor (_NON_OVERRIDABLE_RULE_IDS +
core block categories) survives every layer — an exemption literally cannot let
rm -rf / or secret exfil through.
The request → grant flow
- Dev requests (in the repo):
prismor exempt request --reason "deploy.sh uses curl|sh". Posts{device key, repo remote, reason}→ a pendingPolicyExemption. The dev can only ask — they cannot relax anything locally. - Admin reviews in the console (Admin → Policy → Exemptions): edits the
exact relaxation overlay, sets an expiry, approves → status
granted. - The granted exemption is served in the signed policy bundle; the device verifies the signature and applies it (through the floor-enforcing merge).
- Request + grant + expiry are written to the audit log.
How it is reinforced (the trust model)
| Guarantee | Mechanism |
|---|---|
| Dev can't forge a relaxation | Exemptions are server-authored + Ed25519-signed; the runtime applies only signed exemptions, and the managed-repo gate ignores local relaxation of a company repo. |
| Relaxation can never weaken core | The exemption overlay goes through the same _apply_override that enforces _NON_OVERRIDABLE_RULE_IDS + the core block-category clamp. Proven in tests/test_exemptions.py::test_exemption_cannot_disable_core_floor. |
| Relaxations don't linger | Exemptions are time-boxed; after expires the repo snaps back to full org policy. The device and server both drop expired ones. |
| An exempted repo stays visible | Every telemetry event from the repo is tagged policy_scope = repo_exemption:<id> + repo = host/owner/repo. The dashboard shows which repos run relaxed, by whom, until when — and a "N repos under exemption" surface so they're never forgotten. |
| No silent gap | A company repo is always one of: full org policy · managed-with-visible-exemption · personal (not company data). Never "company repo, unguarded, invisible." |
What's built (runtime — tested, no control plane needed)
prismor/warden:
policy_engine._match_exemption— for a managed workspace, finds the granted, non-expired exemption matching the repo from the signed bundle'ssettings.repo_exemptions, and applies itsoverlayvia_apply_override(floor-enforcing). Recordsengine.active_exemption.cli.pyhook-dispatch — tags telemetryextrawithpolicy_scope(orgvsrepo_exemption:<id>) +repo.telemetry.build_record— carriesrepo+policy_scopeon every record (not sensitive: only managed/company repos report, so the repo id is org context).- Tests:
tests/test_exemptions.py(5) +tests/test_workspace_scope.py(9) — relax-non-core, floor-survives, non-match, expired, telemetry-tag. 499 total green.
Control plane (to wire when the dev DB is back)
Schema (prismor-web/prisma):
PolicyExemption { orgId, repoPattern, reason, overlayYaml, status(requested|granted|revoked), requestedByDeviceId, requestedByUserId, grantedBy, expiresAt }.TelemetryEvent.repo,TelemetryEvent.policyScope(so the dashboard can show exempted repos + per-repo activity).
Endpoints:
POST /api/devices/exemptions(device-key auth) — dev requests for its repo.GET/POST /api/admin/exemptions(ADMIN+) — list pending, grant/edit/revoke, with audit. Granting writes the overlay + expiry./api/policy/resolve— include granted, non-expired exemptions insettings.repo_exemptions;/api/policy/version— include an exemptions signature so changes propagate (mirrorsmanagedReposSig)./api/telemetry/ingest— storerepo+policyScope.
UI:
- Admin → Policy → Exemptions: pending requests + active exemptions (repo, what's relaxed, requester, expiry, revoke).
- Observability: a "repos under exemption" surface + per-event repo/scope tags.
Project-level policy is expressed by expanding a Project's repos into repo patterns server-side, so the runtime only ever deals with repo patterns + exemptions — keeping the device simple.