Sweep findings #2 and #7 (plans/admin-portal-functional-sweep.md). #2. Every admin request body now inherits `_AdminBody` with `extra="forbid"`. This is the rule StrictModel already enforces for the config models, for the reason CLAUDE.md gives: with Pydantic's default a typo "loads cleanly, does nothing, and still looks configured". On this surface the default was destructive, not merely untidy. `_CloudFallbackBody` takes a nested `{"cloud_fallback": {...}}`, so a caller sending the flat shape the field names suggest had every key discarded, leaving the field None -- which is the documented signal to REMOVE the block. Trying to save a cloud classifier deleted it and returned 200 "A restart is required for this change to take effect". The quiet version: `POST /api/profiles/` with `{"name":"x","min_teir":3}` returned 200 and created a profile with no filters at all. Note the asymmetry this removes. The inner dict was already strict, because RouterConfig validates it on the way to disk -- `{"timeout_secondz": 20}` correctly 422'd. Only the outer wrapper was loose, so validation got stricter the deeper you went. Strictness immediately found a real client/server mismatch: the profiles UI sent `name` on the update path, which `_ProfileUpdateBody` has no field for (the name is in the URL; the endpoint cannot rename). It was being silently dropped. Both save paths now strip it. Verified in the browser -- inline edit and duplicate-then-create both still save, with allowed_model_ids preserved. #7. `POST /api/providers/{name}` had no provenance check at all, making it the one way to write to something the portal itself labels "base config / read-only". The result could not be undone from the portal: the write forks the provider into config.local.yaml, and DELETE then refuses if it is the default_provider, so hand-editing the one file in this deployment that is not in git was the only way back. It now refuses a base-only definition exactly as delete does. One POST serves both create and update, so this also means an overlay entry cannot shadow a base provider -- matching admin_profile_create, which refuses that with a 409. Which exposed a smaller thing worth fixing: both profile 403s advised "add an overlay profile with the same name to override it", and admin_profile_create rejects precisely that. The message now says what the API actually supports. The new provider test also demonstrates the fixture shape that fixes sweep finding #12 -- pointing `base_dir` at tmp_path isolates a test from the machine's real config.local.yaml. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
5.4 KiB
5.4 KiB