Skip to content

OFCS conformance status ​

go-oidc-provider is regressed against the OpenID Foundation Conformance Suite (OFCS). The harness lives in conformance/ in the source repo and scaffolds nine OFCS plans; the baseline below is the run captured on 2026-11-04 against the library at commit 9e8d5153, executed end-to-end against a cmd/op-demo instance.

Personal project, not certified

This is a personal project maintained by an individual developer. No OpenID Foundation membership fee is paid and no formal OIDC certification is held. The numbers on this page are reproducible snapshots from the plan set shown below. They are not a substitute for a paid OpenID Foundation certification and should not be cited as one.

Nine plans in, one outcome per module out
Nine conformance test plans across four specification families are run against the demo OP. Every module in every plan lands in exactly one of five outcomes: passed, review, skipped, no verdict, or failed.test plans — nine of themoutcome per moduleOIDC Core 1.06 plansFAPI 2.0 Baseline1 planFAPI 2.0 Message Signing1 planFAPI-CIBA1 plancmd/op-demothe OP under testPASSEDevery condition was metREVIEWthe suite asks a humanSKIPPEDnot applicable hereNo verdictit ran and returned nothingFAILEDa condition was not met
Only the last row is a defect. The middle three are the suite declining to answer — because the module does not apply to this profile, or because the verdict is one a person has to make.

What this snapshot exercises ​

PlanWhat it coversProfile
oidcc-basic-certification-test-planAuthorization Code + PKCE, ID Token, UserInfo, refresh, discoveryOIDC Core 1.0
oidcc-config-certification-test-planThe discovery document and the shape of the published JWKSOIDC Core 1.0 — Discovery
oidcc-dynamic-certification-test-planDynamic client registration, then the code flow against the client it registeredOIDC Core 1.0 — Dynamic Registration
oidcc-formpost-basic-certification-test-planThe basic plan again with response_mode=form_postOIDC Core 1.0 — form_post
oidcc-rp-initiated-logout-certification-test-plan/end_session, id_token_hint, post_logout_redirect_uriRP-Initiated Logout 1.0
oidcc-backchannel-rp-initiated-logout-certification-test-planRP-initiated logout plus logout_token delivery to the registered back-channel URIBack-Channel Logout 1.0
fapi2-security-profile-final-test-plan+ PAR, sender-constrained access tokens (DPoP), strict alg list, redirect_uri exact matchFAPI 2.0 Security Profile Final (Baseline)
fapi2-message-signing-final-test-plan+ JAR (signed authorization request), JARM (signed authorization response)FAPI 2.0 Message Signing Final
fapi-ciba-id1-test-planClient-Initiated Backchannel Authentication (poll mode), mTLS-bound tokensFAPI-CIBA

The fapi2-security-profile-final and fapi2-message-signing-final plans are the FAPI 2.0 Security Profile Final and Message Signing Final plans of the suite. Under profile.FAPI2Baseline and profile.FAPI2MessageSigning the OP accepts only its issuer, as a single-string aud, on client assertions, and it does not rotate refresh tokens (Final §5.3.2.1). The results are a regression run against the suite, not a certification.

Latest baseline ​

201 of 259modules PASSED across nine plans — 77.6% of the run
0259 modules — every module in the run
  • PASSED20177.6%
  • REVIEW3814.7%
  • SKIPPED145.4%
  • No verdict20.8%
  • FAILED41.5%
  • WARNING00.0%

Every FAILED and no-verdict module is a reviewed release exclusion — five causes, each recorded with a rationale and an expiry.

Captured2026-11-04T19:51:48ZRepository SHA9e8d5153OFCS releaserelease-v5.3.4

Snapshot status

The bars are the raw outcome of all nine plans, run against OFCS release-v5.3.4. The JSON artifact is a local gitignored file under conformance/baselines/. make conformance-release-verify accepted the run with zero blockers: every raw failure has a reviewed, expiry-bound entry in conformance/release-exclusions.json, every no-verdict module has an entry with evidence, and every REVIEW or SKIPPED module falls under a reviewed rule. That gate result is not an OIDC certification.

Why four modules FAILED ​

Every failing and no-verdict module traces back to one of five causes. Two are standing design decisions of the OP, one is a registration behaviour the suite rejects, and two are limits of driving the suite headlessly. Each one is listed with an owner and an expiry date in conformance/release-exclusions.json.

