fix(rebuild): store membership credentials at the VTA, and query the vault legally - #251
Merged
Conversation
…vault legally
Membership recovery restored nothing. Running it against a live VTA found
three defects, all in assumptions about the credential vault's contract
rather than in the code — which is why the unit tests, which feed the plan
a listing of full credential bodies, could not catch any of them.
1. A MembershipCredential was never stored at the VTA at all. Invitations
were pushed to the credential vault on receipt; VMCs went into the local
CommunityRecord and nowhere else. `rebuild` reconstructs a membership
FROM its credential — the right design, since the credential is what the
community signed and it names both parties — but the credential has to
be somewhere other than the config file a rebuild exists because you no
longer have.
2. `cred_vault_query({})` is refused by design. The vault is deliberately
non-enumerable: `CredentialQuery::is_empty()` exists precisely to reject
a filterless request, because running one would be a wallet enumeration.
Both `rebuild` and `context_probe` sent `{}` — and both read the refusal
as "zero credentials" and carried on, so the probe was under-reporting
occupied contexts too.
3. `query/0.1` returns body-free descriptors — id, types, issuer, purpose,
status. A membership is defined by its SUBJECT, which only the body
carries, so every descriptor would have been rejected as `NoSubject`.
Each is now fetched with `get/0.1` before verification.
Everything here goes through published Trust Tasks dispatched by
`dispatch_trust_task` — `vault/credentials/{receive,query,get}/0.1`. No
bespoke routes.
The sync runs on connect rather than at message ingest, because the
dispatch path has no VTA session. One mechanism then covers three cases: a
join that just completed, a credential received while the VTA was
unreachable, and the back-fill of every membership from before this
existed. It is idempotent — the vault keys a received credential on the
VC's own `id` — so the query pass exists only to avoid pointless writes.
The probe no longer counts credentials at all. Non-enumeration is the
point, so "how many credentials are here?" is a question the contract does
not answer; personas and sub-contexts are enough to tell a context is in
use and both are properly enumerable.
Both query filters are factored into named functions so a test can assert
they carry a filter. A future edit that drops one fails in CI rather than
in production, silently restoring nothing.
Also adds `openvtc health --recoverable`, the read-only diagnostic that
found all of this. It answers "if this machine were lost, could this
account be rebuilt?" — running the same plan the wizard's Recover option
would and printing it instead of applying it. It reports credentials held
locally alongside those held at the VTA, because without that a reader
cannot tell "no memberships" from "not stored at the VTA yet", and those
need different actions.
Verified end to end against a live VTA: stored via receive, found via
filtered query, body fetched via get, verified against the persona, and
reported as `would restore 1 persona, 1 membership`.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
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.
Why
Membership recovery restored nothing. Running #250 against a live VTA found three defects — all in assumptions about the credential vault's contract rather than in the code, which is why the unit tests could not catch them: they feed the plan a listing of full credential bodies, a fair model of the design and wrong about the service.
Two of these shipped in #248 and #250. This is a fix against
main.The three defects
1. A
MembershipCredentialwas never stored at the VTA at all. Invitations were pushed to the credential vault on receipt; VMCs went into the localCommunityRecordand nowhere else.rebuildreconstructs a membership from its credential — the right design, since the credential is what the community signed and it names both parties — but the credential has to be somewhere other than the config file a rebuild exists because you no longer have.2.
cred_vault_query({})is refused by design. The vault is deliberately non-enumerable —CredentialQuery::is_empty()exists precisely to reject a filterless request, because "running it would be a wallet enumeration". Bothrebuildandcontext_probesent{}, and both read the refusal as "zero credentials" and carried on. So the occupied-context probe was under-reporting too.3.
query/0.1returns body-free descriptors —id,types,issuerDid,purpose,status. A membership is defined by its subject, which only the body carries, so every descriptor would have been rejected asNoSubject. Each is now fetched withget/0.1before verification.Published Trust Tasks only
Every call dispatches through
dispatch_trust_task:spec/vault/credentials/receive/0.1— storespec/vault/credentials/query/0.1— find, filteredspec/vault/credentials/get/0.1— fetch a bodyNo bespoke routes.
Design notes
The sync runs on connect, not at message ingest, because the dispatch path has no VTA session. One mechanism then covers three cases: a join that just completed, a credential received while the VTA was unreachable, and the back-fill of every membership from before this existed. It's idempotent — the vault keys a received credential on the VC's own
id— so the query pass exists only to avoid pointless writes on every launch.The probe no longer counts credentials. Non-enumeration is the point, so "how many credentials are here?" is a question the contract doesn't answer. Personas and sub-contexts are enough to tell a context is in use, and both are properly enumerable.
Both query filters are factored into named functions so a test can assert they carry a filter. A future edit that drops one fails in CI rather than in production, silently restoring nothing.
openvtc health --recoverableThe read-only diagnostic that found all of this, kept because "if this machine were lost, could this account be rebuilt?" is worth being able to ask at any time. It runs the same plan the wizard's Recover option would and prints it instead of applying it.
It reports credentials held locally alongside those held at the VTA — without that a reader cannot tell "no memberships" from "not stored at the VTA yet", and those need different actions.
Verified end to end against a live VTA
Before:
After:
Stored via
receive, found via filteredquery, body fetched viaget, verified against the persona. The key mapping (select_secret_kid) was also confirmed working — 24 keys present, correctly attributed, none mis-assigned.Full gate green: fmt, clippy
-D warnings, both test configurations, andcargo doc -D warnings.