Ship the railiance01 deploy package (OpenBao/ESO custody, NetworkPolicy, probes), provider failure classification and suppression, T04 unit and live proof, and non-secret NK-WP-0024 hand-back evidence. Workplan finished.
4.8 KiB
EMAIL-WP-0004-T04 — invitation delivery failure evidence
Date: 2026-08-12
Cluster: railiance01
Service: http://email-connect.email-connect.svc.cluster.local:8080
Image: forgejo.coulomb.social/coulomb/email-connect@sha256:a9de1994eea88502ec30ba7c2fc029ec901d873140d51e11d2c50e363a0578b1
Unit tests: PYTHONPATH=src python3 -m pytest tests/test_transactional.py — 9 passed
This note is the non-secret hand-back for NK-WP-0024. No SMTP passwords, ingest tokens, or raw provider exception text are included.
Contract for user-engine
| HTTP | error / status |
retryable |
Meaning for outbox |
|---|---|---|---|
| 401 | unauthorized |
— | Wrong or missing bearer; fail closed |
| 400 | template_not_allowed |
— | Event type/source not on allow-list |
| 400 | invalid_recipient |
— | Address failed local validation |
| 400 | idempotency_key_mismatch |
— | Idempotency-Key ≠ event id |
| 400 | recipient_suppressed |
— | Address on suppression list; do not retry as send |
| 400 | invitation_id_required |
— | Missing invitation id |
| 200 | status=duplicate + reference |
— | Same event id already accepted; safe replay |
| 202 | status=accepted + reference + event_id |
— | Provider accepted message; not inbox delivery |
| 503 | provider_timeout / temporary_deferral / provider_unavailable |
true |
Bounded retry |
| 422 | permanent_rejection |
false |
Do not retry; surface failure evidence |
Evidence ceilings (must not become authorization)
- Invitation accept:
evidence_ceiling=provider_accepted— transport only. - Registration consume/cancel:
authorization=false, ceilingsmailbox_challenge_consumed/mailbox_challenge_canceled. assurance.mailbox_control=trueis channel evidence, not identity proof and not a flex-auth decision.
Mailbox ownership challenges and provider acceptance must never alter user-engine authorization decisions.
Unit matrix (local)
| Case | Result |
|---|---|
| Template allow-list denial | 400 template_not_allowed |
| Invalid recipient | 400 invalid_recipient |
| Idempotency mismatch | 400 idempotency_key_mismatch |
| Auth denial | 401 unauthorized |
| Duplicate event id | 200 duplicate + same reference; provider called once |
Resend (family_invitation.resent) |
New event id → second provider send |
| Suppression | 400 recipient_suppressed; no provider call |
| Provider timeout / temporary / unavailable | 503 + retryable=true, redacted codes |
| Permanent rejection | 422 + retryable=false |
| SMTPRecipientsRefused → permanent | ProviderError mapping unit-tested |
| Redaction | Response JSON has no password/SMTP host/exception text |
| Mailbox evidence ≠ authz | authorization=false on consume/cancel |
Live matrix (railiance01, from user-engine pod)
| Case | HTTP | Body (redacted) |
|---|---|---|
| Unauth | 401 | {"error":"unauthorized"} |
| Template deny | 400 | {"error":"template_not_allowed"} |
| Invalid recipient | 400 | {"error":"invalid_recipient"} |
| Idempotency mismatch | 400 | {"error":"idempotency_key_mismatch"} |
Accept (to forgejo@coulomb.social) |
202 | status=accepted, evidence_ceiling=provider_accepted, event_id set |
| Duplicate same event id | 200 | status=duplicate, same reference |
| Resend new event id | 202 | status=accepted |
Bad domain nobody@invalid.invalid |
422 | {"error":"permanent_rejection","retryable":false} |
Suppressed suppressed@example.test |
400 | {"error":"recipient_suppressed"} (seeded in SQLite for smoke) |
| Redaction | — | No password / smtp.ionos in any body |
Negative path (unchanged from T03): pods outside user-engine cannot reach
TCP 8080 (Connection refused).
Non-secret references for NK-WP-0024
- Cluster-local base URL:
http://email-connect.email-connect.svc.cluster.local:8080 - Paths:
POST /v1/send,POST /v1/registration-verifications,…/consume,…/cancel - Auth: shared ingest bearer (OpenBao
platform/workloads/email-connect/transactionalfieldEMAIL_CONNECT_INGEST_TOKENonly — never SMTP fields in user-engine) - Idempotency: header
Idempotency-Key= eventid - Allowed send types:
family_member.invited,family_invitation.resent - Catalog route:
email-connect-transactional(ops-warden)
Temporary deferral / timeout note
Live temporary deferral depends on provider behavior and was covered by unit
classification (temporary_deferral, provider_timeout → 503 retryable).
Live permanent path was observed via invalid domain → 422 permanent_rejection.
Follow-ups (out of T04)
- Mount ingest token into user-engine runtime for production outbox (NK-WP-0024).
- Operator API to manage suppressions (currently SQLite; smoke seeded manually).
- Daily send-quota tiers deferred (
DECISIONS.md).