Phase 2a+ shipped a runnable HTTP boundary for the Phase 2a evaluator, authenticated by X-PTT-* dev headers only, with inline-only override support. Phase 2b closes three honest gaps without crossing into write-path territory:
The intent is to move FF service one honest step closer to production without overclaiming any of the underlying infrastructure.
| Dimension | Phase 2a+ | Phase 2b (additive) |
|---|---|---|
| Auth context | X-PTT-* headers or query params |
Authorization: Bearer <jwt> > headers > query · decode-only unless PyJWT+flag+key all present · service.auth_source added |
| Overrides | inline only (request.overrides.user / tenant) | file-backed store layered under inline · GET /api/flags/overrides/by-flag/{key} read-only inspector · precedence documented |
| Sensitive gate | evaluator step 4 only · no service-level precheck | POST /api/flags/sensitive-eval returns sensitivity profile + local policy_hint + optional eval |
| Health | ready/degraded + registry status | NEW GET /phase-2b/health adds override_store_loaded, override_count, pyjwt_installed, jwt_verify_live, env hints |
| Root response | phase='2a+' · 4 endpoints | phase='2a+ (base) + 2b (additive)' · 6 endpoints · contract lists both files |
| Evaluator logic | evaluator.py 9-step · 19 canonical examples | unchanged · parity gate still 19/19 after Phase 2b merge |
| Hard dependencies | fastapi · uvicorn | same · PyJWT listed as optional commented line in requirements.txt · no new install needed to run |
ptt.audit.trail)
The Phase 2a+ contract already reserved auth_strategy.phase_2b_plan (RS256 · JWKS · claims shape). Actually implementing the Bearer reader now — even without verification — means:
warnings: ['auth_not_verified'] travels with every unverified response — impossible to silently mistake dev mode for prod.
Verification becomes live only when ALL three conditions hold simultaneously:
PyJWT installed · POLICY_JWT_VERIFY=true · POLICY_JWT_PUBLIC_KEY or POLICY_JWKS_URL present. Any missing precondition keeps the service in honest dev mode.
Inline overrides (request.overrides.user / tenant) were the only path in Phase 2a+. That is fine for contract testing but unusable for tenants who want persistent opt-in/opt-out without round-tripping every request with a payload. A file-backed store is the minimum-viable concrete representation that preserves the data model required by Phase 2c's Postgres work, without actually committing us to a database.
Precedence (top wins):
inline request.overrides.user[flag_key]inline request.overrides.tenant[flag_key]
Expired rows (expires_at in the past) are treated as absent but kept in the file for inspection. approval_ref on a row is display-only in this phase — the admin approval store is not consulted. The operator can read the ref and cross-check manually via the Admin policy-engine service if needed.
Importantly, the evaluator's own 9-step logic (Phase 2a) is unchanged. The service merges store-backed overrides into the request.overrides field before calling evaluator.evaluate(), so precedence rules like deps before overrides (step 3 before step 5) still hold — an override cannot activate a flag whose dependencies are unsatisfied. This is a fail-safe feature, not a bug.
The evaluator already blocks flags whose audit.last_approval_ref is null and requires_approval is true (step 4, approval_missing). But from the client's perspective, that's a terse source=approval_missing with no guidance. Phase 2b's POST /api/flags/sensitive-eval returns a richer, operator-friendly response:
sensitivity_profile — sensitive_flag, requires_approval, rollout_stage, policy_precheck_required.policy_hint — a local computation from registry fields: decision (allow / deny-needs-approval / skipped), required_approvers, approval_matrix_row, approvals_present, plus an honest_note explicitly stating this does NOT call the admin service.can_proceed_to_eval — (NOT policy_precheck_required) OR approvals_present.flag_result — populated only when can_proceed=true. Even then, the evaluator may still return approval_missing because the flag-level gate (audit.last_approval_ref) is a separate check. Surfacing both is the honest double-gate.For a REAL policy-first / eval-second chain against the admin policy service (with actual HTTP calls, trace interleaving, and deny-path short-circuiting), operators should continue using the cross-runtime integration demo. Keeping that orchestration out of this service preserves the decision-service safety profile and avoids tight coupling between the two runtimes.
| File / route | Owner | State in Phase 2b |
|---|---|---|
docs/runtime/feature-flags-service/phase_2b_contract.json | A | new · Phase 2b contract |
docs/runtime/feature-flags-service/override_store_schema.json | A | new · JSON-Schema draft-07 |
docs/runtime/feature-flags-service/override_examples.json | A | new · 8 seed rows |
docs/runtime/feature-flags-service/app.py | A | extended append-first · 2 new endpoints + 2b health + helpers |
docs/runtime/feature-flags-service/phase-2b.html | A | new · debug surface |
docs/runtime/feature-flags-service/http_contract.json | A | unchanged · 2a+ contract stable |
docs/runtime/feature-flags/* | A | unchanged · registry + evaluator + Phase 2a debug |
docs/runtime/admin-control-plane-service/policy_contract.json | A | referenced · not modified |
docs/kb/data/approval_matrix.json | B | read-only · referenced for row-sensitive-override naming |
docs/kb/data/tenant_scope.json | B | read-only reference |
docs/kb/data/publish_workflow.json | B | read-only reference |
client → FF service Bearer resolver: if can_verify (PyJWT+flag+key) → verified claims · auth_source='jwt' else if bearer present → decoded only · auth_source='jwt_unverified' + warning else → skip Context merge: bearer claims > X-PTT-* > query > defaults IF endpoint == /api/flags/sensitive-eval: sensitivity_profile ← registry.flag (sensitive_flag · requires_approval · rollout_stage) policy_hint ← LOCAL computation (NOT admin service) can_proceed_to_eval = (NOT precheck_required) OR approvals_present IF can_proceed: merge store overrides UNDER inline overrides (store_user < store_tenant < inline) → evaluator.evaluate(registry, merged_ctx) → flag_result (evaluator may still gate via step 4 approval_missing) ELSE: flag_result = null IF endpoint == /api/flags/eval (existing 2a+ path): → evaluator.evaluate(registry, ctx) IF endpoint == /api/flags/overrides/by-flag/{key}: → return all store rows for {key} with active=bool client ← FF service JSON envelope {ok, data, error, service}
/runtime/feature-flags-service/ — sibling to phase_2b_contract.json, the existing service.html, README.md, and the new phase-2b.html. The word Dashboard is not used (reserved per IA hard rule #5)./planning/feature-flags-phase-2b.html, sibling to feature-flags.html (the existing Phase 1/2a/2a+ banner).idx-planning-flags-2b at 03.14.00.00idx-runtime-flags-service-2b at 04.01.10.0004.01.10.01/02/03 (phase_2b_contract.json · override_store_schema.json · override_examples.json) and 04.01.10.04 (phase-2b.html if kept in idx)service.html footer gains a "Phase 2b →" link| Item | Deferred? | Current state | Why deferred | Next logical phase |
|---|---|---|---|---|
| JWT verification (JWKS/IdP) | yes | decode-only unless 3 preconds met | needs IdP + key rotation infra | Phase 2b+ infra |
| Fallback dev X-PTT-* headers | no (intentional) | live for dev ergonomics | retire when verified JWT is on in prod | deployment phase |
| Override persistence backend | yes | file-backed JSON · loaded once at startup | Postgres not provisioned for this surface | Phase 2c |
| Redis / cache invalidation | yes | no cache layer | infra not wired | Phase 2b+ infra |
| Approval store backing | yes | approval_ref display-only · not verified | belongs to Admin 2b | Admin Phase 2b |
| Sensitive WRITE operations | yes | service is read-only | deliberate decision-service profile | Phase 2c |
| Real flag transition API | yes | not implemented | requires rollout_model state machine impl + audit chain | Phase 2c |
| Audit write / WORM sink | yes | stdout logs only | Kafka ptt.audit.trail producer not wired | Phase 2b+ infra |
| Rate limiting | yes | none | dev-mode localhost binding | Phase 2b+ infra |
| Production deployment hardening | yes | localhost only | dev-mode by design | deployment phase |
bash run.sh on port 8080 with no extra install beyond the existing requirementswarnings: ['auth_not_verified'] is surfacedoverride_examples.json; one row is intentionally expired and flagged inactive