Module adcp.decisioning.observed_modes

Process-scoped tracker of explicit Account.mode values returned from :meth:AccountStore.resolve during framework-side comply-controller dispatch.

Used by the sandbox gate's deprecated env-fallback path (ADCP_SANDBOX=1) to fail closed when a process has resolved any explicit live-mode account.

Rationale: the env-fallback exists for back-compat with adopters that have not yet adopted the per-account mode field. If those adopters have ALSO begun returning explicit mode='live' from their resolver, the env var is a misconfiguration — leaving it set would re-open the gate for live principals after the resolver was meant to close it.

Implicit-default live (resolver returns no mode field, so the dataclass default 'live' applies) is NOT observed here — those adopters are exactly who the env-fallback bridge exists for. Only the deliberate explicit mode (resolver populated mode='...' and stamped _mode_explicit=True) trips the guard.

Multi-tenant blast radius. This tracker is process-scoped, not tenant-scoped. In a multi-tenant SaaS process (salesagent-style — many tenants share one Python process), if ANY tenant's resolver returns an explicit mode='live' account, every subsequent comply-controller call that would admit only via ADCP_SANDBOX=1 raises RuntimeError across ALL tenants. SaaS adopters MUST NOT set ADCP_SANDBOX=1 in shared dev/staging environments where some tenants may resolve live accounts; either gate per tenant via mode='sandbox' on the resolved account or run sandbox tenants in a separate process. JS takes the same posture; the cross-tenant tripping is intentional (silent admission under env-fallback would be the worse failure mode) but worth surfacing.

Mirrors the JS-side src/lib/server/decisioning/runtime/observed-modes.ts.

Functions

def has_observed_live_mode() ‑> bool
Expand source code
def has_observed_live_mode() -> bool:
    """``True`` when this process has observed at least one explicit
    ``mode='live'`` account from :meth:`AccountStore.resolve`.

    The sandbox-gate env-fallback consults this to decide whether
    ``ADCP_SANDBOX=1`` is a safe legacy bridge or a misconfiguration.
    """
    return "live" in _observed

True when this process has observed at least one explicit mode='live' account from :meth:AccountStore.resolve.

The sandbox-gate env-fallback consults this to decide whether ADCP_SANDBOX=1 is a safe legacy bridge or a misconfiguration.

def record_resolved_account_mode(account: Any) ‑> None
Expand source code
def record_resolved_account_mode(account: Any) -> None:
    """Record an account returned from :meth:`AccountStore.resolve`.

    Only counts EXPLICIT mode values — those where the resolver
    deliberately populated ``mode`` (signaled via the
    ``_mode_explicit=True`` flag on the framework's :class:`Account`
    dataclass). Adopters whose resolvers don't yet stamp ``mode`` keep
    working with the env-fallback bridge: their accounts read as
    implicit live, which doesn't trip this guard.

    No-op when ``account`` is ``None`` / not an account-shaped value /
    lacks an explicit mode flag / mode is not a known string. The set
    only collects deliberate, resolver-stamped mode values.
    """
    if account is None:
        return
    # Only count explicit modes — implicit-default live (legacy adopter
    # whose resolver didn't populate mode) is the back-compat target the
    # env-fallback exists for, not a misconfiguration.
    if not getattr(account, "_mode_explicit", False):
        return
    mode = getattr(account, "mode", None)
    if mode in ("live", "sandbox", "mock"):
        _observed.add(mode)

Record an account returned from :meth:AccountStore.resolve.

Only counts EXPLICIT mode values — those where the resolver deliberately populated mode (signaled via the _mode_explicit=True flag on the framework's :class:Account dataclass). Adopters whose resolvers don't yet stamp mode keep working with the env-fallback bridge: their accounts read as implicit live, which doesn't trip this guard.

No-op when account is None / not an account-shaped value / lacks an explicit mode flag / mode is not a known string. The set only collects deliberate, resolver-stamped mode values.