---ci---
phase: 7
milestone: v0.3
status: review
lessons:
- P0 CORS: allow_methods lacked PUT while the build surface writes files with PUT — every cross-origin Save failed preflight; pinned with tests/api/test_cors.py
- P0 ingest leak: queue-overflow flood path returned without the disconnect sentinel, parking the drainer forever (one leaked task-set per flooded trace); sentinel now always enqueued, real-server regression test added
- P1 perf: flood cap counted rows via len(get_trace(...)) — O(trace) per append, O(n²) per session; TraceStore.count() (COUNT(*)) added and wired
- P0 security: file routes followed exec-planted symlinks out of the workspace bind; _resolve_in_workspace refuses escapes (422), read/write now 404 on unknown sandboxes (was 500)
- P1 security: WS ingest accepted any browser Origin (CORS middleware does not cover WS); localhost dev origins + no-Origin (capture agent) allowed, others 1008
- P1 correctness: use-sandbox-session leaked a created sandbox on any mid-start failure (per-learner cap 1 → all retries 429 forever); failed starts now destroy what they created
- P2 testing: reconnect-flush test killed mid-burst (nondeterministic under load, reproduced on pre-change code); now waits for server-side observation of the pre-kill burst — the underlying one-line replay-margin/ACK gap is documented for v0.4
- maintainability: grading-store/templates/grading.ts docstrings claimed grading is variant-blind (stale pre-P4 text) — updated; ARCHITECTURE.md referenced nonexistent voice/openai_audio.py; dead if TYPE_CHECKING: pass blocks removed
---/ci---
Four gaps found by independent verifier probing of the defense endpoints
(all Must-Have-relevant, all trivially fixed):
1. Browser-mode descriptor was dead code: BROWSER_FALLBACK_DESCRIPTOR
existed but start always returned mode='mock' even with
AI_VOICE_PROVIDER=browser (Must-Have #6 violated). start now derives
the descriptor from settings.voice_provider (D-030).
2. answer after finish returned 200 and appended turns to a sealed
transcript — the store explicitly assigns sequencing to the endpoints
(defense_store.py: 'turns after finalize are a sequencing bug for the
endpoints to prevent, task 5-3-01'); the endpoints didn't. Now 409.
3. Zero-byte audio upload crashed the mock provider (MockVoiceFailure ->
500); a real provider would 500 the same way. Empty upload is a client
error: 422 before any provider call (provider contract unchanged).
4. Verdict was NOT persisted (Must-Have #1 'verdict + transcript
persisted'): finish persisted only signals; GET after finish could not
re-serve the verdict. The verdict now nests in integrity_signals
(JSON-object dict per the DefenseStore.finalize contract).
3 regression tests added (empty-audio 422, post-finish 409, verdict
retrievable from GET; browser-descriptor test). Suite 386 green; ruff clean.
---ci---
phase: 5
milestone: v0.3
status: verify
requirements:
covered: [REQ-3-006]
partial: []
lessons:
- A descriptor that exists but is never served is indistinguishable from
dead code until you probe the configured mode end-to-end (factory tests
proved selection, not service).
- Store contracts that 'assign' sequencing to callers need an endpoint
test for the forbidden transition, or the assignment is decorative.
---/ci---