fix(memory/hindsight): pin the isolation-vs-shaping split for scoped reads

`langfuse._secret` and `azure_identity_adapter._scoped_env` were changed to
raise rather than fall back, because swallowing `UnscopedSecretError` hides the
spawn-site bug the exception exists to surface. `_scoped_setting` looked like it
contradicted that, so make the split explicit and pin it.

Hindsight already follows the contract for everything that decides WHERE data
goes: `mode`, `apiKey` and the `bankId` partition read through bare
`get_secret`, so a scopeless multiplexed read raises. In `_load_config` that
raise happens on `HINDSIGHT_MODE` before any shaping value is reached, so the
swallow below cannot mask an isolation failure.

Presentation shaping is deliberately not in that class. `MemoryManager._each_provider`
logs an `initialize` failure at WARNING and drops the provider for the session,
so raising there would cost the whole memory provider because a speaker prefix
could not be resolved. It degrades to the provider's own default instead —
never to `os.environ`, which under multiplex is the default profile's.

The test names the offending key rather than asserting that something raised:
routing `mode` through the shaping helper shifts the failure to
`HINDSIGHT_API_KEY`, which a bare `pytest.raises` would still accept.
This commit is contained in:
John Paul Soliva
2026-09-13 17:23:43 +09:00
committed by Teknium
parent a48dde5316
commit 2bade5daa7
2 changed files with 53 additions and 1 deletions
+10 -1
View File
@@ -64,12 +64,21 @@ def _ensure_client_dependency() -> None:
def _scoped_setting(name: str, default: str = "") -> str:
"""Profile-scoped read of a per-profile Hindsight setting, with the provider's own default on a miss.
"""Profile-scoped read of a retain SHAPING value, with the provider's own default on a miss.
Under ``gateway.multiplex_profiles`` ``os.environ`` holds the DEFAULT profile's ``.env``, so a miss
is a miss — never ``os.environ`` (same rule as the daemon's key and base URL in ``embedded.py``).
Single-profile deployments are unchanged: with no scope installed ``get_secret`` still reads the
process env, where the value IS this profile's own.
Deliberately narrower than a bare ``get_secret``: this helper is only for presentation shaping
(retain source label, speaker prefixes, tags). The isolation-critical values — ``mode``,
``apiKey`` and the ``bankId`` data partition — read through bare ``get_secret`` above and so
still fail loud on a scopeless multiplexed read, matching the other scoped credential readers.
In ``_load_config`` that read happens FIRST, so a missing scope raises on ``HINDSIGHT_MODE``
before this helper is ever reached; swallowing here therefore cannot mask an isolation failure.
What it does avoid is losing the whole memory provider (``initialize`` failing, and the manager
logging + dropping it) because a speaker prefix could not be resolved.
"""
try:
value = get_secret(name, default)