Authorization code flow only — no implicit or hybrid response types

The dynamic certification profile also requires the id_token and token id_token response types and the implicit grant. The OP serves the authorization code flow only, as the OAuth 2.0 Security BCP recommends, so the dynamic plan's discovery check fails on the response-types and grant-types conditions. The signing-algorithm checks of the same module pass when the plan runs against an RS256 OP.

1 FAILED
Registering a signed UserInfo response algorithm is refused

oidcc-userinfo-rs256 registers a client with userinfo_signed_response_alg=RS256 and no UserInfo encryption. Dynamic registration answers invalid_client_metadata, because the OP returns the signed UserInfo shape to a request carrying Accept: application/jwt rather than to a registration value, so the module never reaches its assertions. The algorithm is not the cause: the same registration is refused with ES256, and an OP built with op.WithSigningAlgorithm(op.AlgRS256) issues RS256 UserInfo.

1 FAILED
Back-channel logout is subject-scoped, not session-scoped

The OP advertises backchannel_logout_session_supported: false and logs out by subject rather than by sid. That metadata is optional in the spec, but the certification module requires true unconditionally, and the rest of the plan aborts on the same assertion. Session-scoped sid logout is not currently enabled; subject-only logout is the fail-secure default. Back-channel logout

2 FAILED
A key rotation the harness cannot perform

oidcc-server-rotate-keys asks a human to rotate the signing keys mid-module and press Start. The library takes its keyset at op.New, so a deployment rotates by constructing a new provider — which would drop the in-memory store the plan's registered client lives in. What the OP serves is unaffected: JWKS publishes every configured key. Key rotation

1 no verdict
A request_uri host the OP does not trust

oidcc-request-uri-signed-rs256 publishes its request object at a host served with the suite's self-signed certificate. The OP installs the conformance CA on the JWKS fetcher's transport but not on the request_uri fetcher, so /authorize answers invalid_request_uri before redirecting and the suite waits for a callback that never comes. The signing algorithm is not the cause: a run against an RS256 OP fails at the same fetch.

1 no verdict

Per-plan breakdown ​

Each bar is one plan on its own scale: full width is that plan's own module count, spelled out as the fraction at the right of the row. The segments are therefore shares of that plan, never of the run — so a plan's size is read from its numbers, not from its bar length. Outcomes with a count of zero are left out of the row.

oidcc-basic-certification-test-plan30 / 35 PASSED
30 PASSED3 REVIEW2 SKIPPED
oidcc-config-certification-test-plan1 / 1 PASSED
1 PASSED
oidcc-dynamic-certification-test-plan8 / 23 PASSED
8 PASSED6 REVIEW5 SKIPPED2 no verdict2 FAILED
oidcc-formpost-basic-certification-test-plan30 / 35 PASSED
30 PASSED3 REVIEW2 SKIPPED
oidcc-rp-initiated-logout-certification-test-plan3 / 11 PASSED
3 PASSED8 REVIEW
oidcc-backchannel-rp-initiated-logout-certification-test-plan0 / 2 PASSED
2 FAILED
fapi2-security-profile-final-test-plan42 / 52 PASSED
42 PASSED9 REVIEW1 SKIPPED
fapi2-message-signing-final-test-plan55 / 66 PASSED
55 PASSED9 REVIEW2 SKIPPED
fapi-ciba-id1-test-plan32 / 34 PASSED
32 PASSED2 SKIPPED
The same snapshot as a table
PlanPASSEDREVIEWSKIPPEDWARNINGFAILEDNo verdictTotal
oidcc-basic-certification-test-plan303200035
oidcc-config-certification-test-plan1000001
oidcc-dynamic-certification-test-plan86502223
oidcc-formpost-basic-certification-test-plan303200035
oidcc-rp-initiated-logout-certification-test-plan38000011
oidcc-backchannel-rp-initiated-logout-certification-test-plan0000202
fapi2-security-profile-final-test-plan429100052
fapi2-message-signing-final-test-plan559200066
fapi-ciba-id1-test-plan320200034
Total2013814042259

What each test plan covers ​

Each OFCS test plan exercises a specific spec profile. The tables below map every plan to the library options that turn on the relevant code paths and to the doc pages where that surface is documented, so embedders can verify their own deployments expose the same configuration the suite asserts against.

