fix(seal): read the seal mode from the backend, not a literal - #287
Merged
Merged
Conversation
This was referenced Sep 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found by a runtime pass against a stack rebuilt from
main:SEAL_PROVIDER=localcould not produce a single seal, under any configuration.The bug
seal_drain.rsnamedSealMode::ProviderSealas a literal in the request it builds for every row.That is not a setting. Core's
SealPortstates the rule: "the mode decides whose attestation the seal is."ProviderSealsays a qualified provider holds the key and attests on the operator's behalf. Only the QTSP backend advertises it.The local development sealer signs under the operator's own self-signed certificate and advertises
OperatorSeal— correctly. So the adapter's capability check refused every request the drain built for it, exactly as it should. Each row then backed off through eight attempts and reachedexhausted.Observed on a live node:
Meanwhile
GET /vault/api/v1/sealreportedsealingConfigured: truethroughout, and nothing was logged untilexhaustedclimbed — hours later.A literal could never have been right for more than one backend.
The fix
modenow travels from the composition root, read off the backend's advertised capabilities — the same routecredentialandconformance_levelalready take, and for the same stated reason: it is a property of the provider arrangement, not of the drain loop.Two consequences worth calling out:
drains, so a node with sealing off is unaffected.SEAL_CONFORMANCE_LEVELto a level this backend supports" — wrong guidance whenever the level was not the problem. It cost me a restart chasing the wrong variable. Where the mode is the mismatch, the message now says so, and says it is not settable.Still true, and deliberate
A node running
SEAL_PROVIDER=localneedsSEAL_CONFORMANCE_LEVEL=B. The local sealer advertisesBaselineBonly, and refusing to silently satisfy a request for a level that outlives its own certificate is the documented intent. The difference is that this is now a boot error naming the variable rather than eight silent retries.Tests
Six new unit tests. The regression one is
a_backend_that_signs_under_its_own_certificate_is_asked_for_an_operator_seal;a_provider_held_backend_is_asked_for_a_provider_sealpins that the fix does not swing the other way;a_level_the_backend_cannot_reach_fails_the_boot_and_names_the_remedypins the message;a_node_that_does_not_seal_is_not_checkedpins thedrainsgate.just checkgreen — 1077 tests.check-integrationcaught the feature-gatedseal_outboxsuite callingdrain_onceat the old arity, which is precisely what that step exists for; those call sites now passProviderSeal, matching the QTSP mock they drive.End-to-end verification against a live node is in the PR thread.