Integrate Conda Plugins#

Use broker when a conda plugin benefits from preserving expensive runtime state between commands. Keep ordinary validation, parsing, and one-shot commands inline.

The conda plugin docs describe the host plugin system: packages register a module under [project.entry-points.conda], then expose hooks such as conda_subcommands() and conda_settings(). The conda-plugins catalogue shows the common shapes: subcommands, solver hooks, auth helpers, telemetry, channel tooling, environment specs, and environment/project helpers.

Choose the Integration Shape#

Broker is a good fit when the plugin has one of these properties:

  • It starts a local API server or subprocess from conda hooks.

  • It repeatedly builds the same package, channel, solver, or metadata cache.

  • It has a background watcher or indexer that users should be able to inspect.

  • It needs a shared local endpoint that several CLI commands can reuse.

Broker is usually not worth it when the plugin is just a pure subcommand, a small guardrail hook, or a fast formatter.

From the current catalogue, these are the useful patterns:

Plugin shape

Catalogue examples

Broker opportunity

Local channel or repodata helpers

conda-pypi-channel, conda-repodata

Replace ad hoc Popen or hidden cache lifecycles with a visible broker service, HTTP health check, logs, events, and a stable endpoint.

Metadata and conversion cache

conda-pypi, conda_index

Keep one-shot commands, but optionally reuse a warm local cache or index service when it is already ready.

Solver or repoquery backend

conda-libmamba-solver, conda-random-solver

Query a ready cache/helper service before rebuilding expensive in-memory state; fall back to the normal solver path.

Completion, history, or environment metadata

conda-completion, conda-history-d, conda-env-spec-v2, conda-lockfiles

Keep lightweight hook work inline, but optionally defer heavier refresh or indexing work to a broker service.

Environment or project helpers

conda-spawn, conda-global, conda-ops, conda-declarative

Keep command execution explicit, but expose optional status and service controls if a plugin adds a persistent helper.

UI or assistant wrapper

conda-tui, anaconda-assistant-mcp

Display broker service checks and offer explicit user actions to start or stop plugin services.

Auth, telemetry, or request hooks

anaconda-auth, conda-anaconda-telemetry, conda-dev-request-headers

Keep credential and request decisions inline. Only add broker for optional event batching, report generation, or expensive background refresh work users can inspect.

Pure example or tree subcommands

conda-tree, random-walk, conda-random-subcommand

Do nothing unless profiling shows repeated commands need a warm helper process.

Browser/runtime-specific plugins

conda-wasm

Treat broker as host-only. Do not assume it exists inside restricted runtimes such as Emscripten.

Keep Broker Optional#

Do not make every user install or start broker just because your plugin can use it. Keep the conda plugin entry point lean, and put broker-specific service definitions in a separate module.

[project]
name = "conda-my-plugin"
dependencies = ["conda"]

[project.optional-dependencies]
broker = ["conda-broker"]

[project.entry-points.conda]
"conda-my-plugin" = "conda_my_plugin.plugin"

[project.entry-points.conda_broker]
"conda-my-plugin" = "conda_my_plugin.broker"

The conda plugin module should not import conda_broker at module import time. Import it inside the function that actually wants to query broker:

def broker_service():
    try:
        from conda_broker import Broker
    except ImportError:
        return None
    return Broker.current().service("conda-my-plugin.helper")

The broker provider module can use broker imports directly because it is only loaded by broker discovery:

import sys

from conda_broker.hookspec import hookimpl
from conda_broker.models import CondaService, EndpointSpec, HealthCheck, ProcessSpec


@hookimpl
def conda_broker_services():
    yield CondaService(
        name="conda-my-plugin.helper",
        summary="Local helper API for conda-my-plugin",
        source="conda-my-plugin",
        start_policy="manual",
        restart_policy="on-failure",
        endpoints=(EndpointSpec(protocol="http", path="/health", port_env="PORT"),),
        health_check=HealthCheck(type="http", endpoint="default"),
        process=ProcessSpec(
            argv=(sys.executable, "-m", "conda_my_plugin.helper"),
            env={"PYTHONUNBUFFERED": "1"},
        ),
    )

Use a Ready Service Opportunistically#

Plugin hooks that run during conda install, conda create, or conda update should not silently start broker. They can query state and fall back:

def maybe_use_helper(request):
    service = broker_service()
    if service is None:
        return run_inline(request)

    check = service.check()
    if check.ready and check.endpoint and check.endpoint.url:
        return call_helper_api(check.endpoint.url, request)

    from conda_broker.exceptions import CondaBrokerError

    try:
        service.emit_event(
            "conda-my-plugin.fallback",
            message=f"helper {check.reason or 'not ready'}; using inline path",
        )
    except (OSError, CondaBrokerError):
        pass
    return run_inline(request)

Observability must not turn an optional fast path into a failed conda command when the runtime directory is unavailable.

That pattern keeps the integration invisible when it works and boring when it does not. Users who never enabled the service still get the original plugin behavior.

Add Plugin CLI Status#

Plugin CLIs should expose a status or doctor command when the broker service materially affects behavior. Use Service.check() so the command can render human output or JSON without starting anything:

import json


def status_command(args):
    service = broker_service()
    if service is None:
        payload = {
            "broker_service": {
                "name": "conda-my-plugin.helper",
                "available": False,
                "running": False,
                "ready": False,
                "enabled": False,
                "state": "unknown",
                "health": "unknown",
                "endpoint": None,
                "reason": "conda-broker-not-installed",
            }
        }
    else:
        payload = {"broker_service": service.check().to_dict()}

    if args.json:
        print(json.dumps(payload, indent=2, sort_keys=True))
    else:
        check = payload["broker_service"]
        if check["ready"]:
            endpoint = check["endpoint"] or {}
            suffix = f" at {endpoint['url']}" if endpoint.get("url") else ""
            print(f"helper ready{suffix}")
        elif check["available"]:
            print(f"helper {check['state']}; inline fallback active")
        else:
            print(f"helper unavailable: {check['reason']}")