oidcc-basic-certification-test-plan — OIDC Core 1.0 ​

The Basic plans drive the code flow without PKCE, so the conformance OP declares op.WithProfile(profile.OIDCCore); the OAuth 2.1 default would refuse those requests.

What it testsLibrary option to enableDoc page
Authorization Code flow (PKCE optional for confidential clients)op.WithProfile(profile.OIDCCore); PKCE itself is always supported/concepts/authorization-code-pkce
ID Token issuance + claimsenabled by default/concepts/tokens
UserInfo endpointenabled by default/concepts/tokens
Discovery (/.well-known/openid-configuration)enabled by default/concepts/discovery
JWKS publicationenabled by default/operations/jwks
Refresh tokens + rotationenabled by default; long-lived refresh requires the offline_access scope/concepts/refresh-tokens
Standard scopes (profile, email, address, phone)op.WithScope(...) once per scope/concepts/scopes-and-claims
Public / pairwise subjectsop.WithPairwiseSubject(salt) for pairwise; per-client SubjectType selects which one applies/use-cases/pairwise-subject

fapi2-security-profile-final-test-plan — FAPI 2.0 Baseline ​

What it testsLibrary option to enableDoc page
PAR (RFC 9126)op.WithProfile(profile.FAPI2Baseline) implies feature.PAR/concepts/fapi, /use-cases/fapi2-baseline
JAR (RFC 9101)profile implies feature.JAR/concepts/fapi
S256 PKCE enforcementprofile-enforced/concepts/authorization-code-pkce
iss in authorization response (RFC 9207)profile-enforced/concepts/issuer
ES256 for ID Token signingthe default; OP-issued ID Tokens are never signed with PS256, and op.New refuses op.WithSigningAlgorithm(op.AlgRS256) together with the FAPI profiles/concepts/jose-basics
Refusal of RS256 (FAPI), HS*, noneRS256 with a FAPI profile is a build-time error; HS* and none are absent from the closed alg type at internal/jose/alg.go/security/design-judgments
private_key_jwtprofile-enforced; the client assertion's aud must be the issuer, as a single string/concepts/client-types
DPoP or mTLS sender constraintop.WithFeature(feature.DPoP) or op.WithFeature(feature.MTLS) (at least one is mandatory under FAPI 2.0)/concepts/sender-constraint, /concepts/dpop, /concepts/mtls
redirect_uri exact matchprofile-enforced/concepts/redirect-uri
Refresh tokens are not rotatedprofile-enforced under profile.FAPI2Baseline and profile.FAPI2MessageSigning (FAPI 2.0 Security Profile Final §5.3.2.1); every other profile rotates and detects reuse by default/concepts/refresh-tokens

fapi2-message-signing-final-test-plan — FAPI 2.0 Message Signing ​

Message Signing layers signed authorization responses on top of Baseline. Everything the Baseline plan asserts also runs here — switch the profile constant and JARM activates automatically.

What it testsLibrary option to enableDoc page
Everything from FAPI 2.0 Baseline (above)op.WithProfile(profile.FAPI2MessageSigning)(as above)
Signed authorization response (JARM)profile implies feature.JARM/concepts/fapi (JARM section)
Signed ID Token in token responseprofile-enforced/concepts/tokens
Request object signing (PS256 / ES256)profile-enforced/concepts/fapi

fapi-ciba-id1-test-plan — FAPI-CIBA (Client-Initiated Backchannel Authentication) ​

The CIBA plan exercises the OpenID Connect Client-Initiated Backchannel Authentication grant: an authentication request initiated by the client, completed asynchronously on the user's authentication device (push notification, IVR, etc.), and consumed back via a polling token request. The OP runs in poll mode. The historical FAPI-CIBA-ID1 plan uses mTLS certificate-bound access tokens; the demo explicitly enables feature.MTLS to match its tls_client_certificate_bound_access_tokens metadata. The library's general profile.FAPICIBA preset also permits DPoP and defaults to it when neither binding is chosen, so that preset alone does not reproduce the ID1 plan.

