{"record":{"id":"fd04c4e5599aea97","repo":"nanocoai/nanoclaw","slug":"role-not-found","errorCode":null,"errorMessage":"role not found","messagePattern":"role not found","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/cli/resources/roles.ts","lineNumber":69,"sourceCode":"        return { user_id: userId, role, agent_group_id: groupId };\n      },\n    },\n    revoke: {\n      access: 'approval',\n      description: 'Revoke a role. Use --user, --role, and --group if scoped.',\n      handler: async (args) => {\n        const userId = args.user as string;\n        const role = args.role as string;\n        const groupId = (args.group as string) ?? null;\n        if (!userId) throw new Error('--user is required');\n        if (!role) throw new Error('--role is required');\n        const result = await getDb().run(\n          'DELETE FROM user_roles WHERE user_id = ? AND role = ? AND agent_group_id IS NOT DISTINCT FROM ?',\n          userId,\n          role,\n          groupId,\n        );\n        if (result.changes === 0) throw new Error('role not found');\n        return { revoked: { user_id: userId, role, agent_group_id: groupId } };\n      },\n    },\n  },\n});\n","sourceCodeStart":51,"sourceCodeEnd":75,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/cli/resources/roles.ts#L51-L75","documentation":"Thrown by `ncl roles revoke` when the DELETE on `user_roles` affected zero rows — no row matched the exact (user_id, role, agent_group_id) triple, including NULL-vs-NULL matching via `IS NOT DISTINCT FROM`. The role literally does not exist as granted, so there is nothing to revoke.","triggerScenarios":"`ncl roles revoke --user X --role admin` when X has owner but not admin; revoking without --group when the grant was group-scoped (agent_group_id mismatch); revoking with --group when the grant was global; misspelled user id or role.","commonSituations":"Scope mismatch between grant and revoke (global vs group-scoped admin) is the classic case — `IS NOT DISTINCT FROM` requires the NULL/non-NULL shape to line up exactly; running revoke twice; user id format drift (handle renamed on the platform so the stored user_id differs).","solutions":["List actual roles: `ncl roles list` — confirm the exact user_id, role, and agent_group_id of the grant","If the grant is group-scoped, add --group <agent-group-id>; if it is global, omit --group","If the user id changed (e.g. platform handle rename), revoke using the old stored id or update the user record first"],"exampleFix":"# before (grant was scoped)\nncl roles grant --user telegram:alice --role admin --group grp_123\nncl roles revoke --user telegram:alice --role admin\n# after\nncl roles revoke --user telegram:alice --role admin --group grp_123","handlingStrategy":"try-catch","validationCode":"const roles = await execNclJson(['roles', 'list']);\nconst exists = roles.some(r => r.user_id === userId && r.role === role &&\n  (r.agent_group_id ?? null) === (groupId ?? null));\nif (!exists) { console.error('no such grant; nothing to revoke'); return; }\nawait execNcl(['roles', 'revoke', '--user', userId, '--role', role,\n  ...(groupId ? ['--group', groupId] : [])]);","typeGuard":"const isExactGrant = (row: RoleRow, userId: string, role: string, groupId?: string | null) =>\n  row.user_id === userId && row.role === role &&\n  ((row.agent_group_id ?? null) === (groupId ?? null));","tryCatchPattern":"try { await revoke(); } catch (e) { if (e.message === 'role not found') { /* idempotent: treat as success or log */ } else throw e; }","preventionTips":["Mirror the exact --group shape used at grant time when revoking","Make revokes idempotent by treating 'role not found' as success in automation","Snapshot `ncl roles list --json` before bulk role changes"],"tags":["cli","roles","not-found","permissions"],"backgroundTag":"record-not-found","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}