Skip to content

Groups & Governance

Governance is layered: a namespace scopes an application instance; groups (and subgroups) within it hold members with roles and capabilities and own contexts. This guide covers the pieces you’ll touch most; the exhaustive list is in the admin API reference.

val ns = mero.admin.createNamespace(
CreateNamespaceRequest(applicationId = applicationId, name = "production")
)
val namespaceId = ns.namespaceId
val all = mero.admin.listNamespaces() // List<Namespace>
val one = mero.admin.getNamespace(namespaceId)
// Who this node is. `getNamespaceIdentity` is gone: core#3522 deleted the route
// that asked a namespace — "ask the node who it is" — and it 404s.
val identity = mero.admin.getNodeIdentity()

upgradePolicy is not a parameter any more: core#3485 removed the concept, and it always held "LazyOnAccess".

Invite and join:

val invite = mero.admin.createNamespaceInvitation(namespaceId)
// CreateNamespaceInvitationResult — .Single(...) or .Recursive(...)
// on the joining node, with a SignedGroupOpenInvitation:
val joined = mero.admin.joinNamespace(
namespaceId, JoinNamespaceRequest(invitation = signed)
)
// joined.groupId, joined.memberIdentity

Joining is only half the story: the group’s contexts are not initialized on your node until each is joined and state-pulled, and cross-node sync is asynchronous. Until then a context sits on the all-ones (1111…) uninitialized hash and reads come back empty. Follow the join with syncGroupContexts, retrying while the peer catches up:

repeat(6) {
val contexts = runCatching { mero.admin.syncGroupContexts(joined.groupId) }
.getOrNull().orEmpty()
if (contexts.isNotEmpty()) return@repeat
delay(2_000)
}

If it stays empty, check getPeersCount() — 0 peers means this node has no one to sync from, which is a networking problem, not an app-id mismatch.

val info = mero.admin.getGroupInfo(groupId)
val members = mero.admin.listGroupMembers(groupId).members
val contexts = mero.admin.listGroupContexts(groupId)
mero.admin.addGroupMembers(
groupId, AddGroupMembersRequest(members = listOf(GroupMemberInput(identity = id, role = "member")))
)
mero.admin.updateMemberRole(groupId, identity = id, UpdateMemberRoleRequest(role = "admin"))
mero.admin.removeGroupMembers(groupId, RemoveGroupMembersRequest(members = listOf(id)))
val subgroups = mero.admin.listSubgroups(groupId)

A member’s permissions are a u32 bitmask, carried as a Long in Kotlin. Capabilities provides the bit constants and pure helpers so you never write raw bitwise operators.

import com.calimero.mero.Capabilities
// Read the current mask (MemberCapabilities.capabilities is an Int)
val current = mero.admin.getMemberCapabilities(groupId, identity = id).capabilities
val mask = current.toLong()
// Test / add / remove bits
val canInvite = Capabilities.hasCap(mask, Capabilities.CAN_INVITE_MEMBERS)
val next = Capabilities.withCap(mask, Capabilities.MANAGE_MEMBERS)
val revoked = Capabilities.withoutCap(next, Capabilities.MANAGE_APPLICATION)
// Persist (the request takes an Int)
mero.admin.setMemberCapabilities(
groupId, identity = id, SetMemberCapabilitiesRequest(capabilities = next.toInt())
)

The bits (mirroring core’s MemberCapabilities):

