stage-257 batch (PRs #1402 + #1415):
Opus pre-release advisor caught 4 issues in stage-257:
1. MUST-FIX (security): api/oauth.py::_write_auth_json — tmp.replace()
preserves the temp file umask (0644 default), so OAuth access/refresh
tokens landed world-readable on shared systems. Fix: tmp.chmod(0o600)
BEFORE rename, with try/except OSError that warns but does not abort.
2. SHOULD-FIX: _handle_cron_history and _handle_cron_run_detail accepted
job_id as a path component without validation. Mirrors the rollback
path-traversal vector caught in v0.50.255 (#1405). Path() / .. does NOT
normalize. New regex ^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,63}$ with explicit
. / .. rejection.
3. SHOULD-FIX: _handle_cron_history int(offset)/int(limit) raised
ValueError on malformed input → confusing 500. Now try/except + clamp
to (max(0, offset), max(1, min(500, limit))).
4. NIT: same regex applied to _handle_cron_run_detail (defense-in-depth
even though path-resolve check would catch it downstream).
PR #1415 follow-up: 8 pre-existing tests in test_issue1106 and
test_custom_provider_display_name asserted bare model IDs but #1415
changes named-custom-provider IDs to @custom:NAME:model form when active
provider differs. Tests updated to use _strip_at_prefix helper to keep
checking the same invariant in the new shape.
4 regression tests in test_v050257_opus_followups.py + 8 fixed pre-existing
tests. Full suite: 3602 passed, 0 failed.
Fixes the CI failure introduced by #817: test_model_resolver::test_custom_endpoint_uses_model_config_api_key_for_model_discovery was failing with KeyError: 'auth' due to the 60s TTL cache in get_available_models() being populated by test_byok_model_dropdown.py tests that ran earlier. Added autouse _isolate_models_cache fixture to 5 test files. Full suite 1747/1747, QA harness green.
## Summary
Fixes the ordering-dependent test failure in `test_custom_provider_display_name.py` (issue #754).
**Root cause:** `_models_with_cfg()` patches `config.cfg` then calls `get_available_models()`. That function checks `config.yaml`'s mtime on every call — if it has changed since the last `reload_config()`, it calls `reload_config()` again, which reads from disk and silently overwrites the patch. Any test that writes `config.yaml` (e.g. via `save_settings()`) before this test runs changes the mtime and triggers the reload.
**Fix:** Pin `config._cfg_mtime` to the current `config.yaml` mtime before calling `get_available_models()`, then restore it in the `finally` block. This is the same pattern already used in `test_model_resolver.py` (lines 249, 393).
**Also restores `_cfg_mtime`** in the `finally` block so the patch leaves no side effects on subsequent tests.
## Tests
1510 passed — the previously-flaky test now passes regardless of which tests ran before it.
Closes#754
When a custom_providers entry in config.yaml has a 'name' field (e.g. 'Agent37'),
the web UI model picker now uses that name as the group header instead of the
generic 'Custom' label.
Previously all custom_providers entries were bucketed under 'custom' which
rendered as 'Custom' in the dropdown optgroup — losing the named identity the
user set up during onboarding.
Changes:
- Track named custom providers as 'custom:<slug>' keys internally so multiple
named providers can coexist as separate groups
- When building model groups, emit each named provider under its own display
name (e.g. 'Agent37') rather than falling through to the generic label
- Unnamed entries (no 'name' field) still fall back to the 'Custom' group
- When all entries are named, the bare 'Custom' bucket is suppressed
Adds 7 tests covering single named provider, multiple named providers,
multiple models in same named provider, unnamed fallback, and mixed cases.
Fixes#557