Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Approval-based user management.
.claude/skills/aiskillstore-extension-user-approval/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -34% | 0% |
| case-03 | ✗→✓ | ▲ Improved | -60% | 0% |
| case-04 | ✗→✓ | ▲ Improved | -28% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 36% | 0% |
| case-08 | ✗→✓ | ▲ Improved | -22% | 0% |
User approval extension for Caffeine AI.
This skill adds approval-based user management. Users request access; admins approve or reject. Approved users gain access to protected features.
Approval-based user management:
Prerequisite: You must follow extension-authorization first, as this integration depends on it.
There is a prefabricated module mo:caffeineai-user-approval/approval that cannot be modified. It provides approval-based user management with role-based access control.
mo:caffeineai-user-approval/approvalimport AccessControl "mo:caffeineai-authorization/access-control"; module { public type ApprovalStatus = { #approved; #rejected; #pending; }; public type UserApprovalState = { /* internal state */ }; public func initState(accessControlState: AccessControl.AccessControlState) : UserApprovalState; public func isApproved(state : UserApprovalState, caller : Principal) : Bool; public func setApproval(state : UserApprovalState, user : Principal, approval : ApprovalStatus); public type UserApprovalInfo = { principal : Principal; status : ApprovalStatus; }; public func listApprovals(state : UserApprovalState) : [UserApprovalInfo]; }
Usage (all the following functions are required to be added):
motoko filepath=src/backend/main.moimport AccessControl "mo:caffeineai-authorization/access-control"; import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization"; import UserApproval "mo:caffeineai-user-approval/approval"; import Principal "mo:core/Principal"; import Runtime "mo:core/Runtime"; actor { // Include authorization let accessControlState = AccessControl.initState(); include MixinAuthorization(accessControlState); let approvalState = UserApproval.initState(accessControlState); public query ({ caller }) func isCallerApproved() : async Bool { AccessControl.hasPermission(accessControlState, caller, #admin) or UserApproval.isApproved(approvalState, caller); }; public shared ({ caller }) func requestApproval() : async () { UserApproval.requestApproval(approvalState, caller); }; public shared ({ caller }) func setApproval(user : Principal, status : UserApproval.ApprovalStatus) : async () { if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) { Runtime.trap("Unauthorized: Only admins can perform this action"); }; UserApproval.setApproval(approvalState, user, status); }; public query ({ caller }) func listApprovals() : async [UserApproval.UserApprovalInfo] { if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) { Runtime.trap("Unauthorized: Only admins can perform this action"); }; UserApproval.listApprovals(approvalState); }; // In addition to access control guards, add an approval check where needed: // Admins should have the permission do use all functionality // * Approved users only: // if (not (UserApproval.isApproved(approvalState, caller) or AccessControl.hasPermission(accessControlState, caller, #admin))) { // Runtime.trap("Unauthorized: Only approved users can perform this action"); // }; };
On initState, existing admins are automatically approved. All other users are pending.
IMPORTANT: Apply the right authorization and/or approval check to each public function.
Approval-based user management:
isCallerApproved)requestApproval)For admin users, provide a dashboard to:
listApprovals)setApproval)getCallerUserRole and assignCallerUserRole)The backend already implements the following functionality. The full interface can be found in <backend-interface>
// Check if current user is approved, admins are always approved isCallerApproved(): Promise<boolean>;
// Submit approval request requestApproval(): Promise<void>;
// Get all users and their approval status (admin only) listApprovals(): Promise<Array<UserApprovalInfo>>;
// Approve or reject a user (admin only) setApproval(user: Principal, status: ApprovalStatus): Promise<void>;
// Assign a role to a user (admin only) assignCallerUserRole(user: Principal, role: UserRole): Promise<void>;
// Get current role for a specific user getCallerUserRole(): Promise<UserRole>;
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 26,680 | 16,422 | -38% | 1 | 1 | 0% | 4,554 | 2,986 | -34% | 0 | 0 | — |
case-02 | fail→fail | 55,316 | 10,033 | -82% | 1 | 1 | 0% | 8,261 | 1,677 | -80% | 0 | 0 | — |
case-03 | fail→pass | 34,690 | 15,075 | -57% | 1 | 1 | 0% | 8,020 | 3,224 | -60% | 0 | 0 | — |
case-04 | fail→pass | 25,696 | 9,479 | -63% | 1 | 1 | 0% | 2,692 | 1,947 | -28% | 0 | 0 | — |
case-05 | fail→pass | 16,138 | 13,050 | -19% | 1 | 1 | 0% | 1,787 | 2,439 | +36% | 0 | 0 | — |
case-06 | fail→fail | 15,727 | 17,539 | +12% | 1 | 1 | 0% | 1,841 | 3,498 | +90% | 0 | 0 | — |
case-07 | pass→pass | 19,626 | 5,356 | -73% | 1 | 1 | 0% | 2,167 | 2,085 | -4% | 0 | 0 | — |
case-08 | fail→pass | 19,380 | 4,984 | -74% | 1 | 1 | 0% | 2,310 | 1,811 | -22% | 0 | 0 | — |
case-09 | pass→pass | 16,382 | 10,583 | -35% | 1 | 1 | 0% | 1,964 | 2,096 | +7% | 0 | 0 | — |
case-10 | pass→pass | 19,605 | 16,042 | -18% | 1 | 1 | 0% | 2,469 | 3,233 | +31% | 0 | 0 | — |
case-11 | pass→pass | 17,681 | 21,231 | +20% | 1 | 1 | 0% | 2,851 | 3,997 | +40% | 0 | 0 | — |
case-12 | fail→pass | 21,574 | 12,410 | -42% | 1 | 1 | 0% | 2,952 | 2,648 | -10% | 0 | 0 | — |
case-13 | fail→pass | 13,948 | 10,183 | -27% | 1 | 1 | 0% | 2,415 | 2,109 | -13% | 0 | 0 | — |
case-14 | pass→pass | 7,905 | 7,116 | -10% | 1 | 1 | 0% | 1,182 | 1,442 | +22% | 0 | 0 | — |
case-15 | fail→pass | 14,282 | 4,348 | -70% | 1 | 1 | 0% | 2,202 | 1,923 | -13% | 0 | 0 | — |
case-16 | fail→pass | 18,426 | 2,492 | -86% | 1 | 1 | 0% | 2,292 | 1,545 | -33% | 0 | 0 | — |
case-17 | fail→pass | 20,588 | 3,648 | -82% | 1 | 1 | 0% | 2,494 | 1,851 | -26% | 0 | 0 | — |
case-18 | pass→pass | 14,753 | 8,906 | -40% | 1 | 1 | 0% | 1,364 | 1,833 | +34% | 0 | 0 | — |
case-19 | pass→pass | 23,467 | 16,347 | -30% | 1 | 1 | 0% | 2,821 | 3,120 | +11% | 0 | 0 | — |
case-20 | fail→fail | 32,326 | 23,309 | -28% | 1 | 1 | 0% | 4,391 | 4,543 | +3% | 0 | 0 | — |
case-21 | pass→pass | 21,083 | 23,773 | +13% | 1 | 1 | 0% | 3,934 | 5,852 | +49% | 0 | 0 | — |
case-22 | pass→pass | 18,298 | 30,466 | +66% | 1 | 1 | 0% | 3,797 | 5,908 | +56% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +45 percentage points is the difference between those two pass rates over the 21 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.