Add production email inbox for remote-target QA (Resend inbound)#33
Open
BLamy wants to merge 2 commits into
Open
Add production email inbox for remote-target QA (Resend inbound)#33BLamy wants to merge 2 commits into
BLamy wants to merge 2 commits into
Conversation
Remote (non-emulated) targets previously had no way to complete email signup, magic-link, or OTP flows — agents hit the email wall and flagged login_failed. This is the remote counterpart of the emulated Resend outbox: each project gets a deterministic catch-all address at RESEND_INBOX_DOMAIN, and browsing tasks (exploration, unstepped QA, version-added) get a prompt block teaching the agent to sign up with it, poll GET /api/inbox for the verification email, extract the link/code, log in, and save returningUserEmail for future runs. - lib/inbox.ts: address derivation (full project id, 64-char RFC 5321 cap + hash; the address filter is the only isolation between projects on the shared domain), server-side Resend receiving client with pagination/body-fetch caps, and the agent prompt block with security guardrails (email content is untrusted; only open target-host links). - functions/inbox.ts: GET /api/inbox, auth via requireProjectAccess (NOT requireProjectReadAccess — public-project viewers must not see live magic links). The Resend key never reaches QA containers. - scripts/check-inbox.ts: pure-function regression coverage, wired into npm run check. Feature is off until RESEND_INBOX_DOMAIN is set (endpoint 503s, no prompt block). Ops: create the receiving domain + MX records in Resend, add the var to Infisical (already in RUNTIME_ENV_VARS). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🔎 Loop QA preview (all-k8s backend): https://pr-33--loop-qa-j63k5x.netlify.app |
Replace the Netlify-env-only config with runtime resolution from
Infisical, matching how the rest of the system handles secrets: the
same getInfisicalConfig/fetchBranchSecrets machinery the container-
spawn flow uses, and the same pseudo-branch pattern as the GitHub App
private key (lib/github-app/private-key.ts).
- resolveInboxConfig(): Infisical /branches/qa-inbox/{RESEND_API_KEY,
RESEND_INBOX_DOMAIN} win per-field; env vars are the fallback (local
dev, and prod's existing sending key until a receiving-scoped key is
provisioned). Cached per instance; misses retry after 60s so the
task webhook doesn't hammer Infisical while unconfigured.
- The pseudo-branch (not /global/) is deliberate: pod harnesses load
/global/ secrets into their in-pod secrets server, so a global key
would be loadable inside every task pod. qa-inbox never matches a
pod's push branch, so pods can't even list it. Only the derived
inbox ADDRESS ever enters a prompt.
- Drop RESEND_INBOX_DOMAIN from RUNTIME_ENV_VARS — nothing
inbox-specific lives in Netlify env anymore; enabling the feature is
now purely an Infisical write (picked up within 60s, no deploy).
- inboxAddressForProject/buildProductionInboxPrompt take the resolved
domain/address explicitly; webhook resolves once per task claim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Gives the QA agent a real, pollable email inbox when testing remote/production
target_urls — the counterpart of the emulated Resend outbox. Agents can now complete email signup, magic-link, and OTP flows on production apps instead of flagginglogin_failedat the email wall.Adapted from Resend's agent email inbox skill, but using API polling (
GET /emails/receiving) instead of webhooks — no public webhook endpoint needed.How it works
qa-<project-id-slug>@<receiving domain>(no provisioning; plus-variants like+2@route to the same inbox for fresh-account flows).GET /api/inboxwith aSINCE-anchored curl loop, extract the link/code, log in, then savereturningUserEmailas a global variable so later runs can log back in.GET /api/inbox?project=<id>[&search=][&since=][&limit=](new Netlify function) calls Resend server-side and returns only mail addressed to that project — same JSON shape as the emulated outbox helper.Secrets: Infisical at runtime, nothing in pods or Netlify env
Inbox secrets live in the Infisical pseudo-branch
/branches/qa-inbox/(RESEND_INBOX_DOMAIN, optionally a receiving-scopedRESEND_API_KEY) and are resolved at runtime by the Netlify functions (resolveInboxConfiginlib/inbox.ts) — the samegetInfisicalConfig/fetchBranchSecretsmachinery the container-spawn flow uses, and the same pseudo-branch pattern as the GitHub App private key (lib/github-app/private-key.ts).Why a pseudo-branch and not
/global/: pod harnesses load/global/secrets into their in-pod secrets server (secrets-server.tsin app-building-loopqa), so a global key would be loadable inside every task pod.qa-inboxnever matches a pod's push branch, so pods can't even list it. The key never reaches pods by construction — only the derived inbox address enters any prompt; the agent reads mail through/api/inboxwith its task-id cookie.Why not an in-pod secret-allowlist verb: pods are pooled and project-agnostic, so a verb's project scope would come from agent-supplied args — any agent could read any project's mail. The endpoint derives the project from the authenticated task server-side (same reasoning as the deliberately-removed
LOOPQA_ADMIN_TOKENextraction verb, seeSECRET_ALLOWLISTinlib/spawn.ts).Other security properties:
requireProjectAccess, deliberately notrequireProjectReadAccess— public-project viewers must not see live magic links.proj-<name>-<ts36>, unique only in the tail).Rollout (feature is OFF until configured — no deploy needed to enable)
inbox.replay.io) and publish its MX records — a Resend-managed<id>.resend.appdomain works for interim testing./branches/qa-inbox/and addRESEND_INBOX_DOMAIN(+ optionally a receiving-scopedRESEND_API_KEY; the Netlify-env sending key is the fallback). Picked up within 60s via the miss-retry cache.Local dev uses the
RESEND_INBOX_DOMAIN/RESEND_API_KEYenv vars (no Infisical creds needed).Testing
scripts/check-inbox.ts(wired intonpm run check): config resolution/caching/normalization (env-fallback path), address determinism/collision-resistance/64-char cap, plus-suffix matching,Name <addr>parsing, since-window filtering, prompt guardrail content (incl. key-never-in-prompt), malformed-URL fallback.tsc --noEmit,check-emulatable-runtime, and eslint on the changed files all pass. (Note:npx eslint .currently fails on main'sscripts/lib/replay-node-preload.cjs— pre-existing, untouched here.)qa-inboxfetch in a deployed function, theafterpagination direction, and thetofield shape (code parses both plain andName <addr>forms). Worth one manualcurl /api/inboxonce the domain + secrets are configured — debugging checklist inskills/production-email-inbox.md.🤖 Generated with Claude Code