Skip to content

feat(cdp): refuse a sandbox code send before it is queued - #632

Closed
Silthus wants to merge 6 commits into
sbx-api/integration-nodefrom
sbx-api/sa1-admission
Closed

Silthus wants to merge 6 commits into
sbx-api/integration-nodefrom
sbx-api/sa1-admission

Conversation

@Silthus

@Silthus Silthus commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Part of map #461 (sandbox sender for API sends, slice SA1). Sits on sbx-api/integration-node, a merge of #584 and #587.

Problem

  • A project without a verified domain gets 202 for a code send through the sandbox sender, even when the sandbox skips it after the queue: a non-member recipient, a spent daily allowance, or a paused sender.
  • The caller learns about the skip only from the run log, so an SDK can't tell a sent email from a dropped one.

Merge Danger

Warning

🔴 Merge risk: high · two-way door: a revert brings back the 202 for every sandbox code send

  • Email delivery path. The worker's sandbox SES envelope lists a repeated address once, and the allowance charges it once.
  • Every CDP integration lookup now reads an id the read replica lacks from the primary.
  • The new checks run only for a sandbox integration, which exists only behind workflows-sandbox-sender.

Changes

  • A sandbox code send that the sandbox would skip is refused before it is queued. Nothing is queued on a refusal.
Case Answer
A To, CC or BCC recipient is not a verified member, or a stored copy can't be read 400 sandbox_recipient_not_allowed
The project or recipient allowance is spent 400 sandbox_limit_reached
The sandbox is switched off or paused, or the organization is blocked 422 sending_suspended
A check can't run 503 sandbox_check_failed
  • The allowance check only reads the buckets, so a burst past what is left still gets 202, and the worker skips the extra sends.
  • The 202 answer carries delivery_mode (own or sandbox), and a replay answers with the first send's mode.
  • A refusal emits workflows sandbox email blocked with source: api, and waits at most a second for it.

How did you test this code?

Test seams: the cdp-api code-send route over HTTP (cdp-api.serial.test.ts), the worker's executeSendEmail, and IntegrationManagerService.get.

Proofs:

CI: this PR sits on an integration base until the sandbox stack (through #587) and the send stacks (through #584 and #585) reach master.

Release status

  • This change is behind a feature flag and is not available to users

Docs update

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5)

Skills: /tdd, /writing-tests, /qa-swarm, /pr-description, /writing-pr-descriptions

Question Answer Who Where
Which codes do the recipient and allowance refusals use? 400 sandbox_recipient_not_allowed and 400 sandbox_limit_reached 👤 Human map #461, 2026-10-26
Are the allowances refilling buckets? Yes, accepting about twice the cap on the first day and one recipient bucket per organization 👤 Human map #461, 2026-10-26
What answers a suspended sandbox? 422 sending_suspended, the master code for a suspension 🤖 Agent this PR
Where does a sender the replica lacks get found? In the integration loader, for admission and the worker, not per caller 🤖 Agent this PR

Involvement: one design pass, a 7-lens security swarm, a 4-lens fix round and one clean final read (GPT-6.1 Sol and Claude Opus 5.5); live-run, cross-version and shadow-diff proof captured.

More context for AI agents
  • Session: lane sbx-api of map Map: send code-managed email templates with one PostHog call #461, Claude Code.
  • Follow-ups: a template send by id with a stored sandbox sender ignores a caller's reply_to; a suppressed copy can make the allowance check refuse a send the worker would deliver; the worker's own captureBlocked calls, from the sandbox stack, still wait with no deadline.
  • Read closely: sandbox-send-gate.ts, the envelope change in email.service.ts, and integration-manager.service.ts.

🤖 Generated with Claude Code

Silthus and others added 3 commits October 31, 2026 17:20
The cdp-api send route now runs the sandbox sender's gates for a send
that resolves to the sandbox integration: sender switch and tenant
pause, organization membership over to, cc and bcc, organization block,
and a read-only peek at the daily allowances. A refusal answers before
the send is queued, and the worker keeps its own checks.

The worker's SES envelope now lists each address once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The send route now says whether an accepted send goes out through the
project's own sender or the sandbox sender. The idempotency record keeps
that answer, so a replay reports the mode of the first send even when
the project's sender changed since.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stored sender is read on its own, so a stored cc or bcc that isn't
text no longer hides a sandbox sender from the admission check. Worker
sandbox events name an API send by its transactional email event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus and others added 3 commits October 31, 2026 19:07
A sandbox code send provisions its integration and queues the send in one
request, so the first lookup can reach the read replica before the row does.
The loader cached that miss, the admission gate took the send for an own
sender, and the worker could fail it as a missing integration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The blocked event's first-in-window check uses a Redis client with no
command deadline, so a store that stops answering held the refusal until
the caller timed out. The gate now waits at most a second for it.

The checks stage's metric help now names the sandbox checks it runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e primary

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant