Follow-up from PR #311 (#310) review.
Problem
Native Slack DEFER approvals (#311) record who clicked Approve/Reject but never check whether that user is authorized to approve. Today channel membership is the entire approval ACL — anyone who can see the message and click a button resolves a security-sensitive deferred tool call. There is no approver allowlist, no per-tool restriction, and no requester≠approver (four-eyes) gate. #oncall-style channels routinely include guests, contractors, bots, and integrations.
PR #311 documents this boundary (route to a tightly-scoped private channel); this issue adds the enforcement.
Proposal — email-based, adapter-agnostic, fail-closed
Design the allowlist around email, not Slack user IDs: email is a portable identity that resolves the same way across Slack / Teams / a web console and matches Forge's OIDC identity model, whereas U012… IDs are Slack-specific and opaque.
- Config — per-tool (and/or default) approver allowlist:
security:
defer:
tools:
cli_execute:
to: channel:slack:#sec-approvals
approvers: [alice@corp.com, oncall-lead@corp.com] # NEW
- Adapter resolves email. The Slack
block_actions payload carries only id/username/name/team_id — no email. The adapter resolves user.id → email via users.info (needs the users:read.email scope; one cacheable call per click) and populates a new ApproverEmail field on channels.ApprovalDecision.
- Core check stays adapter-agnostic. The allowlist check lives in the runtime/decisions path against
ApproverEmail, so Teams / web-console approvers use the same rule.
- Fail closed. If an email can't be resolved (scope missing, guest with no email), refuse the approval rather than fall back to the id/username. Surface a clear message on the approval UI ("could not verify your identity — approval refused").
- (Optional) four-eyes. Reject an approver whose email equals the task's requester identity, when the requester identity is known.
Acceptance
Out of scope
Follow-up from PR #311 (#310) review.
Problem
Native Slack DEFER approvals (#311) record who clicked Approve/Reject but never check whether that user is authorized to approve. Today channel membership is the entire approval ACL — anyone who can see the message and click a button resolves a security-sensitive deferred tool call. There is no approver allowlist, no per-tool restriction, and no requester≠approver (four-eyes) gate.
#oncall-style channels routinely include guests, contractors, bots, and integrations.PR #311 documents this boundary (route to a tightly-scoped private channel); this issue adds the enforcement.
Proposal — email-based, adapter-agnostic, fail-closed
Design the allowlist around email, not Slack user IDs: email is a portable identity that resolves the same way across Slack / Teams / a web console and matches Forge's OIDC identity model, whereas
U012…IDs are Slack-specific and opaque.block_actionspayload carries onlyid/username/name/team_id— no email. The adapter resolvesuser.id → emailviausers.info(needs theusers:read.emailscope; one cacheable call per click) and populates a newApproverEmailfield onchannels.ApprovalDecision.ApproverEmail, so Teams / web-console approvers use the same rule.Acceptance
approvers(email list) configurable per DEFER tool (+ optional default).users.info(cached) intoApprovalDecision.ApproverEmail.Out of scope