Files
session-history/SESSION-0013.3-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-58.md
T
Ben Stull 1afa9f50eb Rename all transcripts: SESSION-<letter> → SESSION-NNNN.M (roadmap #23)
Executed in Session 0014.0 (= legacy "Session N"), 2026-05-28. The
session-protocol naming convention switched from alphabetical letters
(SESSION-A through SESSION-M, plus SESSION-M.1/M.2/M.3 subsessions) to
zero-padded 4-digit session numbers with a subagent ordinal suffix
(SESSION-0001.0 through SESSION-0013.0, plus SESSION-0013.1/.2/.3).

Rationale: alphabetical ordering breaks down after Z (does AA sort
before B in a filesystem? a glob?), and the convention doesn't
generalize to deeper structure. The numeric form addresses both, and
the explicit `.0` for the main driver transcript makes the parent-vs-
subagent distinction visible at first glance.

Mapping (all 16 transcripts):
  A    → 0001.0
  B    → 0002.0
  C    → 0003.0
  D    → 0004.0
  E    → 0005.0
  F    → 0006.0
  G    → 0007.0
  H    → 0008.0
  I    → 0009.0
  J    → 0010.0
  K    → 0011.0
  L    → 0012.0
  M    → 0013.0
  M.1  → 0013.1
  M.2  → 0013.2
  M.3  → 0013.3

This commit uses `git mv` for each rename so Gitea's rename detection
records each as a file-was-renamed event rather than file-was-deleted-
and-recreated. The transcript bodies are NOT rewritten — they still
read "Session I" / "Session M.1" / etc. internally; the body's session
reference and the filename's numeric form are both authoritative.
Readers map back via the table above.

External references to the old paths
(e.g. wiggleverse/ohm-session-history/SESSION-I-TRANSCRIPT-…md) will
404 after this rename. The audit trail is in this git log; readers
who care can trace. No redirect tombstones were added.

The publish-transcript.sh validator was extended to accept both the
numeric form (binding from 0014.0 onward) AND the legacy letter form
(so renamed-old-files remain re-publishable if their content is later
corrected). See ohm-infra/scripts/publish-transcript.sh and
ohm-infra/SESSION-PROTOCOL.md §1 (binding spec) for the new
convention.

Session 0014.0 is the first session to ship under the new naming
end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:19:18 -07:00

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.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 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-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_HOSTmissing. SMTP_USERmissing.
  • 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/alternativeall 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-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

  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-M.3-TRANSCRIPT-2026-05-28T05-30--2026-05-28T05-58.md