Safety: stage → confirm
Worker-facing tools use a two-call gate so a human approves exactly what sends, to whom.
Worker-facing tools — Alerts, Messages, and Scheduled Reminders — reach real people's phones. The stage → confirm model makes sure a human sees exactly what will go out, and to whom, before anything is dispatched.
The two-call flow
stage_alert(text_en: "…")
→ { confirmation_token, expires_at, preview: { recipient_count, rate_limit, … } }
← human reviews the preview
send_alert(confirmation_token)
→ { alert_id, recipient_count }
The same pattern applies to Messages (stage_message / send_message) and Scheduled Reminders (stage_scheduled_message / schedule_message). Always show the preview to the user before calling send_* / schedule_*.
Why two calls
- Content can't change after approval.
stage_*stores the full payload server-side;send_*takes only the token — it can't supply or alter text or targeting. What the human approved is what sends. - Replay protection. The token is single-use. A retry with the same token is refused, so a looping workflow can't double-send.
- The recipient list stays live. The preview is a snapshot for review; recipients are re-resolved at send time from the stored filter. Workers who joined are included; those who left are excluded.
The confirmation token
- A UUID stored server-side (
mcp_pending_actions). - Single-use — the first matching
send_*consumes it. - 15-minute TTL — if approval takes longer, call
stage_*again for a fresh token and current preview. - Agent-scoped — a token from one agent can't be consumed by another.
Alert rate limit
Alerts are capped at one per hour per agent, shared across MCP, the REST API, and the email Virtual Operations Agent. If an Alert went out in the last hour, stage_alert still returns a token but its preview shows rate_limit.allowed: false with a retry window; send_alert is then refused with rate_limited until the window clears. Messages and Scheduled Reminders have no hourly cap.
Errors
Worker-facing errors surface as MCP tool errors (isError: true), not JSON ok objects:
| Error | Meaning |
|---|---|
invalid_or_expired_token | Token not found, expired (>15 min), already used, or from a different agent |
rate_limited | The Alert hourly cap is active |
no_recipients | No workers matched the filter at stage time — refine and re-stage |
bot_not_active | The agent is deactivated; worker-facing tools are refused |
ownership_mismatch | The token's Admin doesn't own the worker question being replied to |
status_mismatch: <status> | The worker question isn't awaiting a reply (e.g. already replied) |
not_found | The user_admin_message_id doesn't exist on this agent |
reply_to_user_message is direct
reply_to_user_message is directReplying to a worker question doesn't use stage → confirm. It takes the user_admin_message_id (from list_pending_user_questions) and sends directly — but the server still re-checks that the replying Admin owns the question and that it's still awaiting a reply before dispatching.
Related
Updated about 1 hour ago

