# Session 0061.0 — Transcript > Date: 2026-06-01 > Goal: launch-app Slice 3 — build `provision-vm` (§6.7) + `scaffold-gitea-repos` > (§6.8), then dogfood them against wiggle-snip to a verified pre-prod `/api/health`. > > Outcome: **DONE. Both skills built; wiggle-snip dogfooded green end-to-end — > live pre-prod at `https://wiggle-snip-ppe.wiggleverse.org/api/health` (v0.1.0, > deploys.id=2, all 9 phases ok), re-run idempotent. 5 skill bugs caught + fixed.** > > Highlights: dropped SSH deploy keys for an HTTPS + Secret-Manager-token git > credential helper (Cloudflare-proxied Gitea won't pass SSH :2222 to a GCE VM); > switched the edge scheme `ppe..` → `-ppe.` (Cloudflare > Universal SSL doesn't cover 3-label hosts); repos private-by-default (guide §5.5). --- ## Plan (final) - [x] Read SPEC/ROADMAP/SESSION-PROTOCOL; claim session ID (0061). - [x] Map the deploy substrate (flotilla-core deploy.py phases 2–8). - [x] Reconcile flotilla-core reality vs the launch prompt (Arc 1). - [x] Build `provision-vm` + `scaffold-gitea-repos` skills (Arc 2). - [x] Dogfood: scaffold-gcp-project → VM → provision → deploy → green /api/health. - [x] Fix the 5 skill bugs the dogfood surfaced (Arcs 4, 6, 7). - [x] "private by default" convention (guide §5.5) per operator directive. - [x] Update SPEC §2/§5.1/§5.3 + flotilla-core derive_names + tests (edge scheme). - [x] Close Slice 3 in ROADMAP; commit + push branches; deliver PR compare URLs. - [x] Finalize + publish this transcript. --- ## Pre-session state - launch-app SPEC v0.0.2; Slices 1–2 closed. Skills: `scaffold-gcp-project` (§6.5), `define-deployment` (§6.6). `wiggle-snip` built + tested, NOT a git repo, with a `deployment.toml` that validated 5 PASS + 1 WARN (gcloud config absent). - gcloud active on-disk pointer `benstull-infra` — left untouched all session (every gcloud call pinned `CLOUDSDK_ACTIVE_CONFIG_NAME` per-process). --- ## Turn-by-turn arc ### Arc 1 — flotilla-core reality vs the launch prompt Launch prompt claimed "flotilla-core v0.3.0 on main, tagged, 371 tests" with the scaffold/validate verbs. **Reality (SPEC §4.1 read-the-world):** `main` = `333e0ff` **v0.2.0 (per-PR preview environments)**, no scaffold/validate. The Slice-2 work is on branch **`slice2-schema-validate`** (worktree `~/projects/wiggleverse/flotilla-core-slice2`), rebased on the preview-env main, **not merged, not tagged**. This session used the slice2 worktree's CLI and never touched the flotilla-core main checkout (the parallel preview-environments line owns it). ### Arc 2 — Build the two skills Studied `deploy.py` to pin exactly what phases 2–8 expect on the VM. Built under `launch-app/skills/`: `provision-vm/` (SKILL.md + provision-runtime.sh + read-deployment-params.py) and `scaffold-gitea-repos/` (SKILL.md + init-and-push.sh). Both shell assets `bash -n` clean; param reader round-trips the toml. (Initial provision-runtime used an SSH deploy key for private clones — later abandoned, Arc 6.) ### Arc 3 — GCP foundation for wiggle-snip Operator chose **full live bring-up**, **new** billing account. Drove scaffold-gcp-project: project `wiggle-snip` under org wiggleverse.org; dedicated `wiggle-snip` profile `--no-activate` (active pointer verified unchanged); billing linked (`01789B-…`); APIs enabled; ADC quota pinned to wiggle-snip. **Validate flipped to 6/6 PASS.** (ADC quota-project is global — flagged: next OHM session must re-pin.) ### Arc 4 — VM + firewall + runtime (bugs #1–#3) Static IP 35.255.99.91; VM `wiggle-snip-ppe` (e2-micro, debian-12, shielded) RUNNING. IAP-only firewall. **Bug #1:** default VPC `default-allow-ssh` (:22 ← 0.0.0.0/0) defeats IAP-only (rules union) → delete default-allow-ssh/rdp. IAP SSH verified. provision-runtime installed node 18.20.4 / npm 9.2.0 / python 3.11.2. **Bug #2:** clone temp `${INSTALL_DIR}.clone` under root-owned /opt not writable by the service user → root pre-creates it user-owned. **Bug #3:** Gitea has an AAAA record, VM has no IPv6 route → SSH clone "Network is unreachable" → forced IPv4 (later mooted by Arc 6). ### Arc 5 — Gitea repo + the One-Name fork No GITEA_TOKEN in env; org push-to-create disabled. Operator created the repo as **`wiggleverse/wiggleverse-wiggle-snip-app`** (the `--app` convention), not `wiggle-snip`. **One-Name divergence (§3.3).** Operator chose: keep deployment/project/VM = `wiggle-snip`, repoint `[app].repo`. Logged as Slice-5 backlog. Pushed main + tag `v0.1.0` over SSH (as ben.stull). Repo kept **private** (operator). ### Arc 6 — Private path: SSH deploy keys don't work behind Cloudflare (bug #4) First tried the private path with an on-VM SSH deploy key (operator registered the public half). Clone failed: **the VM cannot reach git.wiggleverse.org:2222** — it resolves the Cloudflare IP, which only forwards HTTP/S, not SSH (the operator's laptop reaches :2222 via a non-CF path). HTTPS :443 works fine. **Pivot:** clone + fetch over HTTPS with a git **credential helper that fetches the read token from Secret Manager via the VM's metadata service account** — no token ever through the session, no SSH, **no deploy key at all** (one read token serves both flotilla's pin fetch and the VM clone). Required: VM `cloud-platform` scope (stop/set-scopes/start) + SA `secretmanager.secretAccessor` on the secret. Operator created the read-only Gitea token → I created the empty SM secret + IAM, operator piped the value (`gcloud secrets versions add … --data-file=-`, pbpaste). Also adopted **"repos are private by default"** (guide §5.5 new) per operator directive — public is now an explicit opt-out. ADC needed `gcloud auth application-default login` (operator) for the laptop-side pin read. HTTPS clone then succeeded. ### Arc 7 — Edge TLS: 3-label host has no Cloudflare cert (bug #5) + nginx :80/:443 Deploy #1 (deploys.id=1) reached phase 8 then **failed: SSLV3_ALERT_HANDSHAKE_FAILURE**. Cause: `ppe.wiggle-snip.wiggleverse.org` is a **3-label host**, and **Cloudflare Universal SSL only covers the apex + `*.`** — no edge cert → handshake fails. **Fix (operator-confirmed):** switch the scheme to **`-ppe.`** (single label, covered by `*.wiggleverse.org`) across SPEC §2/§5.1/§5.3, flotilla-core `derive_names` + tests (371 green), `define-deployment`, and wiggle-snip's record; re-provisioned nginx; operator added the new DNS A record. Then a **301 loop**: Cloudflare was hitting origin :80 (Flexible/uncached) → my nginx `:80→:443` redirect looped. **Fix:** nginx now **serves the app on both :80 and :443** (origin ports are firewalled to Cloudflare only; CF handles the visitor http→https upgrade) — works in Cloudflare Full *or* Flexible. Edge then returned `{"status":"ok","version":"0.1.0"}` 200. ### Arc 8 — Green deploy + idempotency + closeout `flotilla-core deploy wiggle-snip`: **all 9 phases ok → deployed v0.1.0 (deploys.id=2)**, phase-8 verify 2.4s. Re-running provision is a no-op (service user already exists / packages present / repo already cloned). Committed + pushed: flotilla-core `slice2-schema-validate` (edge fix), engineering `slice3-provision-vm-scaffold-gitea` (skills + SPEC + guide §5.5 + ROADMAP), wiggle-snip `main` (toml). PRs left for the operator (no Gitea token in session). --- ## Cut state (end of session) | | | | --- | --- | | engineering | branch `slice3-provision-vm-scaffold-gitea` pushed (PR pending) — 2 skills, SPEC §2/§5.1/§5.3, guide §5.5, ROADMAP Slice 3 DONE | | flotilla-core | branch `slice2-schema-validate` pushed (PR pending) — Slice-2 verbs + session-0061 edge-host fix; 371 tests green; NOT merged/tagged | | wiggleverse-wiggle-snip-app | `main` @ `5745b09` + tag `v0.1.0`; **private** repo | | wiggle-snip live | VM `wiggle-snip-ppe` (project `wiggle-snip`, IP 35.255.99.91), deployed v0.1.0, **green** at https://wiggle-snip-ppe.wiggleverse.org/api/health (deploys.id=2) | | gcloud active pointer | `benstull-infra` — untouched all session | | Slice 3 ledger | Status | | --- | --- | | provision-vm (§6.7) | ✅ built + dogfooded green | | scaffold-gitea-repos (§6.8) | ✅ built (repo created by hand in UI — org push-create off) | | Proof: VM up + /api/health + no-op re-run | ✅ met | --- ## §19.2 / Slice-5 candidates surfaced 1. **One-Name vs the `--app` repo convention (§3.3)** — repo name and deployment/One-Name diverge; decide whether §3.3 blesses it (OHM-grandfathered shape) or enforces equality. 2. **ADC quota-project is global** — scaffold-gcp-project's `set-quota-project` re-points the shared ADC per app; next OHM session must re-pin. Needs a per-process mechanism or a documented re-pin. 3. **Standard-stack run command not in the §5.1 schema** — provision assumes `uvicorn app.main:app` :8001; add optional `[vm].entrypoint`/`port` if a 2nd app diverges. 4. **scaffold-gitea-repos needs a token** to create org repos (org push-create disabled); the dogfood created the repo by hand. 5. **`APP_URL` overlay key** emitted but unused by wiggle-snip (Slice-2 carry-over). --- ## What lands on the operator's plate 1. **Open two PRs** (no Gitea token in session): - engineering: https://git.wiggleverse.org/wiggleverse/engineering/compare/main...slice3-provision-vm-scaffold-gitea - flotilla-core: https://git.wiggleverse.org/wiggleverse/flotilla-core/compare/main...slice2-schema-validate 2. **Optional cleanup:** delete the now-unused read-only deploy key on wiggleverse-wiggle-snip-app, and the old `ppe.wiggle-snip` DNS record if one was added. 3. **Re-pin ADC quota project** before the next OHM secret operation (`gcloud auth application-default set-quota-project wiggleverse-ohm`). 4. wiggle-snip pre-prod is live + billable — tear down at Slice 5 or when done. --- ## Prompt the operator can paste into the next Claude Code session ``` launch-app Slice 4 — orchestrator + design-phase skills (SPEC §3, §4, §6.0–§6.4). Reserve your own session ID first (claim-session-id.sh). State (after Slice 3 / session 0061): - Slices 1–3 DONE. All four phase skills exist under ~/projects/wiggleverse/engineering/launch-app/skills/: scaffold-gcp-project (§6.5), define-deployment (§6.6), provision-vm (§6.7), scaffold-gitea-repos (§6.8). - flotilla-core Slice-2 work + the session-0061 edge-host fix is on branch slice2-schema-validate (worktree ~/projects/wiggleverse/flotilla-core-slice2, 371 tests green, pushed to origin, NOT merged/tagged). Engineering Slice-3 work is on branch slice3-provision-vm-scaffold-gitea (pushed, PR pending). Open both PRs. - wiggle-snip is LIVE pre-prod: repo wiggleverse/wiggleverse-wiggle-snip-app (private), VM wiggle-snip-ppe (project wiggle-snip, IP 35.255.99.91), deployed v0.1.0, green at https://wiggle-snip-ppe.wiggleverse.org/api/health. Task: build the launch-app orchestrator (state from on-disk artifacts §4.1, the §4.2 gates, §4.3 "teach the map", §6.0 intake) + the design-phase skills (scaffold-prototype, write-spec, write-roadmap, scaffold-standard-stack), and extend scaffold-gcp-project to confirm-not-assume the stack. Tests: orchestrator resumes from each partial state; gates refuse out-of-order phases. Conventions to honor (hardened in Slice 3): - Repos PRIVATE by default (guide §5.5); VM clones over HTTPS with a read token from Secret Manager via a git credential helper (NO SSH deploy keys — Cloudflare-proxied Gitea won't pass SSH :2222 to a GCE VM). - Edge host is -ppe. (single label — Cloudflare Universal SSL doesn't cover 3-label hosts). nginx serves :80+:443 (no hard redirect). - gcloud isolation: CLOUDSDK_ACTIVE_CONFIG_NAME per-process; never touch the active pointer. Hard secrets rule: pbpaste/stdin gestures only, never ask for bytes. - Parallel preview-environments line owns flotilla-core main + feat/preview-environments; only ever touch the slice2 worktree. Slice-5 backlog: One-Name-vs-repo-convention (§3.3); global ADC quota-project re-pin; [vm].entrypoint/port schema gap; scaffold-gitea-repos org-repo-create token; unused APP_URL overlay key. wiggle-snip pre-prod is live + billable — tear down at Slice 5. Bump SPEC to v0.0.3 when next touched (its §2/§5.1/§5.3 text + guide §5.5 changed). ```