cd /news/ai-agents/multi-agent-safety-designing-recipie… · home › topics › ai-agents › article
[ARTICLE · art-166491] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Multi-Agent Safety: Designing Recipient Allowlists and Human-in-the-Loop Approval Holds in TypeScript

A developer outlined a TypeScript approach for constraining autonomous agents' outbound actions using deterministic "safety envelopes" rather than prompt instructions. The design pairs a RecipientAllowlistGate, which checks email recipients against exact addresses and allowed corporate domains before any network call, with an ApprovalHoldRegistry that stages external or sensitive actions as pending tickets for human approval instead of dispatching them or failing outright. The writeup argues that prompt-level instructions are not a security boundary because models can be jailbroken or misread context.

by read3 min views1 publishedNov 9, 2026

As autonomous agents transition from read-only coding assistants to proactive systems that deploy code, run marketing workflows, and message stakeholders, security and guardrail design become critical.

Giving an LLM direct access to an unconstrained outbound action (e.g., sending an email, refunding a charge, or deleting a cloud resource) without deterministic safeguards is an accident waiting to happen.

Prompt engineering alone ("Please make sure not to email anyone outside the company") is not a security boundary. Models can be jailbroken, misinterpret context, or suffer from semantic drift.

Let's look at how to build deterministic Safety Envelopes: Recipient Allowlists and Approval Holds.

In production systems, your safety guardrails must exist in code outside the LLM context window:

[Agent Reasoner] ──> [Tool Request] ──> [Deterministic Gate] ──> [Real World API]
                                               │
                                      (Violates Allowlist?)
                                               │
                                               ▼
                                      [Hold for Approval]

Here is a TypeScript utility that verifies target recipients against exact matches, wildcard patterns, and corporate domains before any network packet is dispatched:

export class RecipientAllowlistGate {
  private allowedDomains: Set<string>;
  private explicitAddresses: Set<string>;

  constructor(domains: string[], exactAddresses: string[] = []) {
    this.allowedDomains = new Set(domains.map((d) => d.toLowerCase().replace(/^@/, "")));
    this.explicitAddresses = new Set(exactAddresses.map((a) => a.toLowerCase()));
  }

  public isAllowed(recipientEmail: string): boolean {
    const cleanEmail = recipientEmail.trim().toLowerCase();

    // Check 1: Explicit address match
    if (this.explicitAddresses.has(cleanEmail)) {
      return true;
    }

    // Check 2: Domain match
    const domain = cleanEmail.split("@")[1];
    if (domain && this.allowedDomains.has(domain)) {
      return true;
    }

    return false;
  }

  public assertAllowed(recipientEmail: string): void {
    if (!this.isAllowed(recipientEmail)) {
      throw new Error(
        `🛡️ Security Guardrail: Outbound dispatch to '${recipientEmail}' blocked. Recipient is not in the authorized company allowlist.`
      );
    }
  }
}

What happens when an agent genuinely needs to message an external party or execute a sensitive mutation?

Rather than failing outright or sending without confirmation, the tool creates an Approval Hold and returns a hold ticket to the agent:

export interface HoldTicket {
  ticketId: string;
  action: string;
  payload: Record<string, unknown>;
  createdAt: string;
  status: "pending_approval" | "approved" | "rejected";
}

export class ApprovalHoldRegistry {
  private tickets = new Map<string, HoldTicket>();

  public createHold(action: string, payload: Record<string, unknown>): HoldTicket {
    const ticketId = `hold_${Math.random().toString(36).substring(2, 10)}`;
    const ticket: HoldTicket = {
      ticketId,
      action,
      payload,
      createdAt: new Date().toISOString(),
      status: "pending_approval",
    };

    this.tickets.set(ticketId, ticket);
    return ticket;
  }

  public approve(ticketId: string): HoldTicket {
    const ticket = this.tickets.get(ticketId);
    if (!ticket) throw new Error("Ticket not found");
    ticket.status = "approved";
    return ticket;
  }
}

When the agent attempts to execute an outbound side-effect:

const gate = new RecipientAllowlistGate(["mycompany.com", "partnerdev.io"]);
const holds = new ApprovalHoldRegistry();

async function executeOutboundTool(to: string, subject: string, body: string) {
  // 1. If recipient is internal / authorized, dispatch immediately
  if (gate.isAllowed(to)) {
    return await mailer.send({ to, subject, body });
  }

  // 2. If external, stage an Approval Hold
  const hold = holds.createHold("send_email", { to, subject, body });

  return {
    isError: false,
    content: [
      {
        type: "text",
        text: `⏳ ACTION HELD FOR HUMAN APPROVAL: The recipient '${to}' is outside the verified domain allowlist.\nTicket ID: ${hold.ticketId}\nStatus: Waiting for admin approval. Notify the user to approve this ticket before retrying.`,
      },
    ],
  };
}

💡 Developer Infrastructure Note: If you need pre-built deterministic allowlists without maintaining custom middleware, check out SadaSend. It provides per-API-key recipient allowlists, automated approval holds, and a hosted MCP server specifically for AI agents.

── more in #ai-agents 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/multi-agent-safety-d…] indexed:0 read:3min 2026-11-09 · —