Skip to content

#4088: CredentialSelector extension point in PD matcher#4098

Open
stevenvegt wants to merge 4 commits intofeature/4067-credential-selection-dcqlfrom
feature/4088-credential-selector
Open

#4088: CredentialSelector extension point in PD matcher#4098
stevenvegt wants to merge 4 commits intofeature/4067-credential-selection-dcqlfrom
feature/4088-credential-selector

Conversation

@stevenvegt
Copy link
Member

@stevenvegt stevenvegt commented Mar 24, 2026

Summary

  • Introduces CredentialSelector function type in vcr/pe for pluggable credential selection
  • Extracts FirstMatchSelector as the default (preserves existing behavior)
  • Refactors matchConstraints to collect all matching VCs per input descriptor, then call the selector
  • Adds MatchWithSelector to PresentationDefinitionMatch delegates to it with FirstMatchSelector
  • Adds SetCredentialSelector to PresentationSubmissionBuilderBuild uses it when set
  • Adds ErrMultipleCredentials error for selectors that enforce strict matching
  • Documents the CredentialSelector contract (nil vs error return semantics)
  • All existing tests pass unchanged — pure refactor with additive API

Closes #4088
Part of #4067

Usage example

// Define a custom selector that picks the credential with a specific patient ID
mySelector := func(descriptor pe.InputDescriptor, candidates []vc.VerifiableCredential) (*vc.VerifiableCredential, error) {
    if len(candidates) == 0 {
        return nil, pe.ErrNoCredentials
    }
    if len(candidates) > 1 {
        return nil, pe.ErrMultipleCredentials
    }
    return &candidates[0], nil
}

// Use it with the builder
builder := presentationDefinition.PresentationSubmissionBuilder()
builder.AddWallet(holderDID, walletCredentials)
builder.SetCredentialSelector(mySelector)

submission, signInstruction, err := builder.Build("ldp_vp")

Without SetCredentialSelector, the builder uses FirstMatchSelector which picks the first matching credential per input descriptor — identical to the previous behavior.

Test plan

  • Custom selector picks non-first credential
  • Default behavior preserved (first match without selector)
  • Selector receives all matching candidates
  • Selector error propagates through Build
  • All existing PD matching tests pass
  • All existing PresentationSubmissionBuilder tests pass

stevenvegt and others added 3 commits March 24, 2026 09:12
Introduces CredentialSelector function type and FirstMatchSelector.
Refactors matchConstraints to collect all matching VCs per input
descriptor, then call the selector to pick one. Match delegates to
MatchWithSelector with FirstMatchSelector. No behavior change —
all existing tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds SetCredentialSelector method to configure a custom CredentialSelector
on the builder. Build passes it through to MatchWithSelector. Falls back
to FirstMatchSelector when not set.

Tests verify: custom selector picks non-first credential, default behavior
preserved, selector receives all matching candidates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CredentialSelector doc now describes return value semantics: when to
return nil (unfulfilled), ErrNoCredentials (no match), or
ErrMultipleCredentials (ambiguous). Adds error propagation test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@qltysh
Copy link

qltysh bot commented Mar 24, 2026

0 new issues

Tool Category Rule Count

@qltysh
Copy link

qltysh bot commented Mar 24, 2026

Qlty

Coverage Impact

⬇️ Merging this pull request will decrease total coverage on feature/4067-credential-selection-dcql by 0.01%.

Modified Files with Diff Coverage (2)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
vcr/pe/presentation_definition.go100.0%
Coverage rating: B Coverage rating: B
vcr/pe/presentation_submission.go100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@stevenvegt
Copy link
Member Author

Review finding: ErrNoCredentials soft-fail for submission requirements

During review, we identified that a custom CredentialSelector returning ErrNoCredentials would hard-fail in matchConstraints, preventing submission requirements (e.g., pick rules with min: 0) from evaluating. This breaks backward compatibility: previously, when no VC matched a descriptor, it resulted in a nil candidate that submission requirements could handle.

Example

A PD with pick rule, min: 0:

  • Match(emptyVCs) → succeeds (FirstMatchSelector returns (nil, nil), submission requirement accepts zero fulfilled descriptors)
  • MatchWithSelector(emptyVCs, strictSelector)hard-failed because ErrNoCredentials aborted matchConstraints before submission requirements could evaluate

Fix

matchConstraints now catches ErrNoCredentials from the selector and converts it to a nil selection (soft fail). This allows submission requirements to decide whether zero fulfilled descriptors is acceptable. ErrMultipleCredentials and other errors remain hard failures.

Changes

  • matchConstraints: soft-fail on ErrNoCredentials
  • Updated CredentialSelector contract docs to clarify soft vs hard failure semantics
  • New test TestPresentationDefinition_MatchWithSelector_SubmissionRequirements verifying Match and MatchWithSelector behave identically with min: 0

…bility

When a CredentialSelector returns ErrNoCredentials, treat it as a nil
selection (unfulfilled descriptor) instead of a hard failure. This allows
submission requirements with pick rules (e.g., min: 0) to evaluate
whether zero fulfilled descriptors is acceptable.

ErrMultipleCredentials and other errors remain hard failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor PD matching to return all candidates per input descriptor

2 participants