Skip to content

Plugin system

Start version

The scaffold intentionally starts with the simplest workable discovery model:

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

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

  • optional Python entry-point discovery in addition to explicit module lists
  • 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.

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.answer_callback_query, telegram.persist_update telegram.message.new, …
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

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