What it testsLibrary option to enableDoc page
/bc-authorize endpoint + auth_req_idop.WithCIBAGrant(op.CIBAGrant{HintResolver: ...})/use-cases/ciba
Hint resolution (login_hint / id_token_hint / login_hint_token)embedder-supplied HintResolver/use-cases/ciba
Polling discipline (authorization_pending / slow_down)enabled by default; CIBAGrant.PollInterval overrides advertised interval/use-cases/ciba
Poll-abuse lockout capdefault 5 strikes; CIBAGrant.MaxPollViolations raises or lowers the cap/use-cases/ciba
Sender-constrained access tokensop.WithProfile(profile.FAPICIBA) permits DPoP or mTLS, defaulting to DPoP; this OFCS demo explicitly enables feature.MTLS for the historical ID1 plan/concepts/sender-constraint, /concepts/mtls
Signed request object on /bc-authorizeop.WithFeature(feature.JAR) (auto under FAPI-CIBA)/concepts/fapi
Bound request_object iat and exp claims (FAPI-CIBA §5.2.2)profile-enforced/concepts/fapi

How REVIEW, SKIPPED, WARNING, and FAILED categorize ​

  • REVIEW — the test ran, but a person has to confirm something the harness cannot see, such as the error page the OP rendered or whether a login screen appeared again. The suite uploads a screenshot placeholder and waits in WAITING until someone marks it reviewed in the OFCS UI; the headless runner records REVIEW as-is. Not a failure.
  • SKIPPED — the test depends on something this OP does not advertise in discovery or per-client metadata, so the suite does not run it. For example, the RS256 negative tests skip because the client JWKS the suite registers for the FAPI plans holds only an ES256 key, so there is no RSA key to sign the negative case with. Not a failure.
  • WARNING — the test reached a terminal PASS but logged an advisory. The current snapshot has 0 warnings.
  • FAILED — a module did not reach the result the suite expects. In this run, four failures are reviewed release exclusions: two from the dynamic plan (the authorization-code-only response types, and the refused userinfo_signed_response_alg registration) and two from the deliberately subject-scoped back-channel logout metadata. The release verifier fails unless each one is listed and unexpired.
  • No verdict — the harness could not drive the module to a terminal result. One needs a signing-key rotation in the middle of the module; the other publishes a request object at a host whose certificate the OP's request_uri fetcher does not trust, so the OP refuses before redirecting and the suite waits for a callback that never comes. Both are reviewed release exclusions with evidence from a standalone run.
Why REVIEW modules are not auto-passed

The suite gates these modules on human judgment by design. A headless cmd/op-demo cannot honestly upload a screenshot of "this is what my user saw", and turning the gate off would misstate what was checked. The harness keeps REVIEW as it is; certification through the OpenID Foundation would have a person clear each one in the UI.

Modules currently FAILED — and why ​

Four modules across two plans. Each row is a reviewed, expiry-bound entry in conformance/release-exclusions.json; make conformance-release-verify fails the release if any of them is missing, unowned, or past its expiry.

ModulePlanWhy it fails
oidcc-discovery-endpoint-verificationoidcc-dynamicThe dynamic profile also requires the id_token and token id_token response types and the implicit grant, which the OP does not implement; the failing conditions are the response-types and grant-types checks.
oidcc-userinfo-rs256oidcc-dynamicRegistration requests userinfo_signed_response_alg=RS256 with no UserInfo encryption; dynamic client registration answers invalid_client_metadata, so the module never reaches its assertions. The algorithm is not the cause.
oidcc-backchannel-logout-discovery-endpoint-verificationoidcc-backchannel-rp-initiated-logoutThe OP advertises backchannel_logout_session_supported: false, which the spec permits and this module refuses.
oidcc-backchannel-rp-initiated-logoutoidcc-backchannel-rp-initiated-logoutCascades from the module above — the plan aborts on that assertion before this one can run.

Modules with no verdict — and why ​

Two modules the harness could not drive to a terminal result. Both are recorded with the evidence from a standalone run against a live suite.

ModulePlanWhy there is no verdict
oidcc-server-rotate-keysoidcc-dynamicThe module waits for an operator to rotate the signing keys and press Start. The keyset is fixed at op.New, and rebuilding the provider mid-plan would drop the in-memory store holding the plan's registered client. The module stays CONFIGURED until the runner's idle bound.
oidcc-request-uri-signed-rs256oidcc-dynamicThe module publishes an RS256-signed request object and waits for a successful authorization response. The OP cannot fetch the request_uri: the suite serves a self-signed certificate and the OP installs the conformance CA only on the JWKS fetcher, so /authorize answers invalid_request_uri before redirecting and the suite is left WAITING for a callback that never comes. The signing algorithm is not the cause.

