Phase 4 completion: per-tenant sending identity, worker concurrency, exit-gate ledger#12
Conversation
…exit-gate ledger Finishes the code-buildable remainder of Phase 4; the exit-gate ledger (docs/phase4-readiness.md) records what is pinned by tests versus what needs an operator decision. Per-tenant sending identity (mailbox/domain ownership seam): - tenants.sender_from_address: each client sends AS its own provider-verified address; NULL falls back to the global RELAY_SES_FROM. The worker resolves the tenant's identity inside the job transaction and hands it to the sender; the sender protocol gains an optional sender_identity parameter (simulated sender accepts and ignores it). Identity VERIFICATION remains a §6 operator attest - this is the mechanical isolation seam. Onboarding accepts the address. Worker concurrency (throughput scaling): - process_pending(concurrency=N) / relay-worker --concurrency N: tenants are drained in parallel threads. Tenants are independent work streams - each job runs in its own transaction under its tenant's RLS context, claims are FOR UPDATE SKIP LOCKED, and the real-send caps serialize per tenant on an advisory lock - so parallelism changes throughput, not semantics. Per-tenant FIFO and pacing deferral behavior are unchanged; stats merge across streams. Default stays 1 (the n8n tick is untouched). Exit-gate ledger (docs/phase4-readiness.md): - onboarding without config edits: done, pinned. - two tenants simultaneously with verified DATA and SENDING isolation: done, pinned (RLS row sets exact; per-tenant From at the provider). - per-client cost/profitability visible: done (GET /economics). - sustained throughput: mechanism done and pinned; the TARGET is the operator's to set, then benchmark (single-GPU compute ceiling is the expected bottleneck per §17, not the datastore). Tests (+2, suite now 314): a tenant with its own from-address sends as that identity while another tenant uses the global one (sending isolation at the provider boundary), and the concurrent worker drains a mixed two-tenant queue in one pass with exact isolation afterwards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ASAVj4XgJCH3UcHkZZaYzM
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (14)
📝 WalkthroughWalkthroughThis PR adds per-tenant sending identity with provider attestation, an eligibility gate blocking unverified sends, sender contract changes (DirectSender, SES, Simulated) to accept ChangesSender identity and worker concurrency
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Admin
participant Routes as routes.py
participant DB as Tenant DB
participant Worker as send_worker
participant Sender as SESSender
Admin->>Routes: onboard tenant (sender_from_address)
Routes->>DB: create Tenant
Admin->>Routes: attest-sender-identity
Routes->>DB: set sender_identity_verified=true
Routes-->>Admin: TenantSenderAttestResponse
Worker->>DB: discover tenants with queued jobs
Worker->>DB: resolve sender_identity per tenant
Worker->>Sender: send(sender_identity)
Sender-->>Worker: message_id
Worker->>DB: record audit event with sender_identity
Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… identity, attest gate
Fixes from an adversarial self-review of the Phase 4 completion diff
(three finder agents, all findings verified against the code):
- max_jobs is a GLOBAL pass budget again. The concurrency refactor had
silently made it per-tenant, so a tick with N tenants could process
N*100 jobs in one synchronous HTTP request. The budget is now a
thread-safe shared counter across tenant streams; an unused slot is
returned when a stream runs dry. Pinned by a new test (2 tenants,
4 jobs, max_jobs=3 -> exactly 3 processed).
- Sender registry construction is locked (double-checked). Concurrent
worker threads could both construct the boto3 client on the shared
botocore session (not thread-safe for client creation) - and a crash
there landed AFTER the job claim, terminally failing jobs over a
startup race.
- Worker concurrency is clamped to 8 (_MAX_WORKER_CONCURRENCY): the
app engine runs SQLAlchemy's default pool (5+10=15 connections);
oversubscribing it makes threads block on checkout, time out, and
silently skip tenants. Errors count now appears in the pass log.
- Tenant identity resolution FAILS CLOSED and moved out of the per-job
hot path: resolved once per tenant per pass; if the tenant row is
unreadable the tenant is skipped with a recorded error - mail is
never sent under the wrong (global) identity, and simulated-mode
jobs no longer pay a per-job SELECT for a value they ignore.
- Unverified tenant identity can no longer terminal-fail leads: new
tenants.sender_identity_verified attest (admin endpoint
/internal/tenants/{id}/attest-sender-identity, audited) + eligibility
check tenant_sender_identity_verified. A tenant sending as its own
address is blocked at the gate until the operator attests the
identity is provider-verified - the provider never sees an
unverified From, so an onboarding typo cannot park approved leads in
error_terminal.
- send.executed audit payload records the sender identity used (None =
global), so a compliance incident can answer "which address did job
X send as" from the append-only trail.
- Cleanups from the same review: WorkerStats.merge() lives on the
dataclass, one mapper path with the worker count computed once and
logged truthfully, TenantOnboardResponse fields are fail-loud
(no accidental optional).
Tests (+3, suite now 317): global pass budget across tenants,
unverified identity blocked before the provider with the check named
in the reason, attest endpoint flow (false at onboarding -> 200 attest
-> 409 nothing-to-attest -> 404 unknown tenant).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASAVj4XgJCH3UcHkZZaYzM
Why
Finishes the code-buildable remainder of Phase 4 — Productization & Scale. With onboarding, quotas/spend controls, and cost attribution merged (#11), the two remaining mechanical items were the per-tenant mailbox/domain isolation model and concurrency scaling of the worker. The new
docs/phase4-readiness.mdis the exit-gate ledger: what's pinned by tests versus what needs an operator decision.What changed
Per-tenant sending identity (mailbox/domain ownership seam)
tenants.sender_from_address: each client sends as its own provider-verified address; NULL falls back to the globalRELAY_SES_FROM(so the sandbox pilot is untouched).sender_identityparameter. Onboarding accepts the address.tenants.sender_identity_verifiedattest (second commit): a tenant sending as its own address is blocked at eligibility (tenant_sender_identity_verified) until the operator attests the identity is provider-verified viaPOST /internal/tenants/{id}/attest-sender-identity(audited). Without this, a typo'd onboarding address would reach SES, get rejected, and terminally fail every approved lead.send.executedaudit event records the identity each send actually used.Worker concurrency (throughput scaling)
process_pending(concurrency=N)/relay-worker --concurrency N: tenants drain in parallel threads. Safe by construction — tenants are independent work streams (per-job transactions under RLS,FOR UPDATE SKIP LOCKEDclaims, per-tenant advisory-lock cap serialization).max_jobsremains the GLOBAL pass budget (second commit): shared across tenant streams via a thread-safe counter, so a tick is bounded the same with one tenant or fifty.Exit-gate ledger —
docs/phase4-readiness.mdFromobserved at the provider)GET /economics)--concurrency)Review
The second commit is the result of an adversarial multi-agent self-review of the first (8 finder angles, all findings verified then fixed): global pass budget, registry lock, concurrency clamp, fail-closed identity resolution, the identity-verification attest gate, audit of the identity used, plus stats-merge/onboarding-schema cleanups.
Tests (+5 across both commits, suite now 317)
max_jobs=3across 2 tenants × 2 jobs processes exactly 3 (global budget proven).Verification
ruffclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01ASAVj4XgJCH3UcHkZZaYzM
Summary by CodeRabbit
New Features
Bug Fixes
Documentation