13 KiB
Session M.3 — Transcript
Parent: SESSION-M-TRANSCRIPT-2026-05-28T05-26--.md
Date: 2026-05-28 (PST) Start: 2026-05-28T05-30 PST End: 2026-05-28T05-58 PST Goal: Inventory OHM's email + Gitea-webhook posture (roadmap items #18 + #20, bundled) and draft a hardening runbook plus a framework-side template-hygiene proposal. Read-only only — no DNS edits, no SMTP-provider changes, no rfc-app code, no secret rotations, no test emails.
Outcome: Two docs written and left on disk for the driver at
~/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.mdand~/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md. Five §19.2 candidates surfaced. Three high-impact deployment-side gestures and three high-impact framework-side changes prioritized for the operator. Two surprising findings: SMTP_HOST is missing from the live flotilla overlay (gap to disambiguate), and a stale Gitea webhook onwiggleverse/metastill points at the deprovisionedhttp://rfc.wiggleverse.org.
Pre-session state
- Parent: Session M driver, dispatched M.3 with scope = inventory for items #18 + #20.
~/git/ohm-infra/is a plain directory (not a git repo) — driver noted this; M.3 leaves docs on disk for the driver to handle publishing.rfc-appat v0.17.0 on disk; live OHM running v0.17.0 perflotilla pin check ohm-rfc-app(deploys.id=21).- Hard constraints from dispatch: no operator-led gestures, no secret bytes in transcript, read-only inventory only.
Turn-by-turn arc
Arc 1 — DNS inventory + roadmap read
Ran dig +short TXT wiggleverse.org, dig +short TXT _dmarc.wiggleverse.org, dig +short MX wiggleverse.org, and
probed common DKIM selectors (default, mail, s1, s2,
selector1, k1, mandrill, smtp, google, etc.).
Findings:
- SPF:
"v=spf1 include:_spf.google.com ~all"(soft-fail). - DMARC:
"v=DMARC1; p=none; rua=mailto:ben@wiggleverse.org"(monitoring-only, no enforcement, nopct=, nosp=). - DKIM: present at
google._domainkey.wiggleverse.org— a 2048-bit RSA Workspace key. All other probed selectors empty. - MX:
aspmx.l.google.comfamily — Google Workspace mailbox.
Read roadmap items #18 (l.362-403) and #20 (l.458-547) from
~/projects/wiggleverse/ohm-rfc/ROADMAP.md. Both confirm they
should ship together because the same DNS edits serve both. #20
names BIMI as a follow-on after DMARC reaches p=reject. #18 names
"refresh webhook-shared-secret + replay-resistance + ingress
restriction" as framework-side work.
Arc 2 — Flotilla overlay + secret inventory
.venv/bin/ohm-rfc-app-flotilla overlay show ohm-rfc-app
.venv/bin/ohm-rfc-app-flotilla secret list ohm-rfc-app
Overlay reveals:
SMTP_PORT=587,SMTP_STARTTLS=1,EMAIL_FROM=notifications@wiggleverse.org,EMAIL_FROM_NAME=Wiggleverse— present.SMTP_HOST— missing.SMTP_USER— missing.- Secret refs include
GITEA_WEBHOOK_SECRETbound towiggleverse-ohm/ohm-rfc-app-gitea-webhook-secret@latest.
Greps:
~/projects/wiggleverse/ohm-rfc-app-flotilla/scripts/register-ohm.sh:40setsSMTP_HOST=smtp-relay.gmail.com— but the live overlay doesn't have it. Either the bootstrap script wasn't re-run after a reshape, or SMTP env lives out-of-band on the VM (e.g., systemd-unit override). This is a finding for the runbook — operator must disambiguate viagcloud ssh+systemctl show ohm-app.service.
Confirmed provider = Google Workspace SMTP relay (the bootstrap
script + the google._domainkey DKIM + the MX records align).
Arc 3 — Framework email-helper read
Read all of:
~/git/rfc-app/backend/app/email.py— §15.4 notification mailer (_deliver,_send_one,_send_bundle,make_unsubscribe_url).~/git/rfc-app/backend/app/email_otc.py— v0.7.0 OTC helper.~/git/rfc-app/backend/app/email_invite.py— v0.17.0 admin-create invite helper.
Built header-presence matrix across all four kinds (OTC / invite / watcher-single / watcher-bundle):
Date,Message-ID,Auto-Submitted,List-Unsubscribe,List-Unsubscribe-Post,multipart/alternative— all absent on all four paths. Google's SMTP relay addsDate/Message-IDduring ingestion, but the framework doesn't control the format.- Plain-text bodies only. No HTML variant anywhere.
_deliver(email.py:308) embeds unsubscribe URL in body footer but doesn't set the header.
Stdlib spot-check: python3 -c "from email.message import EmailMessage; …" confirms set_content doesn't auto-populate
Date or Message-ID. Inspected smtplib.SMTP.send_message
source — also doesn't set them.
Arc 4 — Gitea-webhook inventory
Found the handler at ~/git/rfc-app/backend/app/webhooks.py (78
lines). HMAC-SHA-256 over the raw body, hmac.compare_digest for
constant-time compare. But: webhooks.py:43 gates verification
behind if config.webhook_secret: — empty secret = no auth. Per
config.py:75, webhook_secret is _optional. Insecure default.
Pulled gitea-bot-token from GCP Secret Manager into a shell var
(GITEA_TOKEN=$(gcloud secrets versions access latest …)), used
it for read-only curl calls against the Gitea API, immediately
unset after each call. Never echoed the value to stdout or any
file.
Hook inventory across the wiggleverse org:
| Repo | Hook | Notes |
|---|---|---|
wiggleverse/ohm-meta |
https://ohm.wiggleverse.org/api/webhooks/gitea |
active, 12 events — correct |
wiggleverse/meta |
http://rfc.wiggleverse.org/api/webhooks/gitea |
active, plain HTTP, deprovisioned domain |
wiggleverse/rfc-0001-human |
(none) | the only published RFC has no hook |
ben.stull/ohm-rfc |
(bot 403) | can't inventory, operator self-checks |
The org-level hooks endpoint
(/api/v1/orgs/wiggleverse/hooks) returned [] — no org-wide
hooks, all are repo-level.
Gitea's API exposes hook config keys = ['url', 'content_type']
— it does NOT return the configured secret field, so I can't
confirm from the API side that the secret matches what OHM has.
But OHM's cache is current and webhook receiver verifies, so the
secrets must align (otherwise everything would 401).
Arc 5 — Bounce-handling + observability gap
Read ~/git/rfc-app/backend/app/api_notifications.py:471-502 —
/api/webhooks/email-bounce exists, gated on optional
WEBHOOK_EMAIL_BOUNCE_SECRET per SPEC.md §3667 Slice 8 settlement.
But no provider is wired to POST here — Google Workspace SMTP
relay doesn't natively webhook bounces (it bounces back to the
sender mailbox). So today: hard bounces silently retry every
flush_pending pass.
All four send helpers catch Exception, log via log.exception,
return False. No DB write of the failure, no admin notification.
The operator has zero visibility into send failures without
grepping VM logs.
Arc 6 — Draft the two docs
Wrote EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md (~700 lines) with
sections: current posture / hardened shape / operator-execution
sequence / framework-vs-deployment split / §19.2 candidates.
Wrote RFC-APP-EMAIL-HYGIENE-PROPOSAL.md (~280 lines) with the
framework-side minor proposal: build_envelope shared helper,
webhook handler tightening (mandatory secret), outbound_emails
audit table, bounce-correlation, suggested 5-slice shipping plan.
Cross-referenced both docs at each end. Both name files + line numbers for every framework citation.
Arc 7 — Transcript
This file. ohm-infra is not a git repo (driver's check confirmed
pre-session), so no commit; driver decides whether to add a remote
or just publish the transcripts via publish-transcript.sh.
Cut state (end of subsession)
| Surface | State |
|---|---|
~/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md |
written, ~21 KB |
~/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md |
written, ~9 KB |
~/git/ohm-infra/SESSION-M.3-TRANSCRIPT-…md |
this file |
| OHM live | unchanged (v0.17.0, deploys.id=21) |
| DNS / Gitea / SMTP provider | unchanged (read-only inventory only) |
| flotilla overlay / secrets | unchanged (read-only show + list only) |
§19.2 candidates surfaced
-
rfc-app:
GITEA_WEBHOOK_SECRETshould be required, not optional. Today the framework silently accepts unsigned POSTs when the env var is empty. Recommend_requiredwith documentedRFC_APP_INSECURE_WEBHOOKS=1dev-bypass. -
rfc-app: outbound-email observability seam. Add
outbound_emailstable + write on every send. Today failures log-and-forget; the operator has no audit trail. -
flotilla:
overlay showdoesn't surface "expected but missing" keys. The SMTP_HOST gap is invisible without grepping register-ohm.sh + framework source side-by-side. A per-deployment "required keys" manifest +flotilla overlay validateverb would catch this class of regression. -
flotilla: lacks a webhook-inventory verb. Listing / diffing the webhooks on the org's Gitea repos against the deployment's expected set is a recurring operator gesture. A
flotilla webhook list / check <deployment>verb would formalize it. Generic enough for a futureflotilla-coreextraction. -
rfc-app: multiple From-identity references should derive from one source.
EMAIL_FROMis read inemail.py:82,email_otc.pyvia cfg,email_invite.pyvia cfg, plus implied by DMARC alignment + Workspace allowed-senders. A spec section binding all surfaces to one source would prevent skew.
What lands on the driver's plate
-
Decide where to put the two docs.
~/git/ohm-infra/is not a git repo. Options: (a) leave as plain files, publish only viapublish-transcript.sh(which today only handles transcripts); (b) git-init~/git/ohm-infra/and add a Gitea remote so ohm-infra becomes a versioned doc repo; (c) move the two docs into~/projects/wiggleverse/ohm-rfc/under adocs/subdir since they're OHM-deployment-specific. Driver decides. -
Schedule the disambiguation gesture. The SMTP_HOST gap is the most urgent finding — if the framework is actually falling to stdout-fallback, OHM's emails (OTC, invites, notifications, beta-access) aren't leaving the VM. Operator runs
gcloud ssh ohm-app+systemctl show ohm-app.service | grep Environmentnext session. -
Schedule the stale-hook deletion. Five-second gesture; the hook on
wiggleverse/metapointing at deprovisionedhttp://rfc.wiggleverse.org/api/webhooks/giteashould go. -
Decide on the From-identity question.
ohm@wiggleverse.org(roadmap's suggestion) or keepnotifications@wiggleverse.org? Pinning this is a prerequisite for the framework-side proposal'sReply-Todecision. -
Slot the rfc-app email-hygiene minor. Per the proposal, v0.18.0 or v0.19.0; 5 slices; mostly backward-compatible. Operator decides whether to bundle with another minor (e.g., #19's CONTRIBUTING.md docs) or ship solo.
-
Confirm
ben.stull/ohm-rfc's hooks. Bot lacks admin so M.3 couldn't inventory. Operator self-checks via Gitea web UI.
What the driver needs to know (report-back summary)
DNS posture (literal values):
- SPF:
v=spf1 include:_spf.google.com ~all(soft-fail). - DMARC:
v=DMARC1; p=none; rua=mailto:ben@wiggleverse.org(monitoring-only). - DKIM: present at
google._domainkey.wiggleverse.org(2048-bit Workspace key).
SMTP provider: Google Workspace SMTP relay
(smtp-relay.gmail.com) — inferred from bootstrap script + DKIM
- MX. But SMTP_HOST is missing from the live flotilla overlay — either the bootstrap was reshaped and these were dropped, or they're set on the VM out-of-band. Operator must disambiguate.
Top 3 deployment-side hardening gestures (priority order):
- Delete stale
wiggleverse/metahook (5 seconds, removes plain-HTTP-to-deprovisioned-domain liability). - Disambiguate SMTP_HOST gap (5 minutes, confirms email actually leaves the VM).
- DMARC Phase A: add
pct=100; sp=quarantine; adkim=s; aspf=sto the existingp=nonerecord; wait 1 week; flip top=quarantine.
Top 3 framework-side changes (rfc-app minor, target v0.18.0):
build_envelopeshared helper addsDate,Message-ID,Auto-Submitted,List-Unsubscribe,List-Unsubscribe-Post, andmultipart/alternativeto all four email paths.- Make
GITEA_WEBHOOK_SECRETrequired (not silently optional). outbound_emailsaudit table + bounce-correlation hook.
Webhook posture: signature verification is correct
(hmac.compare_digest, HMAC-SHA-256 over raw body) when the
secret is set. On OHM the secret is bound. Two open issues:
(a) framework's silent-accept-when-unset insecure default; (b)
stale wiggleverse/meta hook on a deprovisioned domain.
§19.2 candidates surfaced: 5 (listed above).
Doc paths:
/Users/benstull/git/ohm-infra/EMAIL-AND-WEBHOOK-HARDENING-RUNBOOK.md/Users/benstull/git/ohm-infra/RFC-APP-EMAIL-HYGIENE-PROPOSAL.md/Users/benstull/git/ohm-infra/SESSION-M.3-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-58.md