Skip to content

woodglue.apps.server

Tornado application factory.

Tornado application factory for the woodglue JSON-RPC server.

create_app(namespaces, config=None, engine_registry=None, mounts=None, auth_store=None, ui_processes=None)

Build a Tornado Application with JSON-RPC and optional docs/UI routes.

namespaces maps prefix strings to (Namespace, NamespaceEntry) tuples. The plain Namespace dict (all namespaces) is stored in app settings for internal use. The method_index is filtered by expose_api.

With auth enabled, auth_store defaults to one opened on config.storage.auth_db, which must then be set; pass it to share the instance the system namespace uses. Without config, auth is disabled. ui_processes supervises Process-backed UIs; the caller starts and stops it (see wgl start).

Source code in src/woodglue/apps/server.py
def create_app(
    namespaces: dict[str, tuple[Namespace, NamespaceEntry]],
    config: WoodglueConfig | None = None,
    engine_registry: EngineRegistry | None = None,
    mounts: dict[str, MountContext] | None = None,
    auth_store: AuthStore | None = None,
    ui_processes: UiProcesses | None = None,
) -> tornado.web.Application:
    """
    Build a Tornado Application with JSON-RPC and optional docs/UI routes.

    `namespaces` maps prefix strings to `(Namespace, NamespaceEntry)` tuples.
    The plain `Namespace` dict (all namespaces) is stored in app settings for
    internal use. The `method_index` is filtered by `expose_api`.

    With auth enabled, `auth_store` defaults to one opened on
    `config.storage.auth_db`, which must then be set; pass it to share the
    instance the system namespace uses. Without `config`, auth is disabled. `ui_processes` supervises Process-backed UIs; the caller
    starts and stops it (see `wgl start`).
    """
    if config is None:
        config = WoodglueConfig(namespaces={}, auth=AuthConfig(enabled=False))
    if auth_store is None and config.auth.enabled:
        if config.storage.auth_db is None:
            raise ValueError("Auth is enabled but storage.auth_db is not set")
        auth_store = AuthStore(config.storage.auth_db, config.auth.resolved_roles())

    # Plain namespace dict for internal use (e.g. `wgl run`)
    plain_namespaces = {prefix: ns for prefix, (ns, _) in namespaces.items()}

    method_index = build_method_index(namespaces)

    handlers: list[Any] = [
        (r"/rpc", JsonRpcHandler),
    ]

    if config.docs.enabled:
        from woodglue.apps.llm_docs import LlmsTxtHandler, MethodDocHandler, OpenApiHandler

        handlers.append((r"/docs/llms\.txt", LlmsTxtHandler))
        handlers.append((r"/docs/methods/(.+)", MethodDocHandler))
        if config.docs.openapi:
            handlers.append((r"/docs/openapi\.json", OpenApiHandler))

    login_providers: Mapping[str, ProviderClient] = {}
    if auth_store is not None:
        from woodglue.apps.auth_pages import (
            CliAuthorizeHandler,
            CliTokenHandler,
            LoginHandler,
            LogoutHandler,
            SignInCallbackHandler,
            StartSignInHandler,
        )

        login_providers = make_clients(config.auth.providers)
        handlers.append((r"/auth/login", LoginHandler))
        handlers.append((r"/auth/logout", LogoutHandler))
        handlers.append((r"/auth/start/([^/]+)", StartSignInHandler))
        handlers.append((r"/auth/callback/([^/]+)", SignInCallbackHandler))
        handlers.append((r"/auth/verify", VerifyHandler))
        handlers.append((r"/auth/cli", CliAuthorizeHandler))
        handlers.append((r"/auth/cli/token", CliTokenHandler))

    if config.ui.enabled:
        console_dist = Path(__file__).resolve().parent.parent / "ui" / "dist"
        if console_dist.is_dir():
            handlers.append((r"/console", tornado.web.RedirectHandler, {"url": "/console/"}))
            handlers.append(
                (
                    r"/console/(.*)",
                    UiStaticHandler,
                    {"path": str(console_dist), "default_filename": "index.html"},
                )
            )

    if ui_processes is None:
        ui_processes = UiProcesses(config.uis, "")
    for name, entry in config.uis.items():
        handlers.append((rf"/ui/{name}", tornado.web.RedirectHandler, {"url": f"/ui/{name}/"}))
        if not entry.is_static:
            handlers.append(
                (rf"/ui/{name}/.*", UiProxyHandler, {"ui_name": name, "public": entry.public})
            )
            continue
        assert entry.path is not None
        if not Path(entry.path).is_dir():
            logger.warning("UI '%s': directory %s does not exist", name, entry.path)
        handlers.append(
            (
                rf"/ui/{name}/(.*)",
                UiStaticHandler,
                {
                    "path": entry.path,
                    "default_filename": "index.html",
                    "ui_name": name,
                    "spa_fallback": entry.spa_fallback,
                    "public": entry.public,
                },
            )
        )

    if config.ui.enabled or config.uis:
        handlers.append((r"/", tornado.web.RedirectHandler, {"url": ui_path(config.ui.default)}))

    trusted_proxies = make_trusted_proxies(config.auth.providers) if auth_store else []
    for proxy in trusted_proxies:
        tornado.ioloop.IOLoop.current().add_callback(proxy.load_keys)

    return tornado.web.Application(
        handlers,
        namespaces=plain_namespaces,
        method_index=method_index,
        config=config,
        auth_enabled=config.auth.enabled,
        auth_store=auth_store,
        login_providers=login_providers,
        trusted_proxies=trusted_proxies,
        pending_sign_ins=PendingSignIns(),
        cli_codes=CliCodes(),
        ui_processes=ui_processes,
        ui_sockets=UiSockets(),
        engine_registry=engine_registry,
        mounts=mounts or {},
    )