Skip to content

feat: add capabilities to keyring endowment#3903

Open
hmalik88 wants to merge 22 commits intomainfrom
hm/add-keyring-capabilities
Open

feat: add capabilities to keyring endowment#3903
hmalik88 wants to merge 22 commits intomainfrom
hm/add-keyring-capabilities

Conversation

@hmalik88
Copy link
Copy Markdown
Contributor

@hmalik88 hmalik88 commented Mar 19, 2026

Adding capabilities to the keyring endowment as part of keyring v2 work (https://github.com/MetaMask/decisions/blob/main/decisions/core/0006-keyring-interface.md)


Note

Medium Risk
Expands the endowment:keyring permission/manifest surface area by introducing a new caveat and validation logic, which could affect permission parsing and compatibility for existing snaps if mis-specified.

Overview
Adds keyringCapabilities as an optional caveat for endowment:keyring, allowing snap manifests/initial permissions to declare a structured capabilities object.

Updates the keyring endowment builder and mapper to emit/accept the new caveat, adds getKeyringCaveatCapabilities for retrieving it from granted permissions, and extends caveat/manifest validation via a new KeyringCapabilitiesStruct + assertIsKeyringCapabilities export (with accompanying tests and snapshot updates).

Written by Cursor Bugbot for commit 00733ae. This will update automatically on new commits. Configure here.

@hmalik88 hmalik88 changed the title feature: add capabilities to keyring endowment feat: add capabilities to keyring endowment Mar 19, 2026
@socket-security
Copy link
Copy Markdown

socket-security bot commented Mar 19, 2026

No dependency changes detected. Learn more about Socket for GitHub.

👍 No dependency changes detected in pull request

@hmalik88 hmalik88 marked this pull request as ready for review March 20, 2026 15:07
@hmalik88 hmalik88 requested a review from a team as a code owner March 20, 2026 15:07
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.56%. Comparing base (647063f) to head (a06ad51).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3903   +/-   ##
=======================================
  Coverage   98.56%   98.56%           
=======================================
  Files         426      427    +1     
  Lines       12316    12344   +28     
  Branches     1935     1939    +4     
=======================================
+ Hits        12139    12167   +28     
  Misses        177      177           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null,
validator: createGenericPermissionValidator([
{ type: SnapCaveatType.KeyringOrigin },
{ type: SnapCaveatType.KeyringCapabilities, optional: true },
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we decide on behaviour if this is not defined? Since we are making it optional

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, omission of the capabilities is an implicit indication of using keyring v1

Comment on lines +125 to +139
const caveats = [];

caveats.push({
type: SnapCaveatType.KeyringOrigin,
value: { allowedOrigins: value.allowedOrigins },
});

if (hasProperty(value, 'capabilities')) {
caveats.push({
type: SnapCaveatType.KeyringCapabilities,
value: { capabilities: value.capabilities },
});
}

return { caveats: caveats as unknown as NonEmptyArray<CaveatConstraint> };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's probably a better way of doing this that doesn't require the ugly type cast at the end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I followed the pattern Frederik suggested: #3903 (comment)

Copy link
Copy Markdown
Contributor

@GuillaumeRx GuillaumeRx Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do that to remove the need to cast to unkown :

Suggested change
const caveats = [];
caveats.push({
type: SnapCaveatType.KeyringOrigin,
value: { allowedOrigins: value.allowedOrigins },
});
if (hasProperty(value, 'capabilities')) {
caveats.push({
type: SnapCaveatType.KeyringCapabilities,
value: { capabilities: value.capabilities },
});
}
return { caveats: caveats as unknown as NonEmptyArray<CaveatConstraint> };
const caveats = [];
caveats.push({
type: SnapCaveatType.KeyringOrigin,
value,
});
if (value.capabilities) {
caveats.push({
type: SnapCaveatType.KeyringCapabilities,
value,
});
}
return { caveats: caveats as NonEmptyArray<CaveatConstraint> };

Also I've noticed in Frederik's example that the value is extracted so I don't know if we should do that instead :

Suggested change
const caveats = [];
caveats.push({
type: SnapCaveatType.KeyringOrigin,
value: { allowedOrigins: value.allowedOrigins },
});
if (hasProperty(value, 'capabilities')) {
caveats.push({
type: SnapCaveatType.KeyringCapabilities,
value: { capabilities: value.capabilities },
});
}
return { caveats: caveats as unknown as NonEmptyArray<CaveatConstraint> };
const caveats = [];
caveats.push({
type: SnapCaveatType.KeyringOrigin,
value: value.allowedOrigins,
});
if (value.capabilities) {
caveats.push({
type: SnapCaveatType.KeyringCapabilities,
value: value.capabilities,
});
}
return { caveats: caveats as NonEmptyArray<CaveatConstraint> };

Both works TBH, I'm just wondering why there's a difference between the two

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second one seems to be the standard btw

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

caveats.push({
type: SnapCaveatType.KeyringCapabilities,
value: value.capabilities,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caveat values missing required wrapper object keys

High Severity

getKeyringCaveatMapper stores unwrapped values for both caveats. For KeyringOrigin, it sets value: value.allowedOrigins (a bare array) instead of value: { allowedOrigins: value.allowedOrigins }. For KeyringCapabilities, it sets value: value.capabilities (the inner object) instead of value: { capabilities: value.capabilities }. This causes validateCaveatOrigins to reject the value (arrays fail isPlainObject) and assertIsKeyringCapabilities to reject the value (missing the capabilities key expected by KeyringCapabilitiesStruct). The tests confirm the expected shape includes the wrapper keys, and the getters (getKeyringCaveatOrigins, getKeyringCaveatCapabilities) return caveat.value typed with those wrapper shapes.

Fix in Cursor Fix in Web

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.

3 participants