Stability checkpoint v0.7.1 — multi-issuer ALLOWLIST via MultiIssuerVerifier (ADR-0041)

Closes the v0.7 multi-issuer ticket. Comma-separated SECURED_CLAUDE_IDP_ISSUER
now becomes an ALLOWLIST ; the broker accepts JWTs from any of N IdPs by
routing each token to the correct OIDCVerifier based on its `iss` claim.

- feat(security): multi-issuer ALLOWLIST via MultiIssuerVerifier (ADR-0041) (aee6fa1)

CI :
- ✅ MR pipeline #165 green — https://gitlab.com/benoit.besson/secured-claude/-/pipelines/2491945046
- ✅ Main pipeline #166 green — https://gitlab.com/benoit.besson/secured-claude/-/pipelines/2491956024

Local test pass :
- ✅ uv run pytest -q → 227 tests pass (was 216 in v0.7.0 ; +11 multi-issuer tests)
- ✅ Coverage 90.96 % total (gate fail_under=90 % held)
- ✅ uv run ruff check src tests → clean
- ✅ uv run ruff format --check src tests → 32 files already formatted
- ✅ uv run mypy src → strict mode clean (13 source files)
- ✅ gitleaks detect → no leaks found

Regression check vs v0.7.0 :
- ✅ Single-issuer SECURED_CLAUDE_IDP_ISSUER still returns bare OIDCVerifier (back-compat)
- ✅ OIDCVerifier untouched — wrapper class chosen over in-place refactor
- ✅ Existing _jwks_ts mutation pattern in tests still works
- ✅ All v0.6.x + v0.7.0 features still active : TTL cache + bearer + max-stale-age + mTLS + JWT validation
- ✅ docker-compose.yml still bind-mount-free
- ✅ 5 cosign-signed images still cover binary + policy bytes (carryover)

- Claude Code wrapped, every tool call gated by Cerbos PDP, audited in SQLite hash-chain log (carryover)

- ✨ Multi-issuer ALLOWLIST : token's `iss` claim must match one of N configured issuers ; routing decision is unverified-then-pinned (signature still validated against the matched issuer's actual JWKS).
- ✨ 4 deployment patterns unlock : multi-tenant SaaS (per-tenant Auth0 / Okta / Keycloak), M&A migration windows (both IdPs co-existing), hybrid cloud (AD + GitHub Actions OIDC + GCP/AWS WI), DR failover (primary + secondary IdPs).
- ✨ Fail-closed allowlist : iss missing / not in allowlist → reject + WARNING log line for visibility.
- 5 cosign-signed images cover the FULL supply chain — carryover

- ⏭ N/A — IdP shape change, no new domain features

- ✨ Hybrid-cloud-friendly : token from corporate AD, GitHub Actions, GCP Workload Identity, AWS IAM Roles Anywhere — all accepted via the same comma-separated env knob.
- Per-issuer JWKS caches independent ; first request per tenant pays one-time discovery + JWKS fetch ; subsequent requests hit per-tenant cache.

- ✨ WARNING log line on routing-misses (allowlist rejects) — operators tailing logs see attempted impersonation.

- 227 tests pass (was 216 in v0.7.0 ; +11 multi-issuer covering routing / allowlist / empty-iss / env parsing)
- Coverage 90.96 % (oidc.py 91 %, gateway.py 100 %)
- mypy strict clean ; ruff + format clean
- bandit / pip-audit / grype / trivy / gitleaks all clean (carryover)

- 8 stages green on tag pipelines : lint / test / security / build / smoke / publish / release
- 5-image multi-arch (amd64 + arm64) ; cosign keyless OIDC signing on all 5
- Main pipeline 20/20 green

- ✨ ADR-0041 added (41 ADRs total ; was 40 in v0.7.0)
- Wrapper-class pattern (MultiIssuerVerifier wraps N OIDCVerifiers) — chose over in-place refactor for back-compat + zero existing-test breakage. Both classes share `verify_token` shape so gateway doesn't care.
- Unverified-then-pinned routing : iss extracted without sig check (lookup decision), then full validation with matched verifier (trust decision). Standard PyJWT pattern.

- ⏭ N/A — broker is CLI + HTTP only

- ✨ SECURED_CLAUDE_IDP_ISSUER now accepts comma-separated values (single-value still works) — operators upgrade by adding more issuers, no other changes
- Whitespace-tolerant : `a, b` works ; trailing slashes normalised away ; empty entries silently dropped
- 0 new env vars — same SECURED_CLAUDE_IDP_ISSUER reused

- Per-issuer audience / bearer / mTLS config not supported — all wrapped verifiers share the same SECURED_CLAUDE_OIDC_AUDIENCE / SECURED_CLAUDE_IDP_BEARER_TOKEN / SECURED_CLAUDE_IDP_CLIENT_CERT_PATH. Operators with mixed-auth needs (one IdP needs bearer on JWKS, another doesn't) need v0.7+.
- Subdomain wildcards (`https://*.auth0.com`) not supported — explicit allowlist only ; future ADR if a deployment proves the wildcard pattern is needed.
- Background JWKS refresh thread (proactive re-fetch before TTL expiry) deferred to v0.7.x+.
- Prometheus counter for routing-misses / cache-drops / mTLS events deferred to v0.7.x+.
- mTLS for agent ↔ broker still v0.8+ (this rev covers broker ↔ IdP only).

- v0.7.2 — Prometheus counters for routing-misses + cache-drops + mTLS events (operational signal)
- v0.7.3 — background JWKS refresh thread (only if request-latency-during-refresh becomes a concern)
- v0.8 — agent ↔ broker mTLS (replace JWT with cert-based auth on the hook connection ; bigger architectural piece)