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

  1. Dev requests (in the repo): prismor exempt request --reason "deploy.sh uses curl|sh". Posts {device key, repo remote, reason} → a pending PolicyExemption. The dev can only ask — they cannot relax anything locally.
  2. Admin reviews in the console (Admin → Policy → Exemptions): edits the exact relaxation overlay, sets an expiry, approves → status granted.
  3. The granted exemption is served in the signed policy bundle; the device verifies the signature and applies it (through the floor-enforcing merge).
  4. Request + grant + expiry are written to the audit log.

How it is reinforced (the trust model)

GuaranteeMechanism
Dev can't forge a relaxationExemptions 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 coreThe 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 lingerExemptions 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 visibleEvery 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 gapA 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's settings.repo_exemptions, and applies its overlay via _apply_override (floor-enforcing). Records engine.active_exemption.
  • cli.py hook-dispatch — tags telemetry extra with policy_scope (org vs repo_exemption:<id>) + repo.
  • telemetry.build_record — carries repo + policy_scope on 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 in settings.repo_exemptions; /api/policy/version — include an exemptions signature so changes propagate (mirrors managedReposSig).
  • /api/telemetry/ingest — store repo + 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.