{"record":{"id":"de9534a2c1695c49","repo":"nanocoai/nanoclaw","slug":"ignoring-unauthorized-approval-response","errorCode":null,"errorMessage":"Ignoring unauthorized approval response","messagePattern":"Ignoring unauthorized approval response","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/modules/approvals/response-handler.ts","lineNumber":40,"sourceCode":"  getSession,\n  transitionPendingApprovalStatus,\n} from '../../db/sessions.js';\nimport type { ResponsePayload } from '../../response-registry.js';\nimport { log } from '../../log.js';\nimport { writeSessionMessage } from '../../session-manager.js';\nimport type { PendingApproval } from '../../types.js';\nimport { hasAdminPrivilege, isGlobalAdmin, isOwner } from '../permissions/db/user-roles.js';\nimport { finalizeReject } from './finalize.js';\nimport { ONECLI_ACTION, resolveOneCLIApproval } from './onecli-approvals.js';\nimport { getApprovalHandler, notifyApprovalResolved, REJECT_WITH_REASON_VALUE } from './primitive.js';\nimport { armReasonCapture } from './reason-capture.js';\n\nexport async function handleApprovalsResponse(payload: ResponsePayload): Promise<boolean> {\n  const approval = await getPendingApproval(payload.questionId);\n  if (!approval) return false;\n\n  if (!(await isAuthorizedApprovalClick(approval, payload))) {\n    log.warn('Ignoring unauthorized approval response', {\n      approvalId: approval.approval_id,\n      action: approval.action,\n      userId: payload.userId,\n      channelType: payload.channelType,\n    });\n    return true;\n  }\n\n  if (approval.action === ONECLI_ACTION) {\n    if (await resolveOneCLIApproval(payload.questionId, payload.value)) {\n      return true;\n    }\n    // Row exists but the in-memory resolver is gone (timer fired or the process\n    // was in a weird state). Nothing to do — just drop the row.\n    await deletePendingApproval(payload.questionId);\n    return true;\n  }\n","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/modules/approvals/response-handler.ts#L22-L58","documentation":"An approval button/response arrived from a user who is not the designated approver and lacks admin privilege for the agent group. The response is deliberately ignored (claimed but not acted on) to prevent unauthorized approval actions.","triggerScenarios":"A non-approver group member clicks an approve/reject button on an approval card; a user from a different channel identity than approver_user_id clicks; payload.userId maps to no privileged user.","commonSituations":"Approval card posted into a shared group channel where anyone can click it; multiple admins where the card was addressed to one; spoofed or misrouted callback payloads.","solutions":["Have the designated approver (row.approver_user_id) or an owner/global admin click the response","Grant the intended user admin privilege via 'ncl roles grant' if they should be able to approve","Verify payload.userId resolves to the expected <channel>:<handle> identity"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const authorized = clickerId === row.approver_user_id || (await hasAdminPrivilege(clickerId, row.agent_group_id));\nif (!authorized) return; // don't call handleApprovalsResponse","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Deliver approval cards to DMs, not shared channels","Keep the approver set minimal and explicitly granted via ncl roles"],"tags":["approvals","authorization","button-click"],"backgroundTag":"unauthorized-action-rejected","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}