Denying the whole <root>/profiles tree 403'd legitimate named-profile workspace
media (<base>/profiles/p1/workspace/shot.png). Fix: remove 'profiles' from
_DENY_SUBDIRS and instead enumerate each <root>/profiles/<name> directory as its
own Hermes root — so each profile's sensitive subdirs (sessions/memories/cron/
logs/checkpoints/backups) + secret filenames are denied, while that profile's
workspace/ is allowed via the carve-out. Adds a regression test: named-profile
workspace media serves, profile + sibling-profile auth.json stay 403.
Codex round-4: the carve-out could re-open the hole if the active workspace is
pathologically set to a broad/internal root ($HOME, ~/.hermes, a profile root)
— get_last_workspace only checks is_dir(), so workspace=~/.hermes would serve
state.db. Gate the carve-out: disable it when the active workspace IS, CONTAINS,
or is CONTAINED BY any Hermes root, or is $HOME / a */profiles dir / a named
profile root / an internal state subdir. Adds a unit test proving state.db stays
403 when the active workspace is the Hermes home. Widen CSP-slice test window.
Under a named profile, process HERMES_HOME is ~/.hermes/profiles/<name> but the
allowlist still grants base ~/.hermes — so the prior deny (anchored only on the
active-profile root + STATE_DIR) left ~/.hermes/state.db and sibling-profile
secrets (~/.hermes/profiles/other/auth.json) reachable. Build deny roots from
every Hermes state root the allowlist accepts: active HERMES_HOME, base ~/.hermes,
api.profiles._DEFAULT_HERMES_HOME, and STATE_DIR; apply the state-subdir dir-denies
under each. Widen the CSP-slice structural test window to match.
1. test_issue1362_codex_oauth_onboarding.py::test_anthropic_onboarding_setup_allows_linked_oauth_without_api_key
Pre-existing env-collision bug, surfaced when HERMES_WEBUI_SKIP_ONBOARDING=1
is in the test runner env (set by hosting providers and by isolated test
harnesses). `apply_onboarding_setup()` short-circuits without writing the
config file when SKIP_ONBOARDING is set, but the test asserts the file was
written, so it fails with FileNotFoundError on read_text().
Fix: `monkeypatch.delenv("HERMES_WEBUI_SKIP_ONBOARDING", raising=False)` —
matches the convention already used in test_issue1499_keyless_onboarding.py
and test_issue1500_lmstudio_env_var_alignment.py.
2. test_issue1800_file_html_interactions.py::test_media_html_inline_keeps_csp_sandbox
Slicing-based source-string assertion (4000-char window after `def _handle_media`)
broke because PR #2044's MEDIA_ALLOWED_ROOTS parsing was inserted earlier in
the function and pushed the CSP block to offset 4211. Widened window to 5000.
Assertion content is structural (CSP sandbox string present), not positional.