Skip to content

Capabilities

A group member’s capabilities are stored as a single unsigned 32-bit bitmask. The SDK exports the bit constants that core assigns, plus small helpers for reading and editing a mask. These are used with the group capability methods on sdk.admin (getMemberCapabilities, setMemberCapabilities, getDefaultCapabilities, setDefaultCapabilities). The groups & governance guide shows them in a full flow.

The assigned bits (core MemberCapabilities, bits 0–9):

const CAPABILITIES = {
CAN_CREATE_CONTEXT: 1 << 0,
CAN_INVITE_MEMBERS: 1 << 1,
CAN_JOIN_OPEN_SUBGROUPS: 1 << 2,
MANAGE_MEMBERS: 1 << 3,
MANAGE_APPLICATION: 1 << 4,
CAN_CREATE_SUBGROUP: 1 << 5,
CAN_DELETE_SUBGROUP: 1 << 6,
CAN_MANAGE_VISIBILITY: 1 << 7,
CAN_MANAGE_METADATA: 1 << 8,
CAN_AUTHOR_ON_BEHALF: 1 << 9,
} as const;
type CapabilityName = keyof typeof CAPABILITIES; // 'CAN_CREATE_CONTEXT' | …
type CapabilityBit = (typeof CAPABILITIES)[CapabilityName];

The grant delegated execution runs on: it lets a member publish writes attributed to another member, under a warrant that member signed. Held by relays.

It is implied by nothing — not by membership, not by admin, and it is deliberately not propagated by the subgroup-admit cascade. So every group is authorship-closed until an admin sets this bit, and a relay without it is refused before anything executes. Granting it is the step that makes a namespace writable by keyholders who run no node:

await sdk.admin.grantAuthorship(groupId, relayAccount);

Note the subject is the relay’s account, not its signing key — which is what lets the relay rotate keys without the grant, or any warrant already issued against it, needing to be reissued.

grantAuthorship is read-modify-write, and that is the point: setMemberCapabilities replaces the mask, so adding this bit by hand means re-passing every flag the member already holds, and forgetting one revokes it silently. It is a no-op when the member already has the grant.

A namespace that keeps admitting TEE nodes would otherwise need one admin-signed op per admission — published at the moment a node is assigned and the admin is not watching. Core seeds a non-admin member’s capability row from the group’s default mask at admission, so putting the bit there is what makes an attested node land open:

await sdk.admin.openToDelegatedExecution(namespaceId);
Terminal window
meroctl --node node1 group settings set-default-capabilities <NAMESPACE_ID> \
--can-join-open-subgroups --can-author-on-behalf

Set it on the namespace (the root group): a grant resolves through the membership anchor, so one at the root reaches every Open subgroup beneath it, where contexts actually live. A Restricted subgroup is a membership boundary and needs its own.

Three limits, none of which the API can paper over:

  • Not retroactive. The mask is copied at admission, so members already in the group keep what they have — use grantAuthorship for those. Calling this at namespace creation is the only point that needs no backfill.
  • Never reaches admins. Core seeds the default for non-admin roles only, and the capability is not implied by admin, so an admin’s own node stays closed.
  • Reaches every non-admin member, not only attested TEE nodes. What that does not allow: a holder cannot forge. The write is authorized by the author’s own warrant — signed by their device key, committed to one context, method and the exact arguments, nonce checked unspent — so a holder can only spend a warrant it was deliberately handed. It does gain the arguments in cleartext and the choice of when, or whether, to publish.

All three normalize to unsigned 32-bit (>>> 0), so high bits like 1 << 31 behave correctly.

// True if `mask` has every bit of `cap` set.
hasCap(mask: number, cap: number): boolean;
// `mask` with every bit of `cap` set.
withCap(mask: number, cap: number): number;
// `mask` with every bit of `cap` cleared.
withoutCap(mask: number, cap: number): number;
import { CAPABILITIES, hasCap, withCap, withoutCap } from '@calimero-network/mero-js';
const current = await sdk.admin.getMemberCapabilities(groupId, identity);
if (!hasCap(current.capabilities, CAPABILITIES.CAN_INVITE_MEMBERS)) {
// grant invite + subgroup creation, revoke metadata management
let next = withCap(current.capabilities, CAPABILITIES.CAN_INVITE_MEMBERS);
next = withCap(next, CAPABILITIES.CAN_CREATE_SUBGROUP);
next = withoutCap(next, CAPABILITIES.CAN_MANAGE_METADATA);
await sdk.admin.setMemberCapabilities(groupId, identity, { capabilities: next });
}