Use explicit startup from user-visible commands only:

def start_helper_command(args):
    service = broker_service()
    if service is None:
        raise SystemExit("Install conda-my-plugin[broker] to enable the helper service.")
    service.start(timeout_s=args.timeout)
    if args.wait:
        service.wait(timeout_s=args.timeout)

For plugins with rich output, show the service check beside the plugin’s own checks. Keep the JSON field stable, for example broker_service, so users can script against it.

Use configure_parser for Plugin-Owned Commands#

Conda subcommand plugins can pass configure_parser to CondaSubcommand. That hook receives the plugin’s argparse.ArgumentParser, so a plugin can own commands such as conda my-plugin status without adding another global CLI.

Use this when the plugin already has a conda_subcommands() hook, or when a mostly hook-based plugin needs a small user-facing control surface for its optional broker service.

Use BrokerServiceCommands to install broker subcommands scoped to the service names supplied by the plugin. Pick the mounting shape that matches the plugin.

Hook-Only Plugins#

If the plugin does not already publish a conda_subcommands() hook, use conda_subcommand() to create a small service-management command:

from conda import plugins
from conda_broker.plugin_commands import BrokerServiceCommands


broker_commands = BrokerServiceCommands(
    services=("conda-my-plugin.helper",),
    source="conda-my-plugin",
)


@plugins.hookimpl
def conda_subcommands():
    yield broker_commands.conda_subcommand(
        "my-plugin",
        summary="Manage conda-my-plugin services.",
    )

That creates one stable command shape:

conda my-plugin services status
conda my-plugin services start
conda my-plugin services stop
conda my-plugin services restart
conda my-plugin services enable
conda my-plugin services disable
conda my-plugin services wait --start
conda my-plugin services logs

Plugins With Existing Subcommands#

If the plugin already has its own nested commands, mount the same services group beside those commands:

from conda import plugins
from conda_broker.plugin_commands import BrokerServiceCommands


broker_commands = BrokerServiceCommands(
    services=("conda-my-plugin.helper", "conda-my-plugin.worker"),
    source="conda-my-plugin",
)


def configure_parser(parser):
    subcommands = parser.add_subparsers(dest="command")

    run = subcommands.add_parser("run")
    run.set_defaults(handler=run_command)

    broker_commands.add_group_to_subparsers(subcommands)


def execute(args):
    if not hasattr(args, "handler"):
        raise SystemExit("Choose a subcommand.")
    return args.handler(args)


@plugins.hookimpl
def conda_subcommands():
    yield plugins.CondaSubcommand(
        name="my-plugin",
        summary="Run conda-my-plugin commands.",
        action=execute,
        configure_parser=configure_parser,
    )

That makes plugin-specific commands such as these available:

  • conda my-plugin run: plugin-owned behavior.

  • conda my-plugin services status: broker status for this plugin’s services.

  • conda my-plugin services start: start all services declared by this plugin.

  • conda my-plugin services stop conda-my-plugin.worker: stop one declared service.

  • conda my-plugin services logs conda-my-plugin.helper: show one service log.

Plugins With a Custom services Parser#

If the plugin already creates its own services parser and only wants broker controls inside that parser, configure that parser directly:

def configure_parser(parser):
    subcommands = parser.add_subparsers(dest="command")
    services = subcommands.add_parser("services")
    services.set_defaults(handler=broker_commands.execute)
    broker_commands.configure_commands_parser(services)

The lower-level add_commands_to_subparsers() method exists for advanced argparse layouts, but prefer the services group unless there is a strong reason not to.

All generated service arguments are restricted to the plugin’s service list. For a single-service plugin, start, stop, restart, enable, disable, status, wait, and logs can omit the service name. For a multi-service plugin, commands that can sensibly target many services default to all plugin services, while wait and logs ask for one.

Keep the group name stable unless the plugin has a strong domain-specific reason to use another word. services is the default because users can learn one shape across plugins.

Avoid starting broker from conda_pre_commands() or conda_post_commands(). Those hooks run during ordinary conda operations and should keep using the opportunistic query-and-fallback pattern.

Document It for Users#

User docs for optional broker integration should answer these questions:

  • What improves when the service is ready?

  • What happens when broker or the service is not installed?

  • Which command shows status?

  • Which explicit command starts, enables, disables, or stops the service?

  • Where are logs and events stored?

  • What data does the service keep locally?

Suggested wording:

The helper service is optional. When it is ready, this plugin reuses the
local service for faster repeated commands. When it is stopped, unavailable,
or unhealthy, the plugin uses the normal inline implementation.

Check service state with:

    conda my-plugin services status

Manage the service with:

    conda my-plugin services start
    conda my-plugin services enable
    conda my-plugin services logs

Avoid Surprises#

Follow these rules for seamless integration:

  • Query methods are safe in conda hooks: service.check(), service.status(), service.running(), service.ready(), and service.endpoint(ready=True).

  • Startup methods belong in explicit commands: service.start(), service.wait(start=True), service.started(), and Broker.start().

  • Always keep the original inline path unless the plugin is explicitly a service-only plugin.

  • Emit broker events for observability, but do not fail conda commands if the event cannot be delivered; catch CondaBrokerError and OSError around optional event writes.

  • Prefer localhost HTTP or TCP endpoints with health checks over implicit files or inherited process state.