"""User identity extraction from incoming requests. Provides a pluggable :class:`AuthProvider` ABC and a :class:`UnifiedAuthProvider` that supports three identity sources, selected via the ``OMNIGENT_AUTH_PROVIDER`` env var: - ``"header"`` (default): reads the ``X-Forwarded-Email`` header from a trusted upstream proxy (override the header name with ``OMNIGENT_AUTH_HEADER``, e.g. ``Cf-Access-Authenticated-User-Email`` for Cloudflare Access). Requests without the header are rejected (401) unless the server was explicitly started as a single-user local runtime (``OMNIGENT_LOCAL_SINGLE_USER=1``), in which case they fall back to the reserved ``"local"`` user. - ``"oidc"``: reads the ``__Host-ap_session`` signed cookie minted after a full OIDC authorization-code+PKCE login flow. - ``"accounts"``: same signed cookie machinery as OIDC, but minted by the built-in username+password ``/auth/login`` endpoint. The ``accounts`` provider is the OSS-CUJ-v2 default — first-user-is-admin with invite-only signup; see ``designs/oss-cuj/04-implementation-plan.md``. Cookie validation is identical across OIDC and accounts modes — both share :class:`AccountsConfig`/:class:`OIDCConfig`-shaped cookie parameters. The provider is instantiated once at server startup and closed over by route factories — no per-request import cost. """ from __future__ import annotations import logging import os import time from abc import ABC, abstractmethod from enum import Enum from starlette.requests import HTTPConnection logger = logging.getLogger(__name__) # Opt-in multi-user switch. ``OMNIGENT_AUTH_ENABLED`` is the current # name; ``OMNIGENT_ACCOUNTS_ENABLED`` is the pre-rename name, still # honored as a deprecated alias (see :func:`_auth_enabled`). _AUTH_ENABLED_ENV = "OMNIGENT_AUTH_ENABLED" _AUTH_ENABLED_ENV_DEPRECATED = "OMNIGENT_ACCOUNTS_ENABLED" RESERVED_USER_LOCAL = "local" RESERVED_USER_PUBLIC = "__public__" _RESERVED_USERS = frozenset({RESERVED_USER_LOCAL, RESERVED_USER_PUBLIC}) _TRUTHY_STRINGS = ("1", "true", "yes") # Explicit single-user marker. Set by the managed local-server spawn # paths (`omnigent run` in chat.py, the daemon's # host/local_server.py) and by the canonical bare loopback # `omnigent server` (cli.py) — never by deployed multi-user servers. # Gates the header-mode "local" fallback (see # :meth:`UnifiedAuthProvider._check_header`) and host_id re-owning in # routes/host_tunnel.py. _LOCAL_SINGLE_USER_ENV = "OMNIGENT_LOCAL_SINGLE_USER" # Name of the trusted identity header read in header-auth mode. # Overridable so deploys behind a proxy that uses a different header # name (e.g. Cloudflare Access' ``Cf-Access-Authenticated-User-Email``) # work without an extra proxy transform. Defaults to the oauth2-proxy / # Databricks Apps convention. See :func:`resolve_auth_header`. _AUTH_HEADER_ENV = "OMNIGENT_AUTH_HEADER" _DEFAULT_AUTH_HEADER = "X-Forwarded-Email" # Optional prefix stripped from the identity header value in header-auth # mode. Some trusted proxies namespace the identity they inject — most # notably Google IAP, whose ``X-Goog-Authenticated-User-Email`` carries an # ``accounts.google.com:`` prefix (value # ``accounts.google.com:user@example.com``). Stripping it yields the bare # email used everywhere else. Unset (the default) strips nothing. See # :func:`resolve_auth_header_strip_prefix`. _AUTH_HEADER_STRIP_PREFIX_ENV = "OMNIGENT_AUTH_HEADER_STRIP_PREFIX" LEVEL_READ = 1 LEVEL_EDIT = 2 LEVEL_MANAGE = 3 LEVEL_OWNER = 4 class SharingMode(str, Enum): """Server policy for creating new session permission grants. - ``ON``: grants at any level (read/edit/manage) plus workspace/public read. - ``READ_ONLY``: grants are capped at read (view) — edit/manage grants are rejected; workspace/public read still allowed. - ``RESTRICTED_READ_ONLY``: like ``READ_ONLY`` (grants capped at read), but sessions whose working directory is a user home directory or the filesystem root (see :func:`workspace_sharing_blocked`) cannot be shared at all — not even read — because that cwd exposes an entire home/filesystem. - ``OFF``: no new grants at all. Value is the lowercase name so ``GET /v1/info`` and the ``OMNIGENT_SHARING_MODE`` env var round-trip it directly. Defaults to ``ON``. """ OFF = "off" READ_ONLY = "read_only" RESTRICTED_READ_ONLY = "restricted_read_only" ON = "on" @classmethod def coerce(cls, value: object) -> SharingMode: """Map a ``SharingMode``/str/``None`` to a mode, failing open to ``ON`` for anything unset or unrecognized (env-var parse + callable boundary).""" if isinstance(value, cls): return value if isinstance(value, str): try: return cls(value.strip().lower()) except ValueError: return cls.ON return cls.ON # Directories whose *direct children* are user home directories, across the # Unix / macOS / container layouts a runner might use: ``/home`` (Linux), # ``/Users`` (macOS), and ``/var/home`` (ostree — Silverblue/CoreOS/Flatcar, # where ``/home`` symlinks here). Matched by path *shape*, never by resolving # ``~``: the runner and its home may live on a different host than this server # process, so the local process's home is not a reliable signal. Deliberately # excludes project-workspace roots (``/workspace``, ``/workspaces/``) — # those hold a single checkout, not a whole home, and stay shareable. _HOME_PARENT_DIRS = ("/home", "/Users", "/var/home") # Absolute paths that are themselves a home or the filesystem root. _BLOCKED_WORKSPACE_ROOTS = ("/", "/root") def workspace_sharing_blocked(workspace: str | None) -> bool: """True when a session's working directory is too broad to share under :attr:`SharingMode.RESTRICTED_READ_ONLY` — the filesystem root or a user home directory, whose whole contents a grant would expose. Recognizes the filesystem root (``/``), root's home (``/root``), and any direct child of a common home parent (see :data:`_HOME_PARENT_DIRS` — e.g. ``/home/alice``, ``/Users/bob``, ``/var/home/carol``). A subdirectory of a home (``/home/alice/proj``) is shareable, as is a ``None``/empty workspace (no recorded cwd). Pattern-based on purpose: the runner (and thus the home the session lives in) may be on a different host than this server process, so only the path shape is reliable — resolving the local ``~`` would test the wrong host. """ if not workspace: return False path = os.path.normpath(workspace) if path in _BLOCKED_WORKSPACE_ROOTS: return True parent, _, leaf = path.rpartition("/") return bool(leaf) and parent in _HOME_PARENT_DIRS def env_var_is_truthy(name: str, *, default: bool = False) -> bool: """Parse a boolean-style environment variable. Truthy values match the existing harness env-var convention: ``"1"``, ``"true"``, and ``"yes"`` are true case-insensitively. Unset or empty values return ``default``; every other value is false. :param name: Environment variable name. :param default: Value to return when the variable is unset or empty. :returns: Parsed boolean value. """ raw = os.environ.get(name, "").strip().lower() if not raw: return default return raw in _TRUTHY_STRINGS def local_single_user_enabled() -> bool: """Whether this server is an explicit single-user local runtime. Reads ``OMNIGENT_LOCAL_SINGLE_USER``, the marker the managed local spawn paths set when starting THE user's own loopback server. Deployed multi-user servers never set it, so everything it gates (header-mode ``"local"`` fallback, host_id re-owning) stays fail-closed there. :returns: ``True`` when the single-user marker is set and truthy. """ return env_var_is_truthy(_LOCAL_SINGLE_USER_ENV) def resolve_auth_header() -> str: """Resolve the trusted identity header name for header-auth mode. Reads ``OMNIGENT_AUTH_HEADER`` and falls back to :data:`_DEFAULT_AUTH_HEADER` (``X-Forwarded-Email``) when unset or empty. Header names are case-insensitive per RFC 7230, so the value is used as-is — Starlette's ``request.headers`` lookup is itself case-insensitive. The override exists so a deploy behind a proxy that authenticates with a differently-named header can point the server at it directly, e.g. ``OMNIGENT_AUTH_HEADER=Cf-Access-Authenticated-User-Email`` for Cloudflare Access, instead of standing up an extra hop to rename the header to ``X-Forwarded-Email``. :returns: The header name to read identity from in header mode. """ raw = os.environ.get(_AUTH_HEADER_ENV, "").strip() return raw or _DEFAULT_AUTH_HEADER def resolve_auth_header_strip_prefix() -> str: """Resolve the prefix stripped from the identity header value. Reads ``OMNIGENT_AUTH_HEADER_STRIP_PREFIX`` and returns it (surrounding whitespace trimmed), or ``""`` when unset or empty — the default, meaning the header value is used as-is. The motivating case is Google IAP: point ``OMNIGENT_AUTH_HEADER=X-Goog-Authenticated-User-Email`` at IAP's identity header and set ``OMNIGENT_AUTH_HEADER_STRIP_PREFIX=accounts.google.com:`` so the namespaced value ``accounts.google.com:user@example.com`` resolves to the bare ``user@example.com``. Kept generic rather than IAP-specific so any proxy that namespaces its identity header is supported. :returns: The prefix to strip, or ``""`` to strip nothing. """ return os.environ.get(_AUTH_HEADER_STRIP_PREFIX_ENV, "").strip() _auth_enabled_deprecation_warned = False def _auth_enabled() -> bool: """Whether multi-user auth is opted in via the enable switch. Reads ``OMNIGENT_AUTH_ENABLED``. The pre-rename name ``OMNIGENT_ACCOUNTS_ENABLED`` is still honored as a deprecated alias: when it is set and the current name is not, its value is used and a one-time deprecation warning is logged. The current name always wins when both are set, so a deploy migrating to the new name can leave the old one in place without surprise. Both names share the same truthiness rules (see :func:`env_var_is_truthy`) and the same explicit-falsy kill-switch semantics — ``OMNIGENT_AUTH_ENABLED=0`` disables auth even though the var is "set", which is how the Docker entrypoint lets an operator opt back out of the default-on accounts mode. :returns: ``True`` when multi-user auth should be enabled. """ global _auth_enabled_deprecation_warned if os.environ.get(_AUTH_ENABLED_ENV, "").strip(): return env_var_is_truthy(_AUTH_ENABLED_ENV, default=False) if os.environ.get(_AUTH_ENABLED_ENV_DEPRECATED, "").strip(): if not _auth_enabled_deprecation_warned: logger.warning( "%s is deprecated; rename it to %s. The old name still " "works for now but will be removed in a future release.", _AUTH_ENABLED_ENV_DEPRECATED, _AUTH_ENABLED_ENV, ) _auth_enabled_deprecation_warned = True return env_var_is_truthy(_AUTH_ENABLED_ENV_DEPRECATED, default=False) return False def resolve_auth_source() -> str: """ Resolve the server's auth provider source from the environment. Single source of truth for the auth-mode decision so every spawn path (``create_auth_provider`` here, the daemon-owned local server in ``host/local_server.py``, and the per-command server in ``chat.py``) agrees on which mode a server boots in. The rules mirror :func:`create_auth_provider`: - An explicit ``OMNIGENT_AUTH_PROVIDER`` (case-insensitive) always wins, e.g. ``"header"`` / ``"oidc"`` / ``"accounts"``. This is the low-level escape hatch. - Otherwise ``header`` is the default, unless the opt-in switch ``OMNIGENT_AUTH_ENABLED`` is truthy (see :func:`_auth_enabled`, which also honors the deprecated ``OMNIGENT_ACCOUNTS_ENABLED`` alias). When enabled, the mode depends on whether OIDC config was supplied: - ``OMNIGENT_OIDC_ISSUER`` is set → ``"oidc"`` (the operator brought their own IdP). The issuer is the canonical, always- required OIDC identifier; :func:`OIDCConfig.from_env` then fails loud if the rest of the OIDC config is missing. - otherwise → ``"accounts"`` (the built-in username+password login flow). :returns: The resolved source string, e.g. ``"accounts"``, ``"header"``, or ``"oidc"`` (or any explicit lower-cased value of ``OMNIGENT_AUTH_PROVIDER``). The caller is responsible for rejecting unknown values. """ raw_source = os.environ.get("OMNIGENT_AUTH_PROVIDER") if raw_source and raw_source.strip(): return raw_source.strip().lower() # Opt-in multi-user — see create_auth_provider's docstring. if _auth_enabled(): # An operator-supplied OIDC issuer selects the native # authorization-code flow; otherwise the built-in accounts flow. if os.environ.get("OMNIGENT_OIDC_ISSUER", "").strip(): return "oidc" return "accounts" return "header" class AuthProvider(ABC): """Extract a user ID from an incoming request. Implementations must return a user ID string or ``None``. When ``None`` is returned, the route helpers respond with 401. """ @abstractmethod def get_user_id(self, request: HTTPConnection) -> str | None: """Return the authenticated user ID, or ``None``.""" ... def mint_runner_token(self, user_id: str, ttl_seconds: int) -> str | None: # noqa: ARG002 """ Mint a short-lived bearer a managed-sandbox runner presents as *user_id*. A managed runner runs in a sandbox with no logged-in user credential of its own, so the server mints one for its HTTP callbacks when auth is enabled (see the ``POST /v1/runners/{id}/token`` endpoint). Default: ``None`` — no minting (single-user / no-auth, or a provider whose identity is asserted externally and can't be minted server-side, e.g. header/proxy auth). The runner then authenticates with its tunnel binding token alone. :param user_id: The session owner the runner acts as, e.g. ``"alice@example.com"``. :param ttl_seconds: Token lifetime in seconds. :returns: A bearer token string, or ``None`` when this provider cannot mint one. """ return None class UnifiedAuthProvider(AuthProvider): """Unified authentication provider that supports header-based, OIDC, and accounts cookie-based identity extraction. Exactly one source is active per deployment, selected by ``OMNIGENT_AUTH_PROVIDER``. OIDC and accounts modes share the same cookie machinery — the difference is only in how the cookie was minted (OIDC IdP callback vs ``/auth/login``). :param source: The active identity source: ``"header"``, ``"oidc"``, or ``"accounts"``. :param oidc_config: OIDC configuration. Required when ``source`` is ``"oidc"``, ``None`` otherwise. :param accounts_config: Accounts configuration. Required when ``source`` is ``"accounts"``, ``None`` otherwise. :param local_single_user: When ``True``, header mode falls back to the reserved ``"local"`` identity for requests without the identity header — the explicit single-user posture of the user's own loopback server. When ``False``, such requests are rejected (``None`` → 401, fail closed). ``None`` (the default) resolves from ``OMNIGENT_LOCAL_SINGLE_USER`` at construction (see :func:`local_single_user_enabled`). Only consulted in header mode. Tests pass an explicit bool. :param header_name: The trusted identity header read in header mode. ``None`` (the default) resolves from ``OMNIGENT_AUTH_HEADER`` at construction, falling back to ``X-Forwarded-Email`` (see :func:`resolve_auth_header`). Only consulted in header mode. Tests pass an explicit name. :param header_strip_prefix: A prefix stripped from the identity header value in header mode — e.g. ``accounts.google.com:`` so Google IAP's ``accounts.google.com:user@example.com`` resolves to the bare email. ``None`` (the default) resolves from ``OMNIGENT_AUTH_HEADER_STRIP_PREFIX`` at construction, falling back to ``""`` (strip nothing; see :func:`resolve_auth_header_strip_prefix`). Only consulted in header mode. Tests pass an explicit prefix. """ def __init__( self, source: str, oidc_config: OIDCConfig | None = None, accounts_config: AccountsConfig | None = None, local_single_user: bool | None = None, header_name: str | None = None, header_strip_prefix: str | None = None, ) -> None: self._source = source self._oidc_config = oidc_config self._accounts_config = accounts_config self._local_single_user = ( local_single_user if local_single_user is not None else local_single_user_enabled() ) self._header_name = header_name if header_name is not None else resolve_auth_header() self._header_strip_prefix = ( header_strip_prefix if header_strip_prefix is not None else resolve_auth_header_strip_prefix() ) self._cookie_cache: dict[str, tuple[str, float]] = {} @property def login_url(self) -> str | None: """Where the frontend should redirect on 401. - ``"oidc"`` → ``"/auth/login"`` (server-side GET that builds the PKCE state cookie and redirects to the IdP's authorize endpoint). - ``"accounts"`` → ``"/login"`` (SPA route — the React ``LoginPage`` renders a username + password form and POSTs to ``/auth/login``). Distinct from OIDC because accounts mode has no IdP handoff; the form lives in the browser. - ``"header"`` → ``None`` (no login page; missing identity is the proxy's responsibility). """ if self._source == "oidc": return "/auth/login" if self._source == "accounts": return "/login" return None def get_user_id(self, request: HTTPConnection) -> str | None: """Extract user identity from the active source. - ``"header"``: Read the configured identity header (default ``X-Forwarded-Email``; see :func:`resolve_auth_header`). - ``"oidc"`` / ``"accounts"``: Read ``__Host-ap_session`` cookie, validate HS256 signature and expiry, return ``sub`` claim. :param request: The incoming HTTP request or WebSocket handshake (both are ``HTTPConnection``). :returns: Authenticated user ID, or ``None`` (→ 401). """ if self._source in ("oidc", "accounts"): return self._check_cookie(request) return self._check_header(request) def mint_runner_token(self, user_id: str, ttl_seconds: int) -> str | None: """ Mint a short-lived owner JWT for a managed-sandbox runner. Accounts / OIDC modes sign a session JWT in the same HS256 format :meth:`_check_cookie` validates, so the runner can present it as ``Authorization: Bearer `` on its HTTP callbacks and resolve to *user_id*. Header/proxy mode returns ``None`` — identity there is asserted by the upstream proxy and can't be minted server-side. :param user_id: The session owner the runner acts as, e.g. ``"alice@example.com"``. :param ttl_seconds: Token lifetime in seconds. :returns: An HS256-signed JWT, or ``None`` for header mode, an empty/reserved user, or a missing cookie config. """ if not user_id or user_id in _RESERVED_USERS: return None if self._source not in ("oidc", "accounts"): return None cookie_config = self._oidc_config if self._source == "oidc" else self._accounts_config if cookie_config is None: return None from omnigent.server.oidc import mint_session_token return mint_session_token( user_id, cookie_config.cookie_secret, ttl_seconds, self._source, ) def _check_cookie(self, request: HTTPConnection) -> str | None: """Validate the session cookie or Bearer token and return the user ID. Checks the session cookie first (browser clients), then falls back to ``Authorization: Bearer `` (CLI clients authenticated via ``omnigent login``). Both carry the same HS256-signed JWT. Uses a TTL credential cache keyed by HMAC-SHA256 digest of the raw token to avoid repeated JWT decoding on every request. :param request: The incoming HTTP request or WebSocket. :returns: User ID from the JWT's ``sub`` claim, or ``None`` if no valid token is found. """ import jwt from omnigent.server.oidc import hmac_digest # Both OIDC and accounts modes use the same cookie machinery # — read the active config wherever it lives. The two configs # share `cookie_secret` and `session_cookie_name` properties # by construction (see AccountsConfig docstring). cookie_config = self._oidc_config if self._source == "oidc" else self._accounts_config cookie_name = cookie_config.session_cookie_name token = request.cookies.get(cookie_name) if not token: # Fall back to Bearer token for CLI clients. auth_header = request.headers.get("Authorization", "") if auth_header.startswith("Bearer "): token = auth_header[7:] if not token: return None cache_key = hmac_digest(token, cookie_config.cookie_secret) cached = self._cookie_cache.get(cache_key) if cached is not None and cached[1] > time.monotonic(): return cached[0] try: payload = jwt.decode( token, cookie_config.cookie_secret, algorithms=["HS256"], ) except jwt.InvalidTokenError: return None user_id = payload.get("sub") if not user_id or user_id in _RESERVED_USERS: return None # Cache for remaining lifetime of the token. remaining = payload.get("exp", 0) - time.time() if remaining > 0: self._cookie_cache[cache_key] = ( user_id, time.monotonic() + remaining, ) return user_id def _check_header(self, request: HTTPConnection) -> str | None: """Read the trusted identity header and return the user ID. The header name is :attr:`_header_name` (``X-Forwarded-Email`` by default, overridable via ``OMNIGENT_AUTH_HEADER`` — e.g. ``Cf-Access-Authenticated-User-Email`` for Cloudflare Access). When :attr:`_header_strip_prefix` is set (from ``OMNIGENT_AUTH_HEADER_STRIP_PREFIX``), it is removed from the front of the header value first — e.g. Google IAP's ``X-Goog-Authenticated-User-Email`` value ``accounts.google.com:user@example.com`` becomes the bare ``user@example.com``. A value that is only the prefix (empty after stripping) is rejected, like a reserved name. When the header is present, its value is used as the identity (reserved names like ``"local"`` are rejected). When absent, the request is rejected (``None`` → 401): a missing or dropped proxy header must fail closed, never resolve to a shared default identity that every unauthenticated request would then share. The one exception is the explicit single-user local runtime (``local_single_user=True``, from ``OMNIGENT_LOCAL_SINGLE_USER=1``): there the absent header falls back to :data:`RESERVED_USER_LOCAL`, because the server's only user IS the local user and no proxy exists to inject identity. :param request: The incoming HTTP request or WebSocket. :returns: User ID from the header; ``"local"`` when the header is absent on a single-user local runtime; else ``None`` (→ 401). """ email = request.headers.get(self._header_name) if email: if self._header_strip_prefix: email = email.removeprefix(self._header_strip_prefix) if not email or email in _RESERVED_USERS: return None return email if self._local_single_user: return RESERVED_USER_LOCAL return None def create_auth_provider() -> AuthProvider: """Factory: read ``OMNIGENT_AUTH_PROVIDER`` and return a :class:`UnifiedAuthProvider` configured for the selected source. Defaults to ``"header"`` when the env var is unset — a bare ``omnigent server`` is single-user, no-login out of the box. Header mode rejects requests without the configured identity header (default ``X-Forwarded-Email``, overridable via ``OMNIGENT_AUTH_HEADER``) — 401, fail closed; see :meth:`UnifiedAuthProvider._check_header` — unless the server is an explicit single-user local runtime (``OMNIGENT_LOCAL_SINGLE_USER=1``, set by the managed local spawn paths and the canonical bare loopback ``omnigent server``), where the absent header falls back to the reserved ``"local"`` user — the convenient posture for local development without minting cookies / typing passwords. Opt-in multi-user (accounts / OIDC) ----------------------------------- Set ``OMNIGENT_AUTH_ENABLED=1`` (or any truthy value) to turn on multi-user auth. With no OIDC config present this selects ``accounts`` mode — the built-in login flow with first-user-is-admin setup. Set the ``OMNIGENT_OIDC_*`` env vars (at minimum ``OMNIGENT_OIDC_ISSUER``) alongside it and the same switch instead selects ``oidc`` — the native authorization-code flow against your own IdP. Containerized / remote deploys (Docker, HF Spaces, Render, Railway) flip this on in their entrypoints so a deployed instance is authenticated by default; a bare local server leaves it off. An explicit ``OMNIGENT_AUTH_PROVIDER`` always wins over this switch — it only governs the env-unset default. Deploys behind an SSO proxy that injects ``X-Forwarded-Email`` set ``OMNIGENT_AUTH_PROVIDER=header`` (Databricks Apps, oauth2-proxy); proxies that authenticate with a different header name also set ``OMNIGENT_AUTH_HEADER`` (e.g. ``Cf-Access-Authenticated-User-Email`` for Cloudflare Access — see :func:`resolve_auth_header`). (``OMNIGENT_AUTH_ENABLED`` is the renamed opt-in gate, commit ``b23e886e``, formerly ``OMNIGENT_ACCOUNTS_ENABLED``: header is the shipped default, so the var is an enable switch, not a kill switch. The old name is still honored as a deprecated alias — see :func:`_auth_enabled`.) Validates the source's required env vars at startup (fail loud) — OIDC fetches the discovery document, accounts decodes the cookie secret. :returns: Configured auth provider. :raises RuntimeError: On unknown source or invalid config. """ source = resolve_auth_source() if source not in ("header", "oidc", "accounts"): raise RuntimeError( f"Unknown OMNIGENT_AUTH_PROVIDER={source!r}. Valid: 'header', 'oidc', 'accounts'" ) oidc_config: OIDCConfig | None = None accounts_config: AccountsConfig | None = None if source == "oidc": from omnigent.server.oidc import OIDCConfig oidc_config = OIDCConfig.from_env() elif source == "accounts": # Reaching here means accounts mode was deliberately selected # — either OMNIGENT_AUTH_PROVIDER=accounts or the # OMNIGENT_AUTH_ENABLED=1 opt-in without OIDC config # (resolved above). No second gate: the selection already # expressed intent. from omnigent.server.accounts_config import AccountsConfig accounts_config = AccountsConfig.from_env() return UnifiedAuthProvider( source=source, oidc_config=oidc_config, accounts_config=accounts_config, ) # Backwards-compatible re-export of forward-referenced config # types — both are imported lazily inside `create_auth_provider` # to keep startup cost off the import path that doesn't use them. if False: # TYPE_CHECKING equivalent without the import from omnigent.server.accounts_config import AccountsConfig from omnigent.server.oidc import OIDCConfig