Skip to content

feat: add Context Groups on-chain support (storage, CRUD, membership, proxy governance)#44

Open
rtb-12 wants to merge 22 commits intomasterfrom
feat/context-management-proposal
Open

feat: add Context Groups on-chain support (storage, CRUD, membership, proxy governance)#44
rtb-12 wants to merge 22 commits intomasterfrom
feat/context-management-proposal

Conversation

@rtb-12
Copy link
Contributor

@rtb-12 rtb-12 commented Feb 19, 2026

Summary

Implements the Context Groups feature across the context-config and context-proxy NEAR contracts. Context Groups introduce a hierarchical relationship between contexts, enabling:

  • Workspace-level grouping of related contexts (e.g., DMs, channels)
  • Single-trigger version propagation (upgrade once, all contexts converge)
  • Workspace-level user management with on-chain audit trail

Changes done

  • Added OnChainGroupMeta and extended ContextConfigs with groups and context_group_refs; added Context.group_id, new Prefix variants (9–11), and 03_context_groups migration with feature gate.
  • Added handle_group_request() with create_group(), delete_group(), add_group_members(), remove_group_members().
  • Added query methods group() and is_group_admin().
  • Added register_context_in_group() and unregister_context_from_group() mutations.
  • Added group_contexts() (paginated scan) and context_group() (O(1) reverse lookup) queries.
  • Added set_group_target() for admin-controlled application version updates with audit logging.
  • Added proxy_register_in_group() and proxy_unregister_from_group() on context-config (callable only by the context’s proxy).
  • Handled ProposalAction::RegisterInGroup and ProposalAction::UnregisterFromGroup in proxy’s execute_proposal() via cross-contract calls to context-config.
  • Extended proxy’s ext interface with proxy_register_in_group and proxy_unregister_from_group.
  • Added 03_context_groups migration and migration roundtrip tests.
  • Added integration tests for group operations in context-config/tests/groups.rs.
  • Updated workspace calimero-context-config dependency and Cargo.lock.

Dependency on core repo

This PR depends on the shared types crate changes in the core repo (feat/context-management-proposal branch) which adds ContextGroupId, AppKey, GroupRequest, GroupRequestKind, RequestKind::Group, ProposalAction::RegisterInGroup, and ProposalAction::UnregisterFromGroup.

Before merging: Once the core PR is merged and a new version is released, update the calimero-context-config tag in Cargo.toml (currently 0.10.0-rc.1) to the new release tag containing the context groups types.

Test plan

  • Build both contracts: cd contracts/near/context-config && ./build.sh && cd ../context-proxy && ./build.sh
  • Run context-config tests: cargo test --package calimero-context-config-near
  • Run context-proxy tests: cargo test --package calimero-context-proxy-near
  • Verify migration compiles: cargo check --features "migrations,03_context_groups"
  • Verify 100% backward compatibility — existing context operations unchanged
  • Update calimero-context-config tag in Cargo.toml to new core release version after core PR merges

Note

High Risk
High risk because it introduces new on-chain state (groups, permissions, allowlists, invitation commitments) and new authorization paths/cross-contract calls, plus multiple storage migrations that must preserve existing contract state.

Overview
Adds on-chain Context Groups to the context-config NEAR contract, introducing new persistent group state (groups, context_group_refs, Context.group_id) and a new RequestKind::Group mutation flow for group CRUD, membership management, context registration, and target-application updates (including stored migration_method).

Implements group-level permissions and visibility controls (capability bitfields, per-context Open/Restricted visibility with allowlists) and a commit/reveal group invitation mechanism with signature verification, replay protection, and nonce handling; also emits a structured NEP-297 event when an admin force-joins a restricted context.

Extends proxy governance: context-proxy proposals can now RegisterInGroup/UnregisterFromGroup via new cross-contract calls, and context-config adds corresponding proxy-only entrypoints; adds storage wipe support for groups, plus new feature-gated migrations (0306) and substantial integration/migration test coverage.

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

