{"slug": "multi-agent-safety-designing-recipient-allowlists-and-human-in-the-loop-approval", "title": "Multi-Agent Safety: Designing Recipient Allowlists and Human-in-the-Loop Approval Holds in TypeScript", "summary": "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.", "body_md": "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.\n\nGiving 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.\n\nPrompt 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.\n\nLet's look at how to build deterministic **Safety Envelopes**: **Recipient Allowlists** and **Approval Holds**.\n\nIn production systems, your safety guardrails must exist in code outside the LLM context window:\n\n```\n[Agent Reasoner] ──> [Tool Request] ──> [Deterministic Gate] ──> [Real World API]\n                                               │\n                                      (Violates Allowlist?)\n                                               │\n                                               ▼\n                                      [Hold for Approval]\n```\n\nHere is a TypeScript utility that verifies target recipients against exact matches, wildcard patterns, and corporate domains before any network packet is dispatched:\n\n```\nexport class RecipientAllowlistGate {\n  private allowedDomains: Set<string>;\n  private explicitAddresses: Set<string>;\n\n  constructor(domains: string[], exactAddresses: string[] = []) {\n    this.allowedDomains = new Set(domains.map((d) => d.toLowerCase().replace(/^@/, \"\")));\n    this.explicitAddresses = new Set(exactAddresses.map((a) => a.toLowerCase()));\n  }\n\n  public isAllowed(recipientEmail: string): boolean {\n    const cleanEmail = recipientEmail.trim().toLowerCase();\n\n    // Check 1: Explicit address match\n    if (this.explicitAddresses.has(cleanEmail)) {\n      return true;\n    }\n\n    // Check 2: Domain match\n    const domain = cleanEmail.split(\"@\")[1];\n    if (domain && this.allowedDomains.has(domain)) {\n      return true;\n    }\n\n    return false;\n  }\n\n  public assertAllowed(recipientEmail: string): void {\n    if (!this.isAllowed(recipientEmail)) {\n      throw new Error(\n        `🛡️ Security Guardrail: Outbound dispatch to '${recipientEmail}' blocked. Recipient is not in the authorized company allowlist.`\n      );\n    }\n  }\n}\n```\n\nWhat happens when an agent genuinely needs to message an external party or execute a sensitive mutation?\n\nRather than failing outright or sending without confirmation, the tool creates an **Approval Hold** and returns a hold ticket to the agent:\n\n```\nexport interface HoldTicket {\n  ticketId: string;\n  action: string;\n  payload: Record<string, unknown>;\n  createdAt: string;\n  status: \"pending_approval\" | \"approved\" | \"rejected\";\n}\n\nexport class ApprovalHoldRegistry {\n  private tickets = new Map<string, HoldTicket>();\n\n  public createHold(action: string, payload: Record<string, unknown>): HoldTicket {\n    const ticketId = `hold_${Math.random().toString(36).substring(2, 10)}`;\n    const ticket: HoldTicket = {\n      ticketId,\n      action,\n      payload,\n      createdAt: new Date().toISOString(),\n      status: \"pending_approval\",\n    };\n\n    this.tickets.set(ticketId, ticket);\n    return ticket;\n  }\n\n  public approve(ticketId: string): HoldTicket {\n    const ticket = this.tickets.get(ticketId);\n    if (!ticket) throw new Error(\"Ticket not found\");\n    ticket.status = \"approved\";\n    return ticket;\n  }\n}\n```\n\nWhen the agent attempts to execute an outbound side-effect:\n\n``` js\nconst gate = new RecipientAllowlistGate([\"mycompany.com\", \"partnerdev.io\"]);\nconst holds = new ApprovalHoldRegistry();\n\nasync function executeOutboundTool(to: string, subject: string, body: string) {\n  // 1. If recipient is internal / authorized, dispatch immediately\n  if (gate.isAllowed(to)) {\n    return await mailer.send({ to, subject, body });\n  }\n\n  // 2. If external, stage an Approval Hold\n  const hold = holds.createHold(\"send_email\", { to, subject, body });\n\n  return {\n    isError: false,\n    content: [\n      {\n        type: \"text\",\n        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.`,\n      },\n    ],\n  };\n}\n```\n\n💡 **Developer Infrastructure Note**: If you need pre-built deterministic allowlists without maintaining custom middleware, check out [SadaSend](https://www.sadasend.com/?utm_source=dev.to&utm_medium=article&utm_campaign=dan_kestrel&utm_content=multi_agent_safety_allowlists). It provides per-API-key recipient allowlists, automated approval holds, and a hosted MCP server specifically for AI agents.", "url": "https://wpnews.pro/news/multi-agent-safety-designing-recipient-allowlists-and-human-in-the-loop-approval", "canonical_source": "https://dev.to/dev_kestrel/multi-agent-safety-designing-recipient-allowlists-and-human-in-the-loop-approval-holds-in-52c1", "published_at": "2026-11-09 19:40:59+00:00", "updated_at": "2026-11-09 19:55:17.088109+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/multi-agent-safety-designing-recipient-allowlists-and-human-in-the-loop-approval", "markdown": "https://wpnews.pro/news/multi-agent-safety-designing-recipient-allowlists-and-human-in-the-loop-approval.md", "text": "https://wpnews.pro/news/multi-agent-safety-designing-recipient-allowlists-and-human-in-the-loop-approval.txt", "jsonld": "https://wpnews.pro/news/multi-agent-safety-designing-recipient-allowlists-and-human-in-the-loop-approval.jsonld"}}