Modules currently in REVIEW ​

Thirty-eight modules, all of them cases where the suite has no callback to assert on or records a review item regardless of the outcome, so it asks a person to look at the rendered screen. Where the OP refuses or re-prompts, the refusal itself is pinned by the library's own tests.

PlanModulesWhy a person has to look
oidcc-basic, oidcc-formpost-basic (3 each)oidcc-ensure-registered-redirect-uriAn unregistered redirect_uri must not be redirected to, so the outcome is an error page.
oidcc-max-age-1, oidcc-prompt-loginOnly a person can confirm the login screen appeared again instead of the session being reused.
oidcc-dynamic (6)oidcc-ensure-redirect-uri-in-authorization-request, oidcc-redirect-uri-query-added, oidcc-redirect-uri-query-mismatchSame as the first row: refused without redirecting.
oidcc-registration-logo-uri, oidcc-registration-policy-uri, oidcc-registration-tos-uriThe suite asks a person to confirm the consent screen presents the registered URIs. Whether they render depends on the interaction driver the embedder supplies.
oidcc-rp-initiated-logout (8)oidcc-rp-initiated-logout-*Each ends on a screen the OP renders itself — the logout confirmation, or the error page for a bad id_token_hint or unregistered post_logout_redirect_uri — because a refused logout must not redirect back to the RP.
fapi2-security-profile-final, fapi2-message-signing-final (9 each)the nine modules belowPAR request_uri refusals, request-object mismatches, the unsigned request without PAR and a state supplied only outside the request object are refused before redirecting, leaving an error page to confirm. The two long state / nonce modules are the exception: the OP accepts the long value and the flow succeeds, but the module records a review item either way.

The nine FAPI 2.0 modules (the same names appear in both plans, 18 in total):

  • fapi2-…-ensure-different-nonce-inside-and-outside-request-object
  • fapi2-…-ensure-different-state-inside-and-outside-request-object
  • fapi2-…-ensure-authorization-request-with-long-nonce
  • fapi2-…-ensure-authorization-request-with-long-state
  • fapi2-…-ensure-unsigned-authorization-request-without-using-par-fails
  • fapi2-…-par-attempt-reuse-request_uri
  • fapi2-…-par-attempt-to-use-expired-request_uri
  • fapi2-…-par-attempt-to-use-request_uri-for-different-client
  • fapi2-…-state-only-outside-request-object-not-used

Modules currently SKIPPED — and why ​

Fourteen modules. The suite decides to skip from what discovery and per-client metadata advertise, so a skip here records a deliberate omission, not a test that failed to run.

ModulePlanReason
fapi2-security-profile-final-ensure-signed-client-assertion-with-RS256-failsboth fapi2-* plansThese modules assert that an RS256-signed client assertion or request object is rejected. The client JWKS the suite registers for the FAPI plans holds only an ES256 key, so there is no RSA key to sign the negative case with; the OP refuses RS256 issuing under the FAPI profiles as well.
fapi2-security-profile-final-ensure-signed-request-object-with-RS256-failsfapi2-message-signing-finalSame, for the request object. (The Message Signing plan reuses the security-profile module names.)
fapi-ciba-id1-ensure-request-object-signature-algorithm-is-RS256-failsfapi-ciba-id1Same, for the CIBA client's request object.
fapi-ciba-id1-ensure-client-assertion-signature-algorithm-in-token-endpoint-request-is-RS256-failsfapi-ciba-id1Same, for the client assertion at the token endpoint.
oidcc-ensure-request-object-with-redirect-uri, oidcc-unsigned-request-object-supported-correctly-or-rejected-as-unsupportedoidcc-basic, oidcc-formpost-basic (both), oidcc-dynamic (first only)Request-object modules of the non-FAPI plans. The suite skips them based on the request-object capabilities discovery advertises, and the OP never accepts alg=none. Signed request objects run in the FAPI plans, where JAR is mandatory.
oidcc-request-uri-unsignedoidcc-dynamicSame family, for an unsigned object behind request_uri.
oidcc-idtoken-unsignedoidcc-dynamicAsks for an unsigned ID Token. The OP signs every ID Token and does not advertise none.
oidcc-registration-sector-bad, oidcc-registration-sector-urioidcc-dynamicsector_identifier_uri modules. Pairwise subjects are opt-in (op.WithPairwiseSubject) and the demo runs the dynamic plan with public subjects; pairwise is covered by the library's own tests and by examples/34-pairwise-saas.