- Added support for context groups in the contract state, including new fields for managing groups and their references.
- Introduced a migration script to handle the transition to the new context group structure.
- Updated the `mutate` function to accommodate group-related requests, with a placeholder for future implementation.
- Enhanced the test suite to include migration tests for context groups, ensuring data integrity during transitions.

This update enhances the contract's functionality and prepares it for future group management features.
- Added methods for creating, deleting, and managing group memberships within the context configuration.
- Enhanced the `mutate` function to handle group-related requests, including error handling for group operations.
- Introduced a new `GroupInfoResponse` struct for querying group details.
- Implemented tests for group creation, querying, and membership management to ensure functionality and integrity.

This update significantly expands the contract's capabilities for managing context groups.
…ion and querying

- Implemented methods for registering and unregistering contexts within groups, ensuring only group admins can perform these actions.
- Updated the `mutate` function to handle new context-related requests, including logging for successful registrations and unregistrations.
- Added new query functions to retrieve contexts associated with a group and to find the group of a specific context.
- Expanded the test suite to include integration tests for context registration and validation of group-context relationships.

This update significantly improves the contract's capabilities for managing context associations within groups.
…text groups

- Added functionality to set the target application for context groups, allowing group admins to update the target application associated with a group.
- Enhanced the `mutate` function to handle `SetTargetApplication` requests, including logging for successful updates.
- Implemented tests to verify the correct behavior of target application management, including checks for admin permissions and handling of non-existent groups.

This update significantly improves the management capabilities of context groups by allowing dynamic updates to their target applications.
…egistration

- Implemented `proxy_register_in_group` and `proxy_unregister_from_group` methods in the context configuration contract, allowing context proxies to manage group memberships.
- Updated the `ProposalAction` enum to include actions for registering and unregistering contexts from groups.
- Enhanced the `mutate` function to handle these new actions, ensuring proper interaction with the context configuration contract.

This update improves the functionality of context proxies by enabling them to manage group associations directly.
… tag

- Changed the dependency for `calimero-context-config` from a local path to a specific git tag (`0.10.0-rc.1`), ensuring consistent versioning and easier dependency management.
…ability

- Simplified the formatting of function signatures and require statements in `mutate.rs` and `query.rs` for better readability.
- Enhanced test assertions in `groups.rs` to improve clarity and maintainability.
- These changes do not alter functionality but improve the overall code structure and consistency.
…ions

- Updated function signatures in `groups.rs` and `sandbox.rs` to use lifetime parameters for better memory management.
- Enhanced test assertions to reflect updated expected values, ensuring accuracy in test outcomes.
- These changes improve code clarity and maintainability without altering existing functionality.
…d member handling

- Introduced `admin_nonces` and `members` fields in `OnChainGroupMeta` to track admin nonces and group members.
- Updated the `mutate` function to include nonce checks for group operations, ensuring proper admin authorization.
- Modified member management logic to utilize a set for members, improving efficiency and clarity.
- Enhanced tests in `groups.rs` to validate new member handling and nonce functionality, ensuring robustness in group operations.

This update significantly improves the integrity and functionality of group management within the context configuration.
- Introduced `approved_registrations` field in `OnChainGroupMeta` to track approved context registrations.
- Implemented `approve_context_registration` method in the `mutate` function, allowing group admins to approve context registrations.
- Updated the `mutate` function to handle new `ApproveContextRegistration` request type.
- Enhanced tests in `groups.rs` to validate the approval process for context registrations, ensuring only admins can approve.

This update significantly enhances the context management capabilities within groups by allowing controlled registration approvals.
- Introduced `context_ids` field in `OnChainGroupMeta` to maintain a forward index of contexts belonging to a group, enabling efficient pagination.
- Updated the `mutate` function to initialize and manage `context_ids`, including insertion and removal of context IDs during group operations.
- Enhanced the query functionality to retrieve context IDs associated with a group, improving the overall context management capabilities.

