Repository navigation
Conversation
Silthus
force-pushed
the
workflows-sandbox/recipients
branch
from
October 12, 2026 21:36
cd93c20 to
8c0f07f
Compare
Silthus
force-pushed
the
workflows-sandbox/recipients
branch
2 times, most recently
from
October 19, 2026 10:58
fb0ae20 to
fe5078a
Compare
This was referenced Oct 19, 2026
Silthus
force-pushed
the
workflows-sandbox/recipients
branch
3 times, most recently
from
October 23, 2026 16:26
58c7c93 to
ea5c03c
Compare
This was referenced Oct 23, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus
force-pushed
the
workflows-sandbox/recipients
branch
from
October 24, 2026 13:12
ea5c03c to
30fb28b
Compare
Owner
Author
|
Note 🤖 Claude Opus 5.5 responding on behalf of Michael Decision lines for this PR. The Decisions table in the description links each row here.
|
19 tasks
A deactivated organization now has no verified members, so the sandbox sender blocks every send within the one-minute member snapshot window. Only an explicit false deactivates, matching the web middleware. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 26, 2026
Owner
Author
|
Replaced by the re-sliced fork stack: #594. Tracked in Silthus/personal#71. |
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.
Problem
A team that has not verified its own domain sends from PostHog's shared sandbox sender. Without a recipient check, a workflow could send that email to anyone, such as customers or an imported list. Complaints from those recipients would lower the reputation of the shared sender for every team that uses it.
The sandbox sender is for test email to the team itself. So every recipient, including CC and BCC, must be a verified member of the project's organization.
Staff can deactivate an organization in Django admin. Until now that locked only the app and the API: events still arrived, and a live workflow kept sending sandbox email. Deactivation now also stops sandbox email, which gives a per-organization kill switch with an existing admin tool.
Supersedes PostHog#111483 (left open as the original draft)
Stack: layer 4 of 12, stacked on #430. Next: #441.
This fork PR moves to an upstream PostHog/posthog PR after review.
Caution
🔴 Merge risk: high · one-way door (merge-risk rubric)
falseas deactivated. A NULLis_activestill sends, so organizations that were never deactivated keep their sandbox email.Changes
Organization.is_active = false) has no verified members, so the sandbox sender blocks every send within the one-minute member snapshot. Only an explicitfalsedeactivates, as in the web middleware; NULL counts as active. Own-sender email is unchanged.recipient_unverified, also a newreasonvalue on theworkflows sandbox email blockedevent.Before:
flowchart LR Send[Sandbox email] --> Route[Fixed identity] --> SES[SES delivery] classDef phYellow fill:#f9bd2b,stroke:#f9bd2b,color:#000; classDef phBlue fill:#1d4aff,stroke:#1d4aff,color:#fff; classDef phRed fill:#f54e00,stroke:#f54e00,color:#fff; class Send phYellow; class Route phBlue; class SES phRed;After:
flowchart LR Send[Sandbox email] --> Members[Check all recipients] --> Route[Normalize identity and content] --> Recheck[Recheck membership] --> SES[SES delivery] Members --> Block[Explain blocked addresses] Recheck --> Block classDef phYellow fill:#f9bd2b,stroke:#f9bd2b,color:#000; classDef phBlue fill:#1d4aff,stroke:#1d4aff,color:#fff; classDef phRed fill:#f54e00,stroke:#f54e00,color:#fff; class Send,Block phYellow; class Members,Route,Recheck phBlue; class SES phRed;Known follow-ups (found in the UI walk, not fixed here):
workflows sandbox email blockedevent reportsrecipient_not_member. So the team's run log and any Security review of blocked events cannot tell a deactivated organization from an outsider. A distinct reason, such asorganization_deactivated, is a follow-up.How did you test this code?
Current evidence, 2026-10-26, head
222ac4e5617.Lane gate: Node
tscclean; prettier, eslint and oxlint clean on the 2 changed files;email.service.test.tsandorganization-members.service.test.ts: 204 passed at this head, 236 passed at the restacked tip8418420305e. The strict pre-push preflight reported no failures on every pushed layer. Fork CI ran only its label and sync checks; application CI does not run on the fork, so it is a gap.Owed proofs (
fleet proof gate: proof, owed and quality pass; review fails, see Open below):is_activeNULL sends. Once deactivated, a new worker sends nothing, and a worker that loaded members earlier stops after the one-minute snapshot (it still sent once inside the window). Own-sender email from the deactivated organization still goes out, and reactivating sends again.f279cc92971against this head over the same 14 sends: 8 are identical, and the 6 that differ are each a send this PR blocks (outsiders, an unverified member, a deactivated organization). A second run over every organization in the dev database compared the old and new member queries. The only lost row belonged to the deactivated organization, and a NULL organization kept all its members.is_activetrue, NULL or false needs read-only production access, which arrives on 2026-11-02 (accepted by standing rule).Gaps: real AWS SES delivery (every run uses a local SES fake), clustered Valkey, the production data shape, and application CI on the fork.
Test seams for the deactivation check:
EmailService.executeSendEmailagainst the test Postgres: the "blocks all delivery for %s / %s" table gains a deactivated-organization row (red before the fix: one SES call), and "sends to a verified member of an organization that was never deactivated" coversis_activeNULL andtrue.Evidence at head
30fb28b0ec9, 2026-10-24Evidence at head
30fb28b0ec9, 2026-10-24.Owed proofs:
8ac8853cc82, which contains this layer. The stack's cdp-api sent through the workflow test-run endpoint to a local SES fake that records each request. 9 of 9 cases pass. 12 of 13 recorded cases match the run atfa33d78fb36. Case 2c (an unverified member on To) now shows the verify-your-email guidance with reasonrecipient_unverified, and makes no SES request. For this layer: cases 2a, 2b and 2d (an outsider on To, a non-member on CC, a non-member on BCC) skip with members-only guidance, and case 2c (an unverified member) skips with the verify-your-email guidance. None makes an SES request. Case 1 delivers to a verified member.fa33d78fb36, because round 4 does not touch the own-sender path or the base worker. Own-sender SES requests from fork master448d802and that tip are identical across 7 cases (stored flows, editor drafts, a custom From name, unverified and missing senders), apart from the per-run signatures of tracking tokens. One intended difference: an editor test run with an empty recipient reportsskippedinstead ofsuccess, because feat(workflows): test templates with the sandbox sender #451 reports skipped email steps in test runs. Live sends are unchanged.Changes in this layer since the 2026-10-19 heads (restacked onto fork master
448d802:cdp-services.tsimports only):skips a sandbox send to a person without an email address as a missing recipient).'(empty address)'fallback in the blocked-address log is removed. The existing suites cover it.recipient_unverifiedand the message asks the user to verify their email address, not a domain (email.service.test.ts"blocks all delivery for %s / %s", unverified and legacy cases, red with 2 failed, then green).Local gate at the stack tip
8ac8853cc82, shared by every layertscand frontendtsgo: clean.Test seams: membership lookup public methods and EmailService delivery outcomes, SES commands, run logs and telemetry.
Test rationale: Existing membership cases now use an injected clock and cover cached organization lookups. Reduced matrices retain one explicit test-send case per gate; hourly event cases verify bounded analytics.
Gaps: real AWS SES delivery and the AWS-side tenant pause (every proof uses a local SES fake or an unreachable SES), clustered Valkey, the production data shape (prod-shape gap until production read access on 2026-11-02), and fork CI, which fails during setup: path selection fails without a GitHub App client ID, and the Security aggregate rejects its fork-skipped path-filter job. Application CI is a verification gap.
Earlier evidence (superseded heads)
Round 3 heads, 2026-10-23. This layer's head:
ea5c03c1ad3. Stack tipfa33d78fb36.fa33d78fb36, which contains this layer. The stack's cdp-api sent through the workflow test-run endpoint to a local SES fake that records each request. 9 of 9 cases pass, identical to the earlier run. For this layer: cases 2a to 2d (an outsider on To, a non-member on CC, an unverified member, a non-member on BCC) each skip with members-only guidance and no SES request. Case 1 delivers to a verified member.fa33d78fb36: Python 3007 passed; Node CDP 42 suites, 1157 passed; email service suites at the final heads of feat(workflows): restrict sandbox email to verified org members #434, feat(workflows): cap sandbox sends and refund ses throttles #441, feat(workflows): test templates with the sandbox sender #451 and feat(workflows): honor shared sandbox tenant pauses #453 passed; frontend Jest 17 changed suites, 290 passed; mypy, Nodetscand frontendtsgoclean; ruff, oxfmt, oxlint, prettier and eslint clean on the files changed since round 2. Final read 1 found nothing.Whole-stack QA swarm, 2026-10-19. Reviewed tip
cfee7e148ae. This layer's head:58c7c938739.b9bd346ed0e(this stack's cdp-api against a local SES fake, workflow test-run endpoint): a sandbox send to a verified member went out from the fixed identity through the sandbox tenant and configuration set. The step's From name, From address and Reply-To were ignored with a log line. A non-member on CC and an unverified member were blocked with the clear message. A paused tenant state skipped sandbox sends while own-sender sends continued.1a0fe3663c2; later fixes touch no Node code): Node CDP messaging, members and rate limiter suites (includingemail-tracking.service.test.ts, which now runs), the cdp-api suite, the Python sandbox, tenant, tier, reputation and validation suites, the frontend Jest suites of this stack, ruff, and the strict pre-push preflight: pass.workflows sandbox email blockedevent (the hourly sample key includes the send kind). The extended dedupe test failed before the fix.Rebased onto master
734468db6f9(2026-10-19), headfe5078a82f5.paramsWithReplyTo) to the step parameters.deliveryParams. Sandbox sends still drop Reply-To inwithIdentificationFooter.email-tracking.service.test.tsdid not run then. It runs now; see the QA swarm block above. Fork CI still fails during setup, as noted below. Real AWS delivery and clustered Valkey remain unchecked.Checks at the stack tip
Stack tip
b9bd346ed0e(#456), shared by every layer:tsgo: no errors. Nodetsgo: no errors in CDP. The 23 errors it reports are all in session replay files that need the unbuilt Rust@posthog/replay-anonymizermodule.product:lint(workflows, cdp), IDOR coverage,hogli build:openapi(no diff), and the strict pre-push preflight: pass.Observed round-two snapshot verification
Observed targeted verification after QA fixes
Observed combined-stack gate
👉 Stay up-to-date with PostHog coding conventions for a smoother review.
Release status
Docs update
Provisioning uses
workflows-sandbox-sender; delivery uses the default-off worker switchWORKFLOWS_SANDBOX_SENDER_ENABLED.Keep provisioning and delivery switches off until daily caps and sandbox tenant pause enforcement are deployed.
The workflow email sending tiers document describes eligible recipients, bounded membership snapshots, and blocked-address logs.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Decisions
is_active = false) stops its sandbox emailis_activevalues deactivatefalse; NULL counts as active, as in the web middlewarerecipient_not_memberpath, one-line change; a distinct reason is a known follow-upaddressparser, simpler block logs, and the email service split. Review found no behavior defect in the current gaterecipient_unverifiedand its message says to verify the email address. The worker does not know who sent the test, so it addresses both the sender and the membersOpen:
fleet proof gatefails its review check because round 1 findings G2-4 (singleton test matrices, an unreachable cleanup branch) and G2-12 (the organization-refresh test asserts query calls) are deferred, not fixed. Recommendation: fix both as a test-only change in this layer and restack, since later layers leave those tests unchanged. This was held back because the deactivation change was asked to stay narrow.Involvement: 2026-10-26 deactivation change with review round 3 by GPT-6.1 Sol (no findings), a live run at this head and a base/head shadow diff, then a restack of #441 to #456; earlier: restack onto fork master
448d802on 2026-10-23; round 1 with GPT-6.1 Sol lanes plus Claude Opus 5.5 fresh eyes for group G2; round 2 on the fix diff; final read 1 by GPT-6.1 Sol per group, with no findings; proofs captured; final read 2 by GPT-6.1 Sol on the round 4 decisions, with no findings; the round 4 live run and a UI walk of the two changed surfaces. Before that: the 2026-10-19 whole-stack QA swarm and the modernize rebase (see Earlier evidence). No review bots run on the fork.Agent: Codex with GPT-6.1 Sol (gpt-6.1-sol) built the original layers, orchestrated by Claude Code with Claude Opus 5.5 (claude-opus-5-5). Restack, review fixes, the deactivation check and this description refresh: Claude Opus 5.5 (claude-opus-5-5). Review: GPT-6.1 Sol (gpt-6.1-sol). Fresh eyes: Claude Opus 5.5 (claude-opus-5-5).
Skills:
tdd,pr,stacking-prs,writing-pr-descriptions,merge-risk,writing-tests,writing-code-comments,running-ci-preflight, andwriting-voice. The description refresh usedpr-description,writing-pr-descriptions,writing-voiceandmerge-risk.The rebuild preserves master's UTM tagging and throttle retries. The UTM payload now retains recipient normalization introduced by this layer. Public repository sources and invented test addresses informed the change.
🤖 Generated with Claude Code