# Sprint: audio upload follow-ups (Sweep Sprint, 2026-10) Method: Sweep Sprint (phase 1 parallel read-only research per issue: **done**; phase 2 sequential execution, one issue per commit: **v0.31.1 and v0.32.0 shipped 2026-10-03/04, the owner-held items below remain open**). Closing oath below. Milestones: [v0.31.1 - security and robustness](https://git.d-ma.be/mathias/tapir/milestone/10), [v0.32.0 - audio upload follow-ups](https://git.d-ma.be/mathias/tapir/milestone/11). Context: v0.31.0 shipped audio upload (ADR-033/034/035) and is live in production behind auth at `/uploads`, with the Berget cloud fallback on for audio (infra#522). A real 63.7 min upload transcribed in 2m53s with no errors. The phase-1 investigation found two pre-existing or new weaknesses an authenticated user can reach, so the first release is a hotfix. ## Oath (closing gates, every clause must hold; blocked = descoped and tracked, never "done") | # | Clause | Evidence | |---|--------|----------| | SG1 | Every issue in the sprint is closed with a linked commit, or moved out with a stated reason | issue list of both milestones | | SG2 | Each fix has a test that was seen red on main first, named after the mechanism it observes | red/green output in the issue | | SG3 | `task check` AND `golangci-lint run ./...` exit 0 (checked by `$?`, not by grep); `task check` does not gate lint until #41 | gate output | | SG4 | Control code (#48, #49, the upload cap in #47) has a recorded adversarial review verified by execution, verdict in the issue | gatekeeper verdict comment | | SG5 | Docs current in the same commit: ADR-033/035 text, feature file, use-case coverage test | diff | | SG6 | Each release tagged (v0.31.1, v0.32.0), pushed, deployed, and observed live (pod running the new image, no restart loop, a real upload still works) | `kubectl`/log + one upload | | SG7 | Learnings written to the brain; deferred work filed as issues; decisions that are the owner's recorded as DECISION NEEDED | brain ids + issue numbers | ## Work order ### v0.31.1 (security and robustness; hotfix, do first) 1. **#41 lint gate.** `task check` swallows lint findings (`|| echo`). Make the gate fail on findings, fix what it surfaces. First, so every later gate means something. (Decision D1.) 2. **#48 cross-origin POST protection.** `http.NewCrossOriginProtection()` around `Router()`. Red: foreign-origin POST to `/account/delete`, `/uploads`, `/uploads/{id}/chat` is refused. 3. **#49 ffprobe/ffmpeg limits.** `-max_streams 32` + narrower probe output; probe stdout cap; probe deadline. Three commits. 4. **#47 concurrent-upload cap (remaining item).** Bound the spool: reservation per accepted upload against the 6Gi emptyDir so concurrent uploads cannot evict the pod. 5. **#50 SIGTERM interim.** Handle SIGTERM; record the interrupted job at once; fix the false comment. Full drain recorded as a limitation. (Decision D2.) Release v0.31.1: one adversarial review round over 2-5, then tag, push, observe. ### v0.32.0 (follow-ups) 6. **#52** label a cloud-served transcript in plain words; document the empty-transcript trigger. 7. **#53** bound the LLM client's response body read. 8. **#51** tracker: save timeout, then app-clock heartbeat. 9. **#37** nav link to uploads and per-recording delete (queued/transcribing: refuse until it ends). (Decision D3.) 10. **#36** chat default model after measurement. (Decision D4.) 11. **#38** measurements (Berget length limit, large models on production iguana). (D5, D6.) 12. **#46** remainder of the hardening list after the items above are split out. 13. **#45** evaluate Berget as the reconciler: a question, not work. (D8.) ## DECISION NEEDED (owner's call; recommendation first) | # | Question | Recommendation | Blocks | |---|----------|----------------|--------| | D1 | #41: should any linter be allowed to opt out of the gate? | No opt-out; fix findings | #41 | | D2 | #50: full SIGTERM drain, or record as a limitation and ship the interim fix? | Limitation + interim; a drain costs 4-6 h and downtime on every deploy at current volume | #50 | | D3 | #37: delete of a queued/transcribing recording | Refuse until it ends | #37 | | D4 | #36: make a 31B model the chat default? | Only after measurement, conditional | #36 | | D5 | #38: Berget length measurement needs a throwaway key | Yes, throwaway key | #38 | | D6 | #38: load large models on production iguana | Yes, in a quiet window with the owner present | #38 | | D7 | #46: where the ffmpeg tarball lives | Do nothing now; file an issue | none | | D8 | #45: evaluate Berget as the reconciler | No; stay local-only | #45 | | D9 | Who judges ADR-034's second blind check | The owner; do not reuse the one-off Opus authorisation | ADR-034 | | D10 | When to enable reconcile in production | Needs `iguana/pianissimo` in the key pin and env vars; owner present | ADR-034 | ## Dropped or reduced, with reasons - **#46 item 1 (`alis`/`dref` external reference):** checked, not a problem. Under the application's flags ffmpeg skips the external track and writes a 78-byte WAV. - **#46 item 2 / #47 item 2:** merged into #49. - **#47 item 3 (full drain):** reduced to #50 plus a recorded limitation. - **#46 `Beat` taking the tracker mutex:** incorrect in #46; only `logStill` does. Corrected in #51. ## Human-present items (do not run unattended) - Production iguana model loads (#38, D6). - Enabling reconcile in production (D10). - Any key change or `apply` in infra (infra#523, infra#524 are open and not part of this sprint). ## Release plan SemVer: v0.31.1 patch (fixes, no new behaviour); v0.32.0 minor (nav link, delete, cloud labelling). Every push to tapir main redeploys the pod and fails any job in flight: push when no upload is running (check the pod log first). ## Status (2026-10-04) **Shipped and deployed:** v0.31.1 (PR #55, merge e037c6b) and v0.32.0 (PR #58, merge 85f4cc3; pod tapir-597dbfccc-ng78q 1/1, 0 restarts, image 85f4cc3). Each went through an adversarial review round by agent with the verdicts recorded on the PR (v0.31.1: BLOCK then APPROVE; v0.32.0: BLOCK, BLOCK, APPROVE, APPROVE; every block was a real defect, fixed test-first). **Live evidence:** a foreign-origin `POST` to `/account/delete` and to `/uploads/{id}/delete` is refused (403) on production, a same-origin one reaches the login redirect (302). **Not observed live (needs an authenticated session, so it is the owner's look; closed on word or screenshot):** - #37: the home page shows a "Recordings" link; a finished recording offers a confirm-then-delete control that removes it; a queued or transcribing one offers none. - #52: a transcript served by the cloud fallback shows the "audio left the homelab" note; a local one does not. A reconciled (two-engine) all-local transcript must NOT show it. **Observed live since:** #50: at the 2026-10-04 rollout the old pod went `Terminating` then `Completed` (not `Error`), 0 restarts; #50 closed. No job was in flight, so a drained job is still unobserved (a recorded limitation, D2). **Still open (owner's call, milestone 11):** #36, #38, #45, #46 remainder, #56 (slow-drip bodies hold a receive slot), #57 (shutdown logging, second signal). Decisions D4-D6, D8-D10 above are unchanged. **Deferred and filed from the reviews:** #56, #57, #59 (start-up exits when the identity provider's OIDC discovery returns 503 (seen once at 19:40 on 2026-10-03; the pod restarted itself and recovered)).