This update significantly enhances the ability to track and manage contexts within groups, optimizing performance for context-related queries.
calimero-context-config-near = { path = "./contracts/near/context-config" }

[patch."https://github.com/calimero-network/core"]
calimero-context-config = { path = "../core/crates/context/config" }
Copy link

Choose a reason for hiding this comment

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

Local filesystem path patch committed in Cargo.toml

High Severity

The [patch] section overrides calimero-context-config with a local filesystem path (../core/crates/context/config). This breaks builds for anyone who doesn't have the core repo checked out at that exact relative path, including CI pipelines. The corresponding Cargo.lock also lost its git source line, confirming the override is active.

Fix in Cursor Fix in Web

- Updated the `drain` method in `ContextConfigs` to clear all relevant fields in the group, including `admin_nonces`, `members`, `approved_registrations`, and `context_ids`.
- This change ensures that all group-related data is properly reset, improving data integrity during context operations.
…oup_id

- Modified the `proxy_unregister_from_group` method to accept a `group_id` parameter, enhancing the validation of context-group associations.
- Updated the `ProposalAction` enum to include the `group_id` in the unregistration action, ensuring proper handling during group management operations.
- These changes improve the robustness of group membership management within the context proxy.
Adds a public query method to retrieve the current nonce for a group admin,
mirroring the existing fetch_nonce method for context members.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add on-chain commit/reveal flow for group invitations, mirroring the
existing context invitation pattern. This allows joiners to claim
admin-signed invitations without needing admin action at join time.

- Add GroupInvitationCommitments/GroupUsedInvitations storage prefixes
- Add invitation_commitments and used_invitations fields to OnChainGroupMeta
- Create group_invitation.rs with commit_group_invitation and reveal_group_invitation
- Skip nonce check for invitation variants (joiner has no admin nonce)
- Dispatch new GroupRequestKind variants in handle_group_request
- Initialize/clear new fields in create_group, delete_group, and erase
- Add 04_group_invitations migration for existing groups

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rtb-12 and others added 2 commits March 5, 2026 18:09
Add group_members paginated view method returning admin/member entries
with role tags, enabling cross-node member sync.

Add JoinContextViaGroup mutation allowing verified group members to join
contexts within their group without the full invitation ceremony. Uses
privileged signer bypass to add the new member to the context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Track per-group signer-to-context identities and use authorized guard mutation to remove group members from all registered contexts when membership is revoked.

Made-with: Cursor
rtb-12 and others added 3 commits March 6, 2026 22:16
Store `migration_method` in `OnChainGroupMeta` so peer nodes can recover
it during group sync. Without this, `maybe_lazy_upgrade` on peer nodes
short-circuits because `migration: None` after sync.

- Add `migration_method: Option<String>` to `OnChainGroupMeta`
- Add contract storage migration (05_group_migration_method)
- Update `set_group_target` to accept and store migration method
- Return `migration_method` in `GroupInfoResponse` query
- Update `GroupRequestKind::SetTargetApplication` with new field
- Add sandbox tests for migration method propagation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nd visibility

Implement on-chain group permission system:
- MemberCapabilities bitfield (CAN_CREATE_CONTEXT, CAN_INVITE_MEMBERS, CAN_JOIN_OPEN_CONTEXTS)
- VisibilityMode (Open/Restricted) per context with allowlists
- Migration 06: existing members get CAN_JOIN_OPEN_CONTEXTS
- Capability checks on register_context, join_context_via_group, reveal_group_invitation
- New mutations: set_member_capabilities, set_context_visibility, manage_context_allowlist,
  set_default_capabilities, set_default_visibility
- New queries: context_visibility, context_allowlist
- AdminContextJoinEvent (NEP-297) for admin force-joins on restricted contexts
- Auto-add creator to allowlist on restricted context registration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…sions