Reproducing the baseline yourself ​

sh
git clone https://github.com/libraz/go-oidc-provider.git
cd go-oidc-provider
make conformance-up
make conformance-baseline LABEL=local-check
ls conformance/baselines/   # JSON snapshot lands here

make conformance-up generates certificates, starts the OFCS containers at https://localhost:8443, builds and starts cmd/op-demo at https://127.0.0.1:9443, and seeds the nine plans through the OFCS REST API. make conformance-baseline then drives every module and records a deterministic JSON file. make conformance-baseline-diff exits non-zero on any module that lost PASSED between two snapshots; that is the regression gate the project uses before merging security-relevant changes.

Which op.With... options each plan needs, the plan-to-flag table, and the release gate are in Reproducing the OFCS run. The baseline-diff cycle is under Capturing and diffing a baseline.

What FAPI 2.0 means in this codebase ​

op.WithProfile(profile.FAPI2Baseline) activates the configuration the two fapi2-* plans are built around. The full explanation is in FAPI 2.0; the pieces the plans assert are:

  • feature.PAR and feature.JAR, both auto-enabled by FAPI2Baseline: /oidc/par becomes routable, request / request_uri are validated as signed JWTs.
  • feature.JARM, additionally auto-enabled by FAPI2MessageSigning: authorization responses are signed JWTs.
  • Sender-constrained access tokens. The profile requires DPoP or mTLS. If the embedder enables feature.MTLS (cnf.x5t#S256), that satisfies it and the DPoP default is not added. Otherwise op.New selects feature.DPoP (cnf.jkt), so a plain op.WithProfile(profile.FAPI2Baseline) still boots with sender-constrained tokens. With DPoP active, discovery advertises dpop_signing_alg_values_supported: ES256, EdDSA, PS256.
  • Client assertions accept only the issuer as a single-string aud, and refresh tokens are not rotated (profile.FAPI2Baseline and profile.FAPI2MessageSigning).
  • A JOSE algorithm allow-list of RS256 / PS256 / ES256 / EdDSA across the codebase; HS* and none are structurally unreachable (see internal/jose/alg.go).
  • token_endpoint_auth_methods_supported narrowed to the FAPI production path (private_key_jwt), and redirect_uri exact-string match.
  • Per-client RequestObjectSigningAlg / TokenEndpointAuthSigningAlg pin each FAPI client to PS256 (or ES256 / EdDSA); discovery still advertises the codebase-wide list.

If you set options that conflict with the profile after WithProfile, op.New(...) returns a build-time error instead of letting a partial-FAPI configuration reach production.

Where the harness lives ​

PathWhat it is
conformance/README.mdOperator runbook
conformance/plans/*.jsonPlan templates (server / client / resource blocks)
conformance/docker-compose.ymlCurrent OFCS image pin (release-v5.3.4) + JKS truststore wiring
conformance/release-exclusions.jsonReviewed, expiry-bound exceptions the release gate accepts
scripts/conformance.shcerts / ofcs-up / op-up / seed-plans / drive / batch / baseline / baseline-diff / release-verify
tools/conformance/REST client, headless drive script, baseline and release-verify logic (Python)
conformance/baselines/*.jsonCaptured snapshots (gitignored — environment-specific)

Caveats worth naming ​

  • Plan suite version. The run behind this page used OFCS release-v5.3.4, which is also the current default pin in docker-compose.yml.
  • Headless drive. The drive script reverse-engineers the OFCS REST surface; OFCS does not document it.
  • No real RP cert. The mTLS plan slots use generated self-signed certs at conformance/certs/ so the plan can be instantiated. No real CA chain is exercised.
  • Single OP instance. Cross-instance behaviour (e.g. token introspection across two OPs sharing a store) is exercised by test/scenarios, not OFCS.

The conformance harness sits next to an in-process Spec Scenario Suite under test/scenarios/. OFCS runs end-to-end against a live OP over HTTP; the scenario suite drives the same protocol invariants in-process. Both must pass before security-relevant changes merge.