feat(emailgen): global persona pool + Date-stamped EML mtimes

Two changes that unwind earlier MazeNET-only assumptions and fix a
realism tell:

1. Persona resolution is now per-decky-source, not topology-only.  The
   scheduler walks the union view (list_running_deckies, including
   fleet MACVLAN/IPVLAN + SWARM shards) and picks the right persona
   list for each source:
     * topology decky -> Topology.email_personas (per-topology richness
       preserved)
     * fleet / shard  -> a single host-wide pool loaded from disk
       (DECNET_EMAILGEN_PERSONAS, /etc/decnet/email_personas.json, or
       ~/.decnet/email_personas.json)
   Operators install the global pool via 'decnet emailgen
   import-personas <file>' which validates with the same Pydantic
   schema the worker uses.

2. The driver now runs 'touch -d <Date>' inside the docker exec right
   after the EML write so file mtime matches the email's RFC 2822
   Date: header.  Without this an attacker 'ls -lt'ing the spool sees
   every email clustered inside the worker's tick window — the
   cluster itself was a stylometric tell.

CLI now exposes 'decnet emailgen' as a sub-app with 'run' (default,
backwards-compatible with bare 'decnet emailgen') and 'import-personas'.
list_running_deckies carries topology_id through so consumers can resolve
the parent topology without a second round-trip.
This commit is contained in:
2026-04-26 22:39:16 -04:00
parent 2979997442
commit 4badc75fb2
9 changed files with 640 additions and 37 deletions

View File

@@ -135,6 +135,15 @@ async def test_driver_run_success_path(monkeypatch):
# Two subprocess calls: ollama, then docker exec.
assert calls[0][0] == "ollama"
assert calls[1][0] == "docker"
# docker exec shell command must include `touch -d` so the file's
# mtime matches the EML's Date: header — otherwise the spool's
# `ls -lt` clusters every email inside the worker tick window.
docker_sh = calls[1][-1]
assert "touch -d" in docker_sh
assert "tee" in docker_sh
# And tee must come before touch so we don't touch a file that
# doesn't exist yet.
assert docker_sh.index("tee") < docker_sh.index("touch -d")
@pytest.mark.asyncio