Add 13 new tests covering:
- CAN_CREATE_CONTEXT capability gate on register_context_in_group
- Admin bypass of capability checks
- Creator auto-added to allowlist on Restricted context
- CAN_JOIN_OPEN_CONTEXTS gate on join_context_via_group
- Restricted context blocked for non-allowlist members
- Allowlist member can join restricted context
- New members inherit default_member_capabilities
- Non-admin rejected from set_member_capabilities
- Lockdown mode (default_capabilities=0)
- Default visibility inheritance (Restricted)
- Non-creator/non-admin rejected from set_context_visibility
- Allowlist add/remove lifecycle

Also fix existing tests to pass visibility_mode field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@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 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Erase function doesn't clear all group storage collections
    • Added clearing of member_contexts, member_capabilities, context_visibility, and context_allowlists collections to match delete_group behavior.
  • ✅ Fixed: Removed member privileges not revoked from used_open_invitations guard
    • Added privilege revocation from used_open_invitations guard during cascade removal of group members.

Create PR

Or push these changes by commenting:

@cursor push a78ec72d3b
Preview (a78ec72d3b)
diff --git a/contracts/near/context-config/src/mutate.rs b/contracts/near/context-config/src/mutate.rs
--- a/contracts/near/context-config/src/mutate.rs
+++ b/contracts/near/context-config/src/mutate.rs
@@ -578,9 +578,7 @@
             } => {
                 self.set_default_capabilities(signer_id, group_id, default_capabilities);
             }
-            GroupRequestKind::SetDefaultVisibility {
-                default_visibility,
-            } => {
+            GroupRequestKind::SetDefaultVisibility { default_visibility } => {
                 self.set_default_visibility(signer_id, group_id, default_visibility.into());
             }
         }
@@ -611,12 +609,9 @@
         let used_invitations = IterableSet::new(Prefix::GroupUsedInvitations(*group_id));
         let member_contexts = IterableMap::new(Prefix::GroupMemberContexts(*group_id));
 
-        let member_capabilities =
-            IterableMap::new(Prefix::GroupMemberCapabilities(*group_id));
-        let context_visibility =
-            IterableMap::new(Prefix::GroupContextVisibility(*group_id));
-        let context_allowlists =
-            IterableMap::new(Prefix::GroupContextAllowlists(*group_id));
+        let member_capabilities = IterableMap::new(Prefix::GroupMemberCapabilities(*group_id));
+        let context_visibility = IterableMap::new(Prefix::GroupContextVisibility(*group_id));
+        let context_allowlists = IterableMap::new(Prefix::GroupContextAllowlists(*group_id));
 
         let meta = OnChainGroupMeta {
             app_key,
@@ -755,6 +750,10 @@
             let identity = context_identity.rt().expect("infallible conversion");
             context.members.priviledges().revoke(&identity);
             context.application.priviledges().revoke(&identity);
+            context
+                .used_open_invitations
+                .priviledges()
+                .revoke(&identity);
 
             env::log_str(&format!(
                 "Cascade-removed `{}` from context `{}`",
@@ -784,10 +783,7 @@
                 .map_or(false, |caps| {
                     caps & MemberCapabilities::CAN_CREATE_CONTEXT != 0
                 });
-        require!(
-            can_create,
-            "insufficient capabilities to create context"
-        );
+        require!(can_create, "insufficient capabilities to create context");
 
         let mode = visibility_mode.unwrap_or(group.default_context_visibility);
 
@@ -974,10 +970,7 @@
                     let on_allowlist = group
                         .context_allowlists
                         .contains_key(&(*context_id, signer_id.clone()));
-                    require!(
-                        on_allowlist,
-                        "not on allowlist for this restricted context"
-                    );
+                    require!(on_allowlist, "not on allowlist for this restricted context");
                 } else {
                     // Open context: requires CAN_JOIN_OPEN_CONTEXTS
                     let can_join = group
@@ -986,10 +979,7 @@
                         .map_or(false, |caps| {
                             caps & MemberCapabilities::CAN_JOIN_OPEN_CONTEXTS != 0
                         });
-                    require!(
-                        can_join,
-                        "insufficient capabilities to join open context"
-                    );
+                    require!(can_join, "insufficient capabilities to join open context");
                 }
             } else if is_restricted {
                 // Admin force-joining a restricted context they're not on the allowlist for
@@ -1059,10 +1049,7 @@
             "only group admins can set member capabilities"
         );
 
-        require!(
-            group.members.contains(&member),
-            "member not in group"
-        );
+        require!(group.members.contains(&member), "member not in group");
 
         let _ignored = group.member_capabilities.insert(*member, capabilities);
 
@@ -1142,20 +1129,19 @@
         );
 
         for member in &add {
-            let _ignored = group
-                .context_allowlists
-                .insert((*context_id, **member), ());
+            let _ignored = group.context_allowlists.insert((*context_id, **member), ());
         }
 
         for member in &remove {
-            let _ignored = group
-                .context_allowlists
-                .remove(&(*context_id, **member));
+            let _ignored = group.context_allowlists.remove(&(*context_id, **member));
         }
 
         env::log_str(&format!(
             "Updated allowlist for context `{}` in group `{}`: added {}, removed {}",
-            context_id, group_id, add.len(), remove.len()
+            context_id,
+            group_id,
+            add.len(),
+            remove.len()
         ));
     }
 

