Observe sender ledger continuity and track supervised transition
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
This commit is contained in:
parent
c1c14a0ea1
commit
c2ce58f10d
6 changed files with 526 additions and 0 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
64
docs/sender-ledger-native-2026-10-31.json
Normal file
64
docs/sender-ledger-native-2026-10-31.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
68
docs/sender-transition-boundary-2026-10-31.md
Normal file
68
docs/sender-transition-boundary-2026-10-31.md
Normal file
|
|
@ -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`.
|
||||
133
scripts/inspect-sender-ledger.py
Normal file
133
scripts/inspect-sender-ledger.py
Normal file
|
|
@ -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()
|
||||
138
tests/test_sender_ledger.py
Normal file
138
tests/test_sender_ledger.py
Normal file
|
|
@ -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"
|
||||
118
workplans/EMAIL-WP-0010-platform-sender-transition.md
Normal file
118
workplans/EMAIL-WP-0010-platform-sender-transition.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue