"""Configuration resolution for the opencode-bridge extension. Connection fields (URL / user / password / request timeout) are resolved in the following priority order, highest first: 1. ``--OpencodeConfig.`` CLI flag (e.g. ``--OpencodeConfig.url=...``) 2. ``c.OpencodeConfig.`` line in a Jupyter config file 3. ``OPENCODE_BRIDGE_*`` environment variable 4. Built-in default The ``OpencodeConfig`` class is a ``traitlets.config.Configurable``; the Jupyter server extension instantiates it with ``parent=server_app`` and stashes it on ``web_app.settings["opencode_bridge"]`` so handlers can pick it up via ``resolve_config``. Env vars (kept for backward compatibility — also the default-value source for the traitlets so a fresh ``OpencodeConfig()`` with no overrides still honours them): OPENCODE_BRIDGE_URL — OpenCode Serve base URL (default http://127.0.0.1:4096) OPENCODE_BRIDGE_USER — HTTP Basic Auth username (default 'opencode') OPENCODE_BRIDGE_PASSWORD — HTTP Basic Auth password (default '' = no auth) OPENCODE_BRIDGE_TIMEOUT — request timeout in seconds (default 120) """ from __future__ import annotations import os from typing import NamedTuple, Optional, Tuple from traitlets import Int, Unicode, default from traitlets.config import Configurable ENV_URL = "OPENCODE_BRIDGE_URL" ENV_USER = "OPENCODE_BRIDGE_USER" ENV_PASSWORD = "OPENCODE_BRIDGE_PASSWORD" ENV_TIMEOUT = "OPENCODE_BRIDGE_TIMEOUT" DEFAULT_URL = "http://127.0.0.1:4096" DEFAULT_USER = "opencode" DEFAULT_REQUEST_TIMEOUT = 120 class OpenCodeConfig(NamedTuple): url: str user: str password: str request_timeout_seconds: int @property def auth(self) -> Optional[Tuple[str, str]]: """Return (user, password) for HTTP Basic Auth, or None if no password set.""" if not self.password: return None return (self.user, self.password) class OpencodeConfig(Configurable): """Traitlets-configurable connection settings for the opencode-bridge server extension. Exposed to ``jupyter lab`` / ``jupyter server`` as ``--OpencodeConfig.`` and to ``jupyter_server_config.py`` as ``c.OpencodeConfig.``. Defaults fall back to the ``OPENCODE_BRIDGE_*`` env vars and then to built-in defaults, so the legacy env-var-only flow keeps working unchanged. """ url = Unicode( DEFAULT_URL, config=True, help=( "Base URL of the locally-running OpenCode Serve process. " "Overrides the OPENCODE_BRIDGE_URL env var." ), ) user = Unicode( DEFAULT_USER, config=True, help=( "HTTP Basic Auth username for OpenCode Serve. " "Overrides the OPENCODE_BRIDGE_USER env var." ), ) password = Unicode( "", config=True, help=( "HTTP Basic Auth password for OpenCode Serve (empty = no auth). " "Overrides the OPENCODE_BRIDGE_PASSWORD env var." ), ) request_timeout_seconds = Int( DEFAULT_REQUEST_TIMEOUT, config=True, help=( "Per-request timeout in seconds when calling OpenCode Serve. " "Overrides the OPENCODE_BRIDGE_TIMEOUT env var." ), ) @default("url") def _default_url(self) -> str: return os.environ.get(ENV_URL) or DEFAULT_URL @default("user") def _default_user(self) -> str: return os.environ.get(ENV_USER) or DEFAULT_USER @default("password") def _default_password(self) -> str: return os.environ.get(ENV_PASSWORD) or "" @default("request_timeout_seconds") def _default_request_timeout_seconds(self) -> int: return int(os.environ.get(ENV_TIMEOUT) or DEFAULT_REQUEST_TIMEOUT) def _to_opencode_config(bridge) -> OpenCodeConfig: """Project an :class:`OpencodeConfig` (or duck-typed equivalent) to the internal ``OpenCodeConfig`` NamedTuple consumed by ``OpenCodeClient``. """ return OpenCodeConfig( url=bridge.url, user=bridge.user, password=bridge.password, request_timeout_seconds=bridge.request_timeout_seconds, ) def resolve_config(jupyter_settings) -> OpenCodeConfig: """Resolve OpenCode connection config into an ``OpenCodeConfig`` NamedTuple. Accepts either a settings dict (legacy) or an :class:`OpencodeConfig` instance directly, because request handlers call ``resolve_config(handler.settings.get("opencode_bridge", {}))`` and pass the inner value (which is now an ``OpencodeConfig`` instance after the server extension's load hook populates it). Source order (highest priority first): 1. The argument itself is an :class:`OpencodeConfig` instance. 2. ``jupyter_settings["opencode_bridge"]`` is an :class:`OpencodeConfig` instance (legacy call style). 3. Otherwise fall back to env vars + built-in defaults. """ if isinstance(jupyter_settings, OpencodeConfig): return _to_opencode_config(jupyter_settings) bridge = (jupyter_settings or {}).get("opencode_bridge") if isinstance(bridge, OpencodeConfig): return _to_opencode_config(bridge) return OpenCodeConfig( url=os.environ.get(ENV_URL) or DEFAULT_URL, user=os.environ.get(ENV_USER) or DEFAULT_USER, password=os.environ.get(ENV_PASSWORD) or "", request_timeout_seconds=int( os.environ.get(ENV_TIMEOUT) or DEFAULT_REQUEST_TIMEOUT ), )