diff --git a/contracts/near/context-config/src/sys.rs b/contracts/near/context-config/src/sys.rs
--- a/contracts/near/context-config/src/sys.rs
+++ b/contracts/near/context-config/src/sys.rs
@@ -57,6 +57,10 @@
             group.context_ids.clear();
             group.invitation_commitments.clear();
             group.used_invitations.clear();
+            group.member_contexts.clear();
+            group.member_capabilities.clear();
+            group.context_visibility.clear();
+            group.context_allowlists.clear();
         }
         self.context_group_refs.clear();
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

- Remove redundant `context_count` field from `OnChainGroupMeta`; derive
  count from `context_ids.len()` at query time to prevent drift
- Add missing `clear()` calls for `member_contexts`, `member_capabilities`,
  `context_visibility`, and `context_allowlists` in `erase`
- Add `member_contexts` cleanup to `proxy_unregister_from_group` to match
  the cleanup already done in `unregister_context_from_group`

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…fix migration schemas

- Add context_visibility and context_allowlists cleanup to both
  unregister_context_from_group and proxy_unregister_from_group to
  prevent storage leaks and stale data on re-registration
- Remove nonexistent context_count field from OldOnChainGroupMeta in
  migrations 04-06 to match the actual on-chain schema and prevent
  borsh deserialization corruption

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@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 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

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

.expect("group does not exist");
let _ignored = group
.member_contexts
.insert((signer_id.clone(), *context_id), *new_member);
Copy link

Choose a reason for hiding this comment

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

Repeated group join overwrites tracked context identity

Medium Severity

In join_context_via_group, the member_contexts map uses (SignerId, ContextId) as the key. If the same signer joins the same context multiple times with different ContextIdentity values, insert silently overwrites the previous entry. When remove_group_members later cascade-removes members, it only finds the last recorded identity — earlier identities become orphaned context members that can never be cascade-removed.

Fix in Cursor Fix in Web

err_str.contains("only group admins can register contexts"),
"Expected 'only group admins can register contexts', got: {}",
err_str
);
Copy link

Choose a reason for hiding this comment

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

Test expects wrong error message for register rejection

Low Severity

test_non_admin_register_rejected asserts the error contains "only group admins can register contexts", but register_context_in_group produces "insufficient capabilities to create context" when a non-member signer lacks the CAN_CREATE_CONTEXT capability. The assertion string never matches, so this test would always fail.

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.

1 participant