Constant Bit Grants
Capabilities.CAN_CREATE_CONTEXT 1 shl 0 create contexts in the group
Capabilities.CAN_INVITE_MEMBERS 1 shl 1 invite new members
Capabilities.CAN_JOIN_OPEN_SUBGROUPS 1 shl 2 join open subgroups
Capabilities.MANAGE_MEMBERS 1 shl 3 add/remove members, change roles
Capabilities.MANAGE_APPLICATION 1 shl 4 manage the group’s application
Capabilities.CAN_CREATE_SUBGROUP 1 shl 5 create subgroups
Capabilities.CAN_DELETE_SUBGROUP 1 shl 6 delete subgroups
Capabilities.CAN_MANAGE_VISIBILITY 1 shl 7 change subgroup visibility
Capabilities.CAN_MANAGE_METADATA 1 shl 8 edit group/member/context metadata
Capabilities.CAN_AUTHOR_ON_BEHALF 1 shl 9 relay members’ writes under their warrants (core rc.83; implied by nothing, not even admin)

Group defaults and subgroup visibility:

mero.admin.setDefaultCapabilities(
groupId, SetDefaultCapabilitiesRequest(defaultCapabilities = Capabilities.CAN_CREATE_CONTEXT.toInt())
)
val defaults = mero.admin.getDefaultCapabilities(groupId) // Int
val visibility = mero.admin.getSubgroupVisibility(groupId) // String

Attach a small named key/value record to a group, member, or context:

mero.admin.setGroupMetadata(
groupId, SetGroupMetadataRequest(name = "Production", data = mapOf("region" to "eu", "tier" to "gold"))
)
val meta = mero.admin.getGroupMetadata(groupId) // MetadataRecord?
// meta?.name, meta?.data, meta?.updatedAt, meta?.updatedBy

The same pair exists for members (set/getMemberMetadata) and contexts (set/getContextMetadata). Setting a record replaces it wholesale (server limits: name ≤64 B, ≤64 entries, key ≤64 B, value ≤4096 B).

Restrict which nodes may join a group to those in an approved Trusted Execution Environment:

// Measurement form: MRTD and RTMR1–3 are all required (core rc.83), even with acceptMock.
mero.admin.setTeeAdmissionPolicy(
groupId,
SetTeeAdmissionPolicyRequest.measurements(
allowedMrtd = listOf("<mrtd-hex>"),
allowedRtmr1 = listOf("<rtmr1-hex>"),
allowedRtmr2 = listOf("<rtmr2-hex>"),
allowedRtmr3 = listOf("<rtmr3-hex>"),
allowedTcbStatuses = listOf("UpToDate"),
),
)
// Or the signed-release form: admit any release the mero-tee workflow signed.
mero.admin.setTeeAdmissionPolicy(
groupId,
SetTeeAdmissionPolicyRequest.signedRelease(
allowedProfiles = listOf("locked-read-only"),
mode = TeeAdmissionMode.RELAY, // admit as RelayTee (may author under warrants)
),
)
val policy = mero.admin.getTeeAdmissionPolicy(groupId) // enabled, signedRelease, mode

Changing the policy is a root-guarded owner op since core rc.83: run it on a node holding the owner’s account root, or pass rootProof. See root-guarded owner ops.

Nodes can attest directly; verify the quote client-side (the node’s tee/verify-quote route is gone):

val teeInfo = mero.admin.getTeeInfo()
val attest = mero.admin.teeAttest(TeeAttestRequest(nonce = nonce, includeCollateral = true))
// attest.quote, attest.collateral → your DCAP verifier

Transferring a group, repointing a namespace’s admin and the owner-only delete need the owner account’s root key (core rc.83):

val info = mero.admin.getGroupInfo(groupId) // namespaceId + ownerOpCounter go in the proof
mero.admin.transferOwnership(groupId, TransferOwnershipRequest(newOwner = adminAccount))
mero.admin.ownerDeleteGroup(groupId) // group must hold no contexts

Omit rootProof on a node that holds the root; it signs for itself. A 409 means the counter moved — re-read and re-sign.

mero.admin.upgradeGroup(groupId, UpgradeGroupRequest(targetApplicationId = newAppId, cascade = true))
val status = mero.admin.getGroupUpgradeStatus(groupId)
val migration = mero.admin.getMigrationStatus(namespaceId)
// migration.rollup.allMigrated == true when done