diff --git a/src/modules/approvals/primitive.ts b/src/modules/approvals/primitive.ts index 45620fa07a0..20a865af492 100644 --- a/src/modules/approvals/primitive.ts +++ b/src/modules/approvals/primitive.ts @@ -195,26 +195,77 @@ export async function requestApproval(opts: RequestApprovalOptions): Promise t.label).join(', ')}).`, ); - } catch (err) { - log.error('Failed to deliver approval card', { action, approvalId, err }); - notifyAgent(session, `${action} failed: could not deliver approval request to ${target.userId}.`); return; } } - log.info('Approval requested', { action, approvalId, agentName, approver: target.userId }); + log.info('Approval requested', { + action, + approvalId, + agentName, + approver: target.userId, + deliveredTo: session.messaging_group_id ? 'dm+origin' : 'dm', + }); } diff --git a/src/modules/approvals/response-handler.ts b/src/modules/approvals/response-handler.ts index 2bbdc9d32ff..dc227e8f2b3 100644 --- a/src/modules/approvals/response-handler.ts +++ b/src/modules/approvals/response-handler.ts @@ -19,7 +19,7 @@ import { log } from '../../log.js'; import { writeSessionMessage } from '../../session-manager.js'; import type { PendingApproval } from '../../types.js'; import { ONECLI_ACTION, resolveOneCLIApproval } from './onecli-approvals.js'; -import { getApprovalHandler } from './primitive.js'; +import { getApprovalHandler, pickApprover } from './primitive.js'; export async function handleApprovalsResponse(payload: ResponsePayload): Promise { // OneCLI credential approvals — resolved via in-memory Promise first. @@ -57,6 +57,31 @@ async function handleRegisteredApproval( return; } + // Click authorization: the responder must be in the approver list for this + // session's agent group. We deliver the card to multiple targets (DM + + // origin chat) for visibility, which means non-admin chat members might + // see the buttons. This check prevents them from approving on the admin's + // behalf. The userId comes from the channel adapter's response payload — + // for a button-click in a chat, that's the clicker's namespaced user id. + // Empty userId (legacy or untrusted responder) is rejected. + if (!userId) { + log.warn('Approval click had no userId — ignoring', { + approvalId: approval.approval_id, + action: approval.action, + }); + return; + } + const approvers = pickApprover(session.agent_group_id); + if (!approvers.includes(userId)) { + log.warn('Approval click from non-approver — ignoring', { + approvalId: approval.approval_id, + action: approval.action, + clicker: userId, + approvers, + }); + return; + } + const notify = (text: string): void => { writeSessionMessage(session.agent_group_id, session.id, { id: `appr-note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,