AFP engineering series · Chapter 20 of 25
Keep email, messaging, and document-sharing tools from turning an LLM assistant into an unapproved bulk-action service.
The harmful effect may be outside the conversation
A model can generate a harmless-looking message and still misuse an outbound tool by sending it to the wrong people, too many people, or people who did not consent to that communication. Content moderation alone does not establish recipient authorization. The action service needs to evaluate audience, purpose, rate, and approval.
For a fictional customer-support workflow, the assistant may draft a reply to the customer associated with an open case. That capability should not imply permission to upload a contact list and send a campaign. Define outbound actions around the legitimate workflow rather than exposing a universal send_message function with arbitrary recipients.
Resolve recipients from trusted records
Use a case or customer identifier that the backend resolves to an approved contact channel. If the workflow allows a new recipient, verify the relationship and purpose separately. A document saying “send the report here” is an external claim, not authorization to transmit internal data.
Keep consent, subscription state, and suppression rules in authoritative systems. Do not infer consent from friendly language or a model's summary of earlier correspondence. A recipient who opted out should remain suppressed even if the assistant generates a new message or chooses another delivery tool.
Bind approval to content and audience
A reviewer should see the actual message, recipient count, recipient scope, attachments, and data classification. Store a digest of the canonical content and audience selection. If either changes, invalidate the approval. Approving a draft for one customer is not approval to send a revised draft to an entire segment.
For large recipient sets, use a stable audience snapshot or a versioned query whose semantics are explicit. A dynamic query evaluated hours later can select a different population. The action record should make that difference visible and require reapproval when the consequence changes.
Enforce a narrow outbound contract
Illustrative python; adapt the persistence and identity adapters to your service.
def may_send(case, proposal, approval):
checks = [
proposal.case_id == case.id,
proposal.recipient_id == case.verified_contact_id,
not case.contact_suppressed,
proposal.digest == approval.action_digest,
approval.valid_now,
]
return all(checks)
# Delivery service also enforces rate limits and idempotency.The verified contact and suppression state must come from trusted services. The model must not be able to set them through tool arguments. At execution, recheck current suppression and account status, because they may have changed after the draft was prepared.
Use a stable message operation identifier to avoid duplicates during retries. If delivery status is uncertain, reconcile with the provider rather than creating a new send operation. Track recipient-level outcomes for batches so a partial failure does not resend successfully delivered messages.
Inspect attachments and links
A message can disclose information through an attachment or a link even when its visible text is acceptable. Classify attached documents, validate access grants, and ensure shared links do not expose a broader folder or tenant. Expiring links should have an appropriate audience and revocation mechanism.
Treat generated URLs as data requiring validation. Avoid automatically fetching or embedding arbitrary remote resources in a message preview. A preview service can itself create an outbound request and leak information. Apply destination and data rules to preview generation as well as final delivery.
Test abuse scenarios without contacting real people
| Synthetic scenario | Required outcome |
|---|---|
| Case reply to verified contact | Allowed under policy |
| Same content to unrelated recipient | Denied |
| Approved draft with changed attachment | Reapproval required |
| Suppression added after approval | Delivery blocked |
| Timeout after provider acceptance | No duplicate send |
| Large unapproved audience | Capability or budget denial |
Use a fake delivery adapter that records recipients and payloads in a sandbox. Assert zero deliveries for denied cases. Browser tests should verify that previews show the same content and audience as the action record. Do not validate a messaging guardrail by sending test mail to unsuspecting recipients.
Include legitimate bulk workflows if the product supports them, but give them separate capabilities, approval rules, and budgets. A narrow support tool should not grow campaign functionality through an undocumented optional parameter.
Worked delivery incident: a support reply becomes a campaign
A support agent drafts a refund explanation for one customer. A later instruction asks it to send the same message to everyone who bought the product. If the tool accepts arbitrary recipient lists, a narrow support task can become an unapproved campaign. The text may pass content screening while the audience and purpose violate policy.
Define send_case_reply(case_id, draft_id) so the backend resolves one verified contact and the current suppression state. A separate campaign capability, if the product supports it, requires audience approval, consent rules, rate limits, and a stable audience snapshot. The assistant cannot promote a case reply into a campaign by adding recipients to the arguments.
Test a draft approved for one recipient, then change the audience to ten recipients. The approval digest should fail. Test a recipient opting out after approval but before delivery; the current suppression check should prevent the send. Test a provider timeout after acceptance; the operation should reconcile rather than create a duplicate message.
Attachments need the same scrutiny. A case reply should not attach an internal export merely because a retrieved document suggested it. Resolve permitted attachments from the case and verify their data classification and access scope. The preview should show the actual attachment names and audience that will be used at execution.
For containment, pause the campaign or tool while preserving read-only case support. Use the delivery ledger to identify actual recipients, not just intended ones, and avoid duplicate remediation. This case demonstrates that outbound abuse prevention is about authority over audience, content, timing, and data together. A well-written message is still an unsafe action when sent outside the approved relationship.
Operate with rapid containment
Provide controls to pause delivery by tenant, credential, campaign, or tool while leaving read-only support available. Record the reason and scope of each pause. Preserve a restricted delivery ledger so operators can identify affected recipients and avoid duplicate remediation messages.
Measure unauthorized-recipient attempts, suppressed deliveries, duplicate prevention, complaint rates, and legitimate completion. Content quality and delivery authority are complementary concerns. An AFP system protects the people reached by the assistant, not just the person typing into its chat window.
Practice and continue
Open the interactive companion lab to inspect the assumptions and outcomes with synthetic data. Then return to the 25-chapter AFP learning path.
Sources and implementation scope
Primary references checked October 15, 2026. Framework details follow the linked documentation; the Northstar cases, policy contracts, and test designs are original illustrative designs. Python snippets labeled illustrative are not complete production integrations. ADK package metadata checked for this series reported 2.12.0; pin and test the version used by your application.