Files
session-history/ohm/0013/SESSION-0013.3-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-58.md

312 lines
13 KiB
Markdown

# Session 0013.3 — Transcript
> Parent: SESSION-0013.0-TRANSCRIPT-2026-05-28T05-26--<end>.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.md` and
> `~/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 on `wiggleverse/meta` still points at the deprovisioned
> `http://rfc.wiggleverse.org`.**
---
## Pre-session state
- Parent: Session 0013.0 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-app` at v0.17.0 on disk; live OHM running v0.17.0 per
`flotilla 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, no `pct=`, no `sp=`).
- **DKIM:** present at `google._domainkey.wiggleverse.org` — a
2048-bit RSA Workspace key. All other probed selectors empty.
- **MX:** `aspmx.l.google.com` family — 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_SECRET` bound to
`wiggleverse-ohm/ohm-rfc-app-gitea-webhook-secret@latest`.
Greps:
- `~/projects/wiggleverse/ohm-rfc-app-flotilla/scripts/register-ohm.sh:40`
sets `SMTP_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 via `gcloud 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 adds `Date` /
`Message-ID` during 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-0013.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
1. **rfc-app: `GITEA_WEBHOOK_SECRET` should be required, not optional.**
Today the framework silently accepts unsigned POSTs when the env
var is empty. Recommend `_required` with documented
`RFC_APP_INSECURE_WEBHOOKS=1` dev-bypass.
2. **rfc-app: outbound-email observability seam.** Add
`outbound_emails` table + write on every send. Today failures
log-and-forget; the operator has no audit trail.
3. **flotilla: `overlay show` doesn'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 validate` verb would
catch this class of regression.
4. **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 future `flotilla-core` extraction.
5. **rfc-app: multiple From-identity references should derive from
one source.** `EMAIL_FROM` is read in `email.py:82`,
`email_otc.py` via cfg, `email_invite.py` via 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
1. **Decide where to put the two docs.** `~/git/ohm-infra/` is not
a git repo. Options: (a) leave as plain files, publish only via
`publish-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 a `docs/` subdir
since they're OHM-deployment-specific. Driver decides.
2. **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
Environment` next session.
3. **Schedule the stale-hook deletion.** Five-second gesture; the
hook on `wiggleverse/meta` pointing at deprovisioned
`http://rfc.wiggleverse.org/api/webhooks/gitea` should go.
4. **Decide on the From-identity question.** `ohm@wiggleverse.org`
(roadmap's suggestion) or keep `notifications@wiggleverse.org`?
Pinning this is a prerequisite for the framework-side proposal's
`Reply-To` decision.
5. **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.
6. **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):**
1. Delete stale `wiggleverse/meta` hook (5 seconds, removes
plain-HTTP-to-deprovisioned-domain liability).
2. Disambiguate SMTP_HOST gap (5 minutes, confirms email actually
leaves the VM).
3. DMARC Phase A: add `pct=100; sp=quarantine; adkim=s; aspf=s` to
the existing `p=none` record; wait 1 week; flip to `p=quarantine`.
**Top 3 framework-side changes (rfc-app minor, target v0.18.0):**
1. `build_envelope` shared helper adds `Date`, `Message-ID`,
`Auto-Submitted`, `List-Unsubscribe`, `List-Unsubscribe-Post`,
and `multipart/alternative` to all four email paths.
2. Make `GITEA_WEBHOOK_SECRET` required (not silently optional).
3. `outbound_emails` audit 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-0013.3-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-58.md`