diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index d4334c2..52ede77 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -17,6 +17,7 @@ | workplan | EMAIL-WP-0007 | finished | — | workplans/EMAIL-WP-0007-ledger-backup-recovery.md | | workplan | EMAIL-WP-0008 | finished | — | workplans/EMAIL-WP-0008-scoped-transactional-callers.md | | workplan | EMAIL-WP-0009 | finished | — | workplans/EMAIL-WP-0009-wave2-resource-right-sizing.md | +| workplan | EMAIL-WP-0010 | active | — | workplans/EMAIL-WP-0010-platform-sender-transition.md | | task | EMAIL-WP-0001-T01 | done | — | workplans/EMAIL-WP-0001-repo-onboarding.md | | task | EMAIL-WP-0001-T02 | done | — | workplans/EMAIL-WP-0001-repo-onboarding.md | | task | EMAIL-WP-0001-T03 | done | — | workplans/EMAIL-WP-0001-repo-onboarding.md | @@ -63,3 +64,7 @@ | task | EMAIL-WP-0008-T03 | done | — | workplans/EMAIL-WP-0008-scoped-transactional-callers.md | | task | EMAIL-WP-0008-T04 | done | — | workplans/EMAIL-WP-0008-scoped-transactional-callers.md | | task | EMAIL-WP-0009-T01 | done | — | workplans/EMAIL-WP-0009-wave2-resource-right-sizing.md | +| task | EMAIL-WP-0010-T01 | done | — | workplans/EMAIL-WP-0010-platform-sender-transition.md | +| task | EMAIL-WP-0010-T02 | todo | — | workplans/EMAIL-WP-0010-platform-sender-transition.md | +| task | EMAIL-WP-0010-T03 | wait | — | workplans/EMAIL-WP-0010-platform-sender-transition.md | +| task | EMAIL-WP-0010-T04 | todo | — | workplans/EMAIL-WP-0010-platform-sender-transition.md | diff --git a/docs/sender-ledger-native-2026-10-31.json b/docs/sender-ledger-native-2026-10-31.json new file mode 100644 index 0000000..9bfceb2 --- /dev/null +++ b/docs/sender-ledger-native-2026-10-31.json @@ -0,0 +1,64 @@ +{ + "consistent_read_transaction": true, + "ledger_fingerprint": { + "schema_sha256": "13148e5a9c6e01cc593bb07278a8649b13e43b26c30b0ed1a6e15995f7b00732", + "tables": { + "caller_buckets": { + "rows": 1, + "sha256": "14eac0ee940c87217439ff255f96c92c746be1bcd87a76db350647cea2aaddf0" + }, + "deliveries": { + "rows": 2, + "sha256": "a0387716815b2ec191187cb34bb3252b9cf51f5fb15a2019918a9cf1737c061e" + }, + "send_attempts": { + "rows": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "suppressions": { + "rows": 1, + "sha256": "ae26d10dfec16f1b626292b342a5e86afd1858fcaed7c29d4e68e7cf821183d7" + }, + "verification_attempts": { + "rows": 121, + "sha256": "4a20f2865438daaf9ba9192019ebf4d689778560cb7b27fd118fc9cea1ea7f39" + }, + "verifications": { + "rows": 121, + "sha256": "ffaf67ac6665d3d7d9fdd50d5feb57f75c70061d773b689669421f1017454334" + } + }, + "user_version": 0 + }, + "mail_sent": false, + "needs_outcome_reconciliation": false, + "observed_at": "2026-10-31T15:16:41.014728+00:00", + "outcomes": { + "accepted_invitations": 2, + "accepted_verifications": 121, + "canceled_challenges": 0, + "consumed_challenges": 0, + "failed_invitations": 0, + "failed_verifications": 0, + "orphan_verification_attempts": 0, + "suppressed_recipients": 1, + "unknown_invitations": 0, + "unknown_verifications": 0, + "unlinked_verifications": 0, + "unrecognized_attempt_states": 0 + }, + "private_rows_printed": false, + "rollout_authorized": false, + "schema": "email-connect.sender-ledger-observation.v1", + "writers_fenced": false, + "runtime": { + "deployment_uid": "2e98e52a-a2d1-4c52-9ac8-6efbc0a18e72", + "generation": 74, + "image": "forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "sender": "forgejo@coulomb.social", + "deployment_spec_unchanged_during_observation": true, + "transactional_sha256": "36fdfd8cb58f10148cd491a7e34d7aea798e4289fce760af70a48776bede204d", + "packaged_source_matches_checkout": true + }, + "observer_sha256": "262d6d52d5c10fb8583e0828ed34945b647f1d53bf9de5286374fef85cf76a8d" +} diff --git a/docs/sender-transition-boundary-2026-10-31.md b/docs/sender-transition-boundary-2026-10-31.md new file mode 100644 index 0000000..47feaa1 --- /dev/null +++ b/docs/sender-transition-boundary-2026-10-31.md @@ -0,0 +1,68 @@ +# Sender transition: ledger and shutdown boundary + +The current receiver remains generation 74/image 5d79fd1f, using +forgejo@coulomb.social. The platform sender's credential foundation is prepared +under RPF-WP-0068 but unadmitted. EMAIL-WP-0010 now owns the separate configuration +executor, attended switch and native return, supporting CSOC-WP-0015-T02. + +At 15:16:41Z, `inspect-sender-ledger.py` read the native ledger in one read-only +SQLite transaction. All six tables pass integrity/fingerprint checks, including +committed WAL. Aggregate results: 2 accepted invitations, 121 accepted verification +attempts, 1 suppression, no unknown outcomes, orphan attempts, unrecognized states +or unlinked verification rows. No private row values were printed. Evidence: +`sender-ledger-native-2026-10-31.json`. The receipt binds the exact observer source +and native transactional source, which matches the checkout. The deployment spec +was unchanged across observation; no resource or sender change was made. + +## What the observation means + +The successful invitation path writes its provider receipt into deliveries but +leaves the corresponding send_attempts row in_flight. That row by itself is not +evidence of uncertainty: `delivery_status` and `reserve_send` check deliveries +first. Verification sends instead explicitly update their attempt to accepted. +The observer follows both conventions and reports only counts and hashes. + +The source uses `wsgiref.simple_server` without an explicit SIGTERM handler. The +Deployment has 15 seconds termination grace, but that setting does not establish +application-level draining. Termination between SMTP acceptance and ledger commit +can retain an unknown outcome. Existing source refuses blind resubmission; a +configuration change must preserve this behavior and the whole ledger. The new +observer neither alters reservations nor decides whether a provider accepted mail. + +No uncertain outcomes were present at this observation. Writers were active; +new requests can arrive afterward. Therefore this does not establish a drained +receiver or a valid execution-time baseline. Unlinked historical challenges are +reported separately; they may include superseded retries and are not automatically +classified as uncertain sends or relabeled as accepted. + +## Remaining executor work + +The executor must combine a verified foundation return with current identity/spec +guards, writer quiescence and a fresh recoverable ledger point. It must retain +phase evidence across lost replies and distinguish source configuration rollback +from data restoration. A failed rollout must not overwrite accepted/unknown sends, +consumed or canceled handles, suppression or quotas, including later legitimate +writes. The old reader/projection remains retained for guarded rollback; no +mailbox credential is copied into source, chat or a public receipt. + +The existing consumer dry-run candidate changes only the sender and credential +references, but is not an execution procedure. Exact supervised controller and +interruption tests are EMAIL-WP-0010-T02; live switch and independent postflight +are T03/T04. Productive mail authentication and inbox/return handling remain +CSOC-WP-0015-T02/T05. No new execution approval is requested by this observation. + +## Validation + +Twelve new synthetic cases pass. They compare the exact fingerprint format with +the existing archive implementation, preserve all rows, distinguish accepted +in_flight invitation rows from unknowns, retain unknown retry refusal after reopen, +classify verification/history cases, and prove that a concurrent committed WAL +write cannot split the fingerprint and outcome counts across snapshots. Missing, +symlinked, oversized and unexpected-schema input refuses; CLI failures redact +paths. Tests use no production URLs/accounts or provider sends. + +Owner default suite: 192 passed, 21 skipped in 13.21s. Those 21 existing tests require the +opt-in container mail harness; no new test is skipped. This is sufficient evidence +for the read-only observer and is not a delivered-message or rollout acceptance +claim. Ruff passes for both changed Python files. Log: +`/tmp/email-sender-ledger-check-20261031.log`. diff --git a/scripts/inspect-sender-ledger.py b/scripts/inspect-sender-ledger.py new file mode 100644 index 0000000..02220ab --- /dev/null +++ b/scripts/inspect-sender-ledger.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Read-only, coherent ledger observation for a supervised sender transition. + +No application startup, migration, SMTP, receipt mutation or outcome repair. +An observation while writers are active never proves a drained receiver. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import sqlite3 +import stat +from contextlib import closing +from datetime import UTC, datetime +from pathlib import Path + +TABLES = { + "deliveries", "verifications", "suppressions", "send_attempts", + "verification_attempts", "caller_buckets", +} +MAX_BYTES = 128 * 1024**2 + + +class Refused(RuntimeError): + pass + + +def inspect(path): + path = Path(path).absolute() + if (any(p.is_symlink() for p in [path, *path.parents]) + or not stat.S_ISREG(path.stat().st_mode)): + raise Refused("real_ledger_required") + with closing(sqlite3.connect(path.as_uri() + "?mode=ro", uri=True, timeout=5)) as db: + db.execute("PRAGMA trusted_schema=OFF") + db.execute("PRAGMA query_only=ON") + db.execute("BEGIN") + pages = db.execute("PRAGMA page_count").fetchone()[0] + size = db.execute("PRAGMA page_size").fetchone()[0] + if not 0 < pages * size <= MAX_BYTES: + raise Refused("ledger_size_outside_contract") + if db.execute("PRAGMA integrity_check").fetchall() != [("ok",)]: + raise Refused("ledger_integrity_failed") + schema = list(db.execute( + "SELECT type,name,tbl_name,sql FROM sqlite_master ORDER BY type,name")) + tables = {row[1] for row in schema + if row[0] == "table" and not row[1].startswith("sqlite_")} + if tables != TABLES: + raise Refused("unexpected_ledger_schema") + # Same representation as ledger_archive.fingerprint, but this read and + # every outcome query share one transaction, including committed WAL. + fingerprint = { + "schema_sha256": hashlib.sha256(json.dumps(schema).encode()).hexdigest(), + "user_version": db.execute("PRAGMA user_version").fetchone()[0], + "tables": {}, + } + for table in sorted(tables): + rows = [json.dumps(row, separators=(",", ":"), + default=lambda value: {"bytes_hex": value.hex()}).encode() + for row in db.execute('SELECT * FROM "' + table + '"')] + fingerprint["tables"][table] = { + "rows": len(rows), + "sha256": hashlib.sha256(b"\n".join(sorted(rows))).hexdigest(), + } + + def count(sql): + return db.execute(sql).fetchone()[0] + + outcomes = { + "accepted_invitations": count("SELECT COUNT(*) FROM deliveries"), + # Successful reserve_send rows stay in_flight; deliveries takes + # precedence in the application's status and retry logic. + "unknown_invitations": count( + "SELECT COUNT(*) FROM send_attempts a WHERE state='in_flight' " + "AND NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.event_id=a.event_id)"), + "failed_invitations": count( + "SELECT COUNT(*) FROM send_attempts a WHERE state='failed' " + "AND NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.event_id=a.event_id)"), + "accepted_verifications": count( + "SELECT COUNT(*) FROM verification_attempts WHERE state='accepted'"), + "unknown_verifications": count( + "SELECT COUNT(*) FROM verification_attempts WHERE state='in_flight'"), + "failed_verifications": count( + "SELECT COUNT(*) FROM verification_attempts WHERE state='failed'"), + "unlinked_verifications": count( + "SELECT COUNT(*) FROM verifications v WHERE NOT EXISTS " + "(SELECT 1 FROM verification_attempts a WHERE a.request_id=v.request_id)"), + "unrecognized_attempt_states": count( + "SELECT COUNT(*) FROM send_attempts WHERE state NOT IN ('in_flight','failed')" + ) + count( + "SELECT COUNT(*) FROM verification_attempts " + "WHERE state NOT IN ('in_flight','accepted','failed')"), + "orphan_verification_attempts": count( + "SELECT COUNT(*) FROM verification_attempts a WHERE NOT EXISTS " + "(SELECT 1 FROM verifications v WHERE v.request_id=a.request_id)"), + "consumed_challenges": count( + "SELECT COUNT(*) FROM verifications WHERE consumed_at IS NOT NULL"), + "canceled_challenges": count( + "SELECT COUNT(*) FROM verifications WHERE canceled_at IS NOT NULL"), + "suppressed_recipients": count("SELECT COUNT(*) FROM suppressions"), + } + db.rollback() + return { + "schema": "email-connect.sender-ledger-observation.v1", + "observed_at": datetime.now(UTC).isoformat(), + "ledger_fingerprint": fingerprint, + "outcomes": outcomes, + "needs_outcome_reconciliation": any(outcomes[key] for key in ( + "unknown_invitations", "unknown_verifications", + "unrecognized_attempt_states", + "orphan_verification_attempts")), + "consistent_read_transaction": True, + "writers_fenced": False, + "rollout_authorized": False, + "mail_sent": False, + "private_rows_printed": False, + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--database", type=Path, required=True) + args = parser.parse_args() + try: + result = inspect(args.database) + except (OSError, ValueError, sqlite3.Error, Refused): + raise SystemExit("ledger_observation_refused") from None + print(json.dumps(result, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/tests/test_sender_ledger.py b/tests/test_sender_ledger.py new file mode 100644 index 0000000..5d3df62 --- /dev/null +++ b/tests/test_sender_ledger.py @@ -0,0 +1,138 @@ +"""Synthetic ledger observations; never connects to an SMTP service.""" + +import importlib.util +import json +import sqlite3 +import subprocess +import sys +from pathlib import Path + +import pytest +from test_ledger_backup import synthetic_store + +from email_connect.transactional import ProviderError, SQLiteDeliveryStore + +SCRIPT = Path(__file__).resolve().parents[1] / "scripts/inspect-sender-ledger.py" +spec = importlib.util.spec_from_file_location("inspect_sender_ledger", SCRIPT) +observer = importlib.util.module_from_spec(spec) +spec.loader.exec_module(observer) + + +def test_matches_archive_fingerprint_and_preserves_every_row(tmp_path, monkeypatch): + monkeypatch.syspath_prepend(str(SCRIPT.parent)) + from ledger_archive import fingerprint + + path = tmp_path / "ledger.sqlite3" + synthetic_store(path) + before = fingerprint(path) + result = observer.inspect(path) + assert result["ledger_fingerprint"] == before == fingerprint(path) + assert not result["mail_sent"] and not result["private_rows_printed"] + assert not result["writers_fenced"] and not result["rollout_authorized"] + assert result["consistent_read_transaction"] + assert "example.test" not in json.dumps(result) + + +def test_accepted_invitation_in_flight_row_is_not_unknown(tmp_path): + path = tmp_path / "ledger.sqlite3" + store = SQLiteDeliveryStore(str(path)) + assert store.reserve_send("accepted-event", "fingerprint") + store.record("accepted-event", "synthetic-provider-reference") + assert store.db.execute("SELECT state FROM send_attempts").fetchone()[0] == "in_flight" + result = observer.inspect(path) + assert result["outcomes"]["accepted_invitations"] == 1 + assert result["outcomes"]["unknown_invitations"] == 0 + assert not result["needs_outcome_reconciliation"] + assert store.reserve_send("accepted-event", "fingerprint") is False + store.db.close() + + +def test_unknown_invitation_survives_inspection_and_restart(tmp_path): + path = tmp_path / "ledger.sqlite3" + store = SQLiteDeliveryStore(str(path)) + store.reserve_send("uncertain-event", "fingerprint") + store.db.close() + result = observer.inspect(path) + assert result["outcomes"]["unknown_invitations"] == 1 + assert result["needs_outcome_reconciliation"] + store = SQLiteDeliveryStore(str(path)) + with pytest.raises(ProviderError, match="delivery_outcome_unknown"): + store.reserve_send("uncertain-event", "fingerprint") + assert observer.inspect(path)["ledger_fingerprint"] == result["ledger_fingerprint"] + store.db.close() + + +@pytest.mark.parametrize("state", ["in_flight", "accepted", "failed"]) +def test_verification_outcomes_and_unlinked_history_are_distinct(tmp_path, state): + path = tmp_path / "ledger.sqlite3" + store = SQLiteDeliveryStore(str(path)) + payload = {"tenant": "fixture", "client_id": "client", "registration_id": "one", + "normalized_email": "one@example.test", "preferred_username": "one"} + request, _ = store.reserve_verification(payload, "synthetic-hash", "2030-01-01T00:00:00Z") + if state == "accepted": + store.verification_accepted(request, "synthetic-reference") + elif state == "failed": + store.verification_failed(request, ProviderError("definitive", retryable=False)) + payload["registration_id"] = "historical" + store.create_verification(payload, "other-hash", "2020-01-01T00:00:00Z") + result = observer.inspect(path) + label = {"in_flight": "unknown", "accepted": "accepted", "failed": "failed"}[state] + assert result["outcomes"][label + "_verifications"] == 1 + assert result["outcomes"]["unlinked_verifications"] == 1 + assert result["needs_outcome_reconciliation"] == (state == "in_flight") + store.db.close() + + +def test_concurrent_wal_commit_cannot_split_the_observation(tmp_path, monkeypatch): + path = tmp_path / "ledger.sqlite3" + store = SQLiteDeliveryStore(str(path)) + store.db.execute("PRAGMA journal_mode=WAL") + store.reserve_send("accepted", "fp") + store.record("accepted", "reference") + real_connect = sqlite3.connect + changed = [] + + class ObservedConnection(sqlite3.Connection): + def execute(self, statement, *args, **kwargs): + if statement == "SELECT COUNT(*) FROM deliveries" and not changed: + changed.append(True) + store.reserve_send("later-uncertain", "fp2") + return super().execute(statement, *args, **kwargs) + + monkeypatch.setattr(observer.sqlite3, "connect", lambda *a, **kw: + real_connect(*a, **kw, factory=ObservedConnection)) + result = observer.inspect(path) + assert changed + assert result["ledger_fingerprint"]["tables"]["send_attempts"]["rows"] == 1 + assert result["outcomes"]["unknown_invitations"] == 0 + assert store.db.execute("SELECT COUNT(*) FROM send_attempts").fetchone()[0] == 2 + store.db.close() + + +@pytest.mark.parametrize("invalid", ["missing", "symlink", "schema", "size"]) +def test_invalid_inputs_are_refused_without_writing(tmp_path, monkeypatch, invalid): + path = tmp_path / "ledger.sqlite3" + if invalid != "missing": + store = SQLiteDeliveryStore(str(path)) + store.db.close() + if invalid == "symlink": + target = tmp_path / "target.sqlite3" + path.rename(target) + path.symlink_to(target) + elif invalid == "schema": + with sqlite3.connect(path) as db: + db.execute("CREATE TABLE unexpected (value TEXT)") + elif invalid == "size": + monkeypatch.setattr(observer, "MAX_BYTES", 1) + before = path.read_bytes() if path.exists() else None + with pytest.raises((observer.Refused, OSError)): + observer.inspect(path) + assert (path.read_bytes() if path.exists() else None) == before + + +def test_cli_errors_do_not_echo_private_paths(tmp_path): + result = subprocess.run([sys.executable, str(SCRIPT), "--database", + str(tmp_path / "private-recipient@example.test")], + capture_output=True, text=True, timeout=10, check=False) + assert result.returncode != 0 and not result.stdout + assert result.stderr.strip() == "ledger_observation_refused" diff --git a/workplans/EMAIL-WP-0010-platform-sender-transition.md b/workplans/EMAIL-WP-0010-platform-sender-transition.md new file mode 100644 index 0000000..24c7d27 --- /dev/null +++ b/workplans/EMAIL-WP-0010-platform-sender-transition.md @@ -0,0 +1,118 @@ +--- +id: EMAIL-WP-0010 +type: workplan +title: "Supervised platform sender transition with ledger continuity" +domain: infotech +repo: email-connect +status: active +owner: codex +topic_slug: email-connect +created: "2026-10-31" +updated: "2026-10-31" +related: + - CSOC-WP-0015 + - RPF-WP-0068 +state_hub_workstream_id: "6d8e1363-e67e-5837-bb6f-e5a95f43d71b" +--- + +Owner execution dependency of CSOC-WP-0015-T02. The platform mailbox's additional +reader admission is RPF-WP-0068/CCR-2026-0056; this workplan does not duplicate or +self-approve that credential lane. Bernd/platform-root reply ownership is settled. +Existing caller admission, image, ledger, backup schedules and old custody remain +in scope for preservation. Productive sender/return handling stays in CSOC-WP-0015. + +## T01 — Establish native ledger and receiver transition boundaries + +```task +id: EMAIL-WP-0010-T01 +status: done +priority: high +state_hub_task_id: "f5b03013-3c1b-5b1a-86e2-a96dd817fa00" +``` + +Completed 2026-10-31 with `scripts/inspect-sender-ledger.py` and +`docs/sender-ledger-native-2026-10-31.json`. The read-only helper fingerprints all +six tables and classifies outcomes in one SQLite transaction, including committed +WAL. Native generation 74/image 5d79fd1f retains forgejo@coulomb.social. Packaged +transactional source matches the owner checkout. Two invitation receipts and 121 +verification attempts are accepted; zero unknown/unrecognized/orphan outcomes, +zero unlinked challenges, one suppression. No message body, address, handle, +credential or individual record was printed. No deployment or ledger write. + +Successful invitations can retain an in_flight attempt; the deliveries row is +authoritative. The receiver has no explicit SIGTERM drain handler. A process exit +can leave an uncertain provider outcome, which must remain non-retryable. This +observation is not a writer fence, backup, outcome repair or approval. Twelve new +synthetic cases pass, including concurrent WAL writes, retry continuity, exact +archive-fingerprint compatibility and private-output refusal. Owner suite: +192 passed, 21 skipped (pre-existing opt-in container harness tests). + +## T02 — Prepare and rehearse the supervised configuration executor + +```task +id: EMAIL-WP-0010-T02 +status: todo +priority: high +depends_on: [EMAIL-WP-0010-T01] +state_hub_task_id: "acb6f995-9177-5a3c-909a-feb152d6bfed" +``` + +Turn the existing five-operation consumer candidate into a concrete source-bound +sender executor after inspecting the actual service/caller boundary. Reuse the +already prepared foundation resources; do not recreate them. Pin cluster, +namespace, deployment, image, existing caller policy/projections and complete +before/after configuration. Preserve all unrelated deployment fields and old +credential custody. The actual source sender change is only forgejo@ -> platform@, +with the dedicated admitted SMTP projection and explicit username. + +Prove writer quiescence, fresh recoverable ledger capture, outcome classification, +complete data preservation, private durable intent before every mutation and +independent runtime/no-send checks. Reuse existing ledger capture/rehearsal and +fixed projection proof. Review ambiguity after SIGTERM/lost replies, interruption +before/after each configuration change, and rollback with legitimate subsequent +ledger writes. Never restore a pre-release ledger, replay uncertain sends, reenable +the retired legacy bearer or delete old custody as a configuration rollback. +Prepare a bounded recovery procedure that refuses unrelated drift. Deliver the +exact review packet and passing synthetic/native dry-run evidence before asking +for execution; a read-only ledger snapshot alone does not close this task. + +## T03 — Execute the admitted sender switch + +```task +id: EMAIL-WP-0010-T03 +status: wait +blocking_reason: "Requires the complete sender executor and verified RPF-WP-0068-T04 credential-owner return." +priority: high +depends_on: [EMAIL-WP-0010-T02, RPF-WP-0068-T04] +state_hub_task_id: "e0cbed16-f08f-50f7-8e15-633b3af29e88" +``` + +After the verified credential-owner return and concrete sender-packet review, +obtain the attended execution return through the existing owner deployment lane. +Do not treat mailbox ownership, credential admission, a prior image release or +the dry-run candidate as approval for an unprepared configuration change. Inspect +any existing attempt before replay; uncertain outcomes require the reviewed +recovery procedure. Preserve application/caller state and retained evidence. + +## T04 — Verify the native sender return and reconcile sources + +```task +id: EMAIL-WP-0010-T04 +status: todo +priority: high +depends_on: [EMAIL-WP-0010-T03] +state_hub_task_id: "19485ff3-132c-567c-8c28-8c039e63c2b4" +``` + +Independently verify private durable receipts, complete intended spec, actual +process sender/credential references, Ready state, caller denials and ledger +continuity. Reconcile deployment sources to the verified runtime only after +success. Return bounded evidence to CSOC-WP-0015-T02. Delivered-message +authentication, selected inbox/return handling, suppression authority and +retention/productive verdicts remain live consumer requirements; this configuration +return does not establish them. No unsolicited mail is part of these checks. + +## Completion + +Finish after all four tasks have their stated native evidence. Hand any newly +discovered actionable residual to a live work record before finishing.