Feature Flags · Phase 2b 14 · JWT-aware context + file-backed override store + sensitive-flag precheck · additive on top of Phase 2a+

Phase statement ข้อกำหนดเฟส

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.

Delta from Phase 2a+ ความเปลี่ยนแปลงจาก 2a+

DimensionPhase 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

Scope vs non-scope ขอบเขต

In scope (live after this commit)

  • JWT claim expectation documented · dev Bearer resolver live · decode-only unless 3 verification preconditions all met
  • Override store schema + 8 demo rows loaded at startup · read-only HTTP inspector endpoint
  • Sensitive-eval endpoint combining in-process policy hint + optional flag eval
  • Runtime debug surface (phase-2b.html) · this planning page · catalog entries · cross-links

Non-scope (deliberately deferred)

  • Real JWT signature verification against JWKS/IdP at runtime
  • Postgres-backed override persistence + CRUD API
  • Redis cache · pubsub invalidation
  • Flag transition / flip write API (Phase 2c)
  • WORM audit sink (Kafka ptt.audit.trail)
  • Rate limiting · quota enforcement
  • Admin mutation UI / Kanban
  • Real cross-service policy call from inside this service (the cross-runtime demo owns that)

JWT boundary rationale ทำไมต้องมี JWT boundary ตอนนี้

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:

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.

Override store rationale ทำไมต้องมี override store

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):

  1. inline request.overrides.user[flag_key]
  2. inline request.overrides.tenant[flag_key]
  3. store user-row matching (flag_key, user_id) · non-expired
  4. store tenant-row matching (flag_key, tenant_id) · non-expired
  5. evaluator staged bucketing / rollout_stage_map / default

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.

Sensitive-flag gating rationale ทำไมต้องมี precheck endpoint ใหม่

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:

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.

A-owned vs B-owned boundary เส้นแบ่ง ownership

File / routeOwnerState in Phase 2b
docs/runtime/feature-flags-service/phase_2b_contract.jsonAnew · Phase 2b contract
docs/runtime/feature-flags-service/override_store_schema.jsonAnew · JSON-Schema draft-07
docs/runtime/feature-flags-service/override_examples.jsonAnew · 8 seed rows
docs/runtime/feature-flags-service/app.pyAextended append-first · 2 new endpoints + 2b health + helpers
docs/runtime/feature-flags-service/phase-2b.htmlAnew · debug surface
docs/runtime/feature-flags-service/http_contract.jsonAunchanged · 2a+ contract stable
docs/runtime/feature-flags/*Aunchanged · registry + evaluator + Phase 2a debug
docs/runtime/admin-control-plane-service/policy_contract.jsonAreferenced · not modified
docs/kb/data/approval_matrix.jsonBread-only · referenced for row-sensitive-override naming
docs/kb/data/tenant_scope.jsonBread-only reference
docs/kb/data/publish_workflow.jsonBread-only reference

Sequence overview ลำดับการทำงาน

// Request arrives at FF service (Phase 2b extended)

client → FF service
    headers: Authorization: Bearer <jwt>   (optional)
    headers: X-PTT-* (optional · legacy)

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)    // unchanged Phase 2a
        → 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)       // unchanged · parity 19/19

IF endpoint == /api/flags/overrides/by-flag/{key}:
    → return all store rows for {key} with active=bool    // read-only

client ← FF service JSON envelope {ok, data, error, service}

Route rationale เหตุผลของ path

Discoverability impact ผลกระทบต่อ discoverability

Deferred matrix รายการที่ยังไม่ทำ

ItemDeferred?Current stateWhy deferredNext logical phase
JWT verification (JWKS/IdP)yesdecode-only unless 3 preconds metneeds IdP + key rotation infraPhase 2b+ infra
Fallback dev X-PTT-* headersno (intentional)live for dev ergonomicsretire when verified JWT is on in proddeployment phase
Override persistence backendyesfile-backed JSON · loaded once at startupPostgres not provisioned for this surfacePhase 2c
Redis / cache invalidationyesno cache layerinfra not wiredPhase 2b+ infra
Approval store backingyesapproval_ref display-only · not verifiedbelongs to Admin 2bAdmin Phase 2b
Sensitive WRITE operationsyesservice is read-onlydeliberate decision-service profilePhase 2c
Real flag transition APIyesnot implementedrequires rollout_model state machine impl + audit chainPhase 2c
Audit write / WORM sinkyesstdout logs onlyKafka ptt.audit.trail producer not wiredPhase 2b+ infra
Rate limitingyesnonedev-mode localhost bindingPhase 2b+ infra
Production deployment hardeningyeslocalhost onlydev-mode by designdeployment phase

Definition of Done · Phase 2b only เกณฑ์จบรอบนี้

Done when:

  • Phase 2a+ 19/19 parity still passes after the Phase 2b merge
  • Service runs with bash run.sh on port 8080 with no extra install beyond the existing requirements
  • Bearer token (even unsigned) populates claims in unverified mode; warnings: ['auth_not_verified'] is surfaced
  • Override store loads 8 rows from override_examples.json; one row is intentionally expired and flagged inactive
  • Tenant override and user override both win over default in documented precedence order
  • Sensitive-eval returns deny-needs-approval for sensitive flag + no approval_refs; allow when approval_refs are supplied
  • Every deferred item is explicit in both the runtime debug page and this planning page
  • Index Portal finds the surfaces via the target search terms
  • No B-owned file modified · no reinterpretation of B contracts
  • No new main-console / Operations Portal button

What Phase 2b is NOT สิ่งที่รอบนี้ไม่ใช่

Explicit disclaimers:

  • Not production-ready
  • Not fully secured · signature verification is conditional on operator config
  • No live approval-backed writes · all writes remain Phase 2c
  • Not a fully persistent override store · file is a scaffold, not a database
  • Not a complete rollout engine · transition API still Phase 2c
  • Not a replacement for the cross-runtime demo · sensitive-eval does NOT call the admin policy service
Planning · Feature Flags Phase 2b · v1 · 2026-04-19 · A-owned
→ Runtime debug · phase_2b_contract.json · override_examples.json · override_store_schema.json · ← Phase 1/2a/2a+ planning · Cross-runtime integration · Admin planning · IA Governance