Admin Service · Phase 2b

JWT-aware context + file-backed approval store + TTL enforcement + audit sink BOUNDARY · additive on top of Phase 2a · 24/24 parity preserved

Base URL

1 · JWT / verified auth boundary

Resolution order: Bearer > X-PTT-* > body > query > defaults. Verification needs ALL three: PyJWT installed · POLICY_JWT_VERIFY=true · POLICY_JWT_PUBLIC_KEY or POLICY_JWKS_URL. Without them the service still decodes the payload but adds warnings: ['auth_not_verified'].

2 · Approval store

File-backed · loaded at startup from ADMIN_APPROVAL_STORE_PATH (default approval_examples.json). Read-only in Phase 2b (no create / sign / withdraw endpoints — those are Phase 2c). Row shape mirrors approval_queue_model.queue_shape.item_fields.

3 · TTL / expiry semantics · sensitive.check.2b

ttl_remaining = max(0, sla_due_at - now). Engine fail-safe: expired approvals are treated as absent (per access_policy.precedence_rules.prec-8). signed_partial counts as valid only on single-signer rows; dual-signer rows need signed_full. For a REAL policy-first / eval-second chain against the Feature Flags service, use the cross-runtime demo.

Use case presets

UC 1 · JWT present + access check Tenant admin reads own-tenant PII actor=tenant_admin · field=email · non-sensitive path Uses 2a /access/check (unchanged) · decision=allow · mask=unmasked
UC 2 · Sensitive · no approval health read without approval_refs field=health · approval_refs=[] Expect: decision=deny · any_valid_ref=false
UC 3 · Sensitive · valid approval health read with APP-260419-V200 (signed_full) approval_refs=[APP-260419-V200] Expect: decision=allow · satisfies=true
UC 4 · Sensitive · EXPIRED approval APP-260419-E400 · past sla_due_at now_iso=2026-04-19T06:00:00Z Expect: decision=deny · any_expired_ref=true · reason approval_expired
UC 5 · Audit boundary Preview access.grant envelope event_type=access.grant · subject=case CASE-002 Returns shape · sink_status=deferred · NOT delivered

4 · Audit sink boundary (preview-only)

Renders an envelope matching audit_event_model.base_shape for any (actor, action, subject) tuple. Nothing is deliveredsink_status='deferred' on every response. Stdout also logs the envelope as NDJSON so operators can tail the service log and see what would be produced.

⚠ Honest limits · deferred matrix