docs(agents): record multiplex profile-scoped env fail-closed rule (#86905)

Lesson from the feishu DM multiplex investigation: under multiplex,
os.environ holds the default profile's values, so any profile-level env
config (credentials AND authorization) must be read scope-aware, and a
scoped miss with a scope installed must fail closed instead of borrowing
from os.environ. The _get_scoped_secret wrapper is copy-pasted across
~15 platform adapters — new adapters and edits to existing ones must
keep the fail-closed semantics.
This commit is contained in:
SHT
2026-08-15 18:50:23 +08:00
committed by Teknium
parent 795e035f60
commit b560c0d241
+24
View File
@@ -1288,6 +1288,30 @@ automatically scope to the active profile.
This is intentional — it lets `hermes -p coder profile list` see all profiles regardless
of which one is active.
7. **Multiplex profile-scoped env reads MUST fail closed — never borrow from `os.environ`**
(`agent/secret_scope.py` contract; #72348, #86905). Under `gateway.multiplex_profiles`,
`os.environ` holds the **default profile's** values; a secondary profile's `.env` lives
only in its secret scope (installed per-turn by `_profile_runtime_scope`). Any
profile-level env config — credentials (`app_secret`, tokens) AND authorization
(`FEISHU_ALLOWED_USERS`, `{PLATFORM}_ALLOW_ALL_USERS`, `GATEWAY_ALLOW_ALL_USERS`,
`group_policy`, `allow_bots`, ...) — must be read scope-aware:
- Adapters: `_get_scoped_secret()` (canonical fail-closed copy in
`plugins/platforms/feishu/adapter.py`, #86905).
- Gateway authz: `_auth_env()` / `_platform_gate_env()` (`gateway/authz_mixin.py`).
Rules:
- Scope installed + multiplex active → a scoped miss returns the **default**.
NEVER fall through to `os.environ` — that leaks another profile's value and
silently breaks routing/admission (a leaked default allowlist skips the
allow-all check and rejects every secondary-profile sender, #86905).
- Unscoped default-profile path (`UnscopedSecretError`) and single-profile
deployments keep the `os.environ` read — there it IS the profile's own value.
- Authorization config is the sharpest edge: allowlist/allow-all leaks cause
silent rejections (or worse, fail-open) that only show up as missing replies.
- The `_get_scoped_secret` wrapper is copy-pasted across ~15 platform adapters —
when touching any of them, make sure the fail-closed semantics are present;
do not reintroduce the `except _UnscopedSecretError: val = os.getenv(...)`
fallback-after-miss shape.
## Known Pitfalls
### DO NOT hardcode `~/.hermes` paths