Skip to content

Plugin system

Explicit module loading

The default deployment model is an explicit plugin list:

plugins:
  - call_baxter.plugins.system
  - call_baxter.plugins.telegram

Each plugin module exports:

def register(registry, config: dict[str, object] | None = None) -> None:
    ...

The daemon imports each configured module with importlib.import_module(...) and calls register(registry, config) when the function accepts a second argument.

Both simple and configured forms are supported:

plugins:
  - call_baxter.plugins.system
  - module: call_baxter.plugins.telegram
    config:
      database_url: ./tg-state.db
      media_storage_mode: file

Optional entry-point discovery

The core loader can also opt into Python entry-point discovery. The default group is call_baxter.plugins, and callers can supply other groups. A discovered entry point must load to a callable and is invoked as register(registry).

This lets independently packaged interfaces advertise plugins without modifying the Call Baxter distribution. For example, cb-social-plugin exposes bluesky, mastodon, and x entry points. Provider-specific configuration still fits the explicit module form because those register() functions also accept (registry, config).

Reload behavior

POST /v1/reload and cb reload currently do three things:

  1. rebuild the registry
  2. re-import configured plugins and call register(...)
  3. re-load YAML rules and re-sync schedules

This is a safe start version because it does not attempt hot code reloading of arbitrary Python module changes. For code changes, restarting the daemon remains the clean path. For YAML changes, cb reload is usually enough.

Optional future extensions

  • file watching for rules.d/*.yml
  • separate plugin-owned projection stores for browse-heavy sources like Telegram

Decorator-based plugin DSL (PluginBuilder)

New plugins can use PluginBuilder from call_baxter.dsl to reduce boilerplate.

from call_baxter.dsl import PluginBuilder
from call_baxter.registry import Registry

plugin = PluginBuilder("myplugin")

@plugin.event("myplugin.thing", description="A thing happened",
               yields={"ref": "string", "label?": "string"})
def _(): pass

@plugin.poller("myplugin.check", description="Poll for things")
def poll(config: dict) -> list:
    ...

@plugin.action("myplugin.do", description="Do something",
               expects={"target": "string"})
def do_action(plugin_config: dict, action, runtime):
    # plugin_config injected automatically when first param is named 'plugin_config'
    ...

def register(registry: Registry, config=None):
    plugin.register_all(registry, config)

Field shorthand

yields and expects accept dicts of "field_name": "type". Keys ending in ? become optional fields; all others are required.

Supported types: string, integer, number, boolean, object, array, datetime.

Action safety contract

expects and yields are data-shape contracts. They do not claim that an executor is safe to repeat. Plugin actions should also provide the narrowest accurate ActionExecutionContract through registry.add_action_type(...) or the DSL action declaration. Leave behavior unknown when the runtime cannot prove more.

See Arbitrary actions and replay safety for idempotency boundaries, authorization provenance, replay-eligibility evidence, and recursive on_success / on_error failure semantics.

Available plugins

Module Poller(s) Action(s) Events
call_baxter.plugins.system system.heartbeat log.write system.heartbeat
call_baxter.plugins.telegram telegram.updates telegram.send_message, telegram.send_photo, telegram.send_document, telegram.edit_message_text, telegram.set_message_reaction, telegram.delete_message, telegram.pin_chat_message, telegram.unpin_chat_message, telegram.answer_callback_query, telegram.request_decision, telegram.request_confirmation, telegram.send_progress, telegram.update_progress, telegram.persist_update telegram.message.new, telegram.message.edited, telegram.channel_post.new, telegram.channel_post.edited, telegram.business_message.new, telegram.business_message.edited, telegram.callback_query.new, telegram.media.present, telegram.message.reaction
call_baxter.plugins.http_poll http.check http.request http.check.ok, http.check.failed
call_baxter.plugins.docker_watch docker.containers docker.restart docker.container.up, docker.container.down
call_baxter.plugins.mqtt mqtt.subscribe mqtt.publish mqtt.message.received
call_baxter.plugins.caldav caldav.agenda caldav.add_event, caldav.add_todo, caldav.complete_todo caldav.event.upcoming, caldav.event.starting_soon
call_baxter.plugins.zrok — zrok.run —
call_baxter.plugins.sonic_byte — sonic_byte.inspect, sonic_byte.render, sonic_byte.play —
call_baxter.plugins.file_out — file.out —

See Sonic Byte plugin for secure command configuration, sound-design options, and complete rule examples.