Skip to content

Custom EnvPlugin DevelopmentΒΆ

Create plugins that extend OWA with your own functionality.

OWA's Env: MCP for Desktop Agents

Just as Model Context Protocol (MCP) is the "USB-C of LLMs", OWA's Env is the "USB-C of desktop agents" - a universal interface for native desktop automation.

Plugin DiscoveryΒΆ

How It WorksΒΆ

OWA automatically discovers plugins using Python's Entry Points system:

flowchart TD
    A[Your Plugin Package] --> B[pyproject.toml]
    B --> C[Entry Point Declaration]
    C --> E[Plugin Specification]

    E --> F[Component Definitions]

    F --> G[Callables]
    F --> H[Listeners]
    F --> I[Runnables]

    J[OWA Core Registry] -.->|Plugin Discovery| C
    G -.-> J
    H -.-> J
    I -.-> J

    J --> K[Available to Users]
    K --> L["CALLABLES['namespace/name']"]
    K --> M["LISTENERS['namespace/name']"]
    K --> N["RUNNABLES['namespace/name']"]

    style A fill:#e1f5fe
    style J fill:#f3e5f5
    style K fill:#e8f5e8

Discovery ProcessΒΆ

  1. Entry Point Scanning - OWA scans all installed packages for "owa.env.plugins" entry points
  2. Plugin Spec Loading - Loads and validates each PluginSpec object
  3. Lazy Registration - Registers component metadata without importing actual code
  4. On-Demand Loading - Components are imported only when first accessed

Quick Start: Your First Plugin in 5 MinutesΒΆ

Step 1: Copy the TemplateΒΆ

# Copy the example plugin as your starting point
cp -r projects/owa-env-example my-first-plugin
cd my-first-plugin

Step 2: Make It YoursΒΆ

Edit pyproject.toml to change the plugin name:

[project.entry-points."owa.env.plugins"]
myfirst = "owa.env.plugins.myfirst:plugin_spec"  # Changed from 'example'

Edit owa/env/plugins/example.py and change the namespace:

plugin_spec = PluginSpec(
    namespace="myfirst",  # Changed from 'example'
    version="0.1.0",
    description="My first OWA plugin",
    # ... rest stays the same
)

Step 3: Install and TestΒΆ

# Install your plugin
pip install -e .

# Verify OWA discovered it
owl env list myfirst

# Test a component
python -c "from owa.core import CALLABLES; print(CALLABLES['myfirst/add'](2, 3))"

πŸŽ‰ Congratulations!

You just created your first OWA plugin. Your components are now available to any OWA user or application.

Component TypesΒΆ

CallablesΒΆ

Functions for immediate results:

def get_weather(city: str) -> dict:
    return {"city": city, "temp": 25, "condition": "sunny"}

# Usage: CALLABLES["myplugin/weather"]("New York")

ListenersΒΆ

Event monitoring with callbacks (inherits from Runnable):

from owa.core import Listener

class FileWatcher(Listener):
    def on_configure(self, callback, watch_folder, **kwargs):
        self.callback = callback
        self.watch_folder = watch_folder

    def start(self):
        # Monitor folder, call self.callback(event) on changes
        pass

    def stop(self):
        # Stop and cleanup
        pass

RunnablesΒΆ

Background processes with start/stop control:

from owa.core import Runnable

class DataCollector(Runnable):
    def on_configure(self, output_file, interval=60, **kwargs):
        self.output_file = output_file
        self.interval = interval

    def loop(self, *, stop_event):
        while not stop_event.is_set():
            # Do work
            stop_event.wait(self.interval)

Plugin SpecificationΒΆ

StructureΒΆ

owa/env/plugins/myplugin.py
from owa.core.plugin_spec import PluginSpec

plugin_spec = PluginSpec(
    namespace="myplugin",
    version="0.1.0",
    description="My custom plugin",
    components={
        "callables": {
            "weather": "owa.env.myplugin.api:get_weather",
            "calculate": "owa.env.myplugin.math:add_numbers",
        },
        "listeners": {
            "file_watcher": "owa.env.myplugin.watchers:FileWatcher",
        },
        "runnables": {
            "data_collector": "owa.env.myplugin.workers:DataCollector",
        }
    }
)

Key ElementsΒΆ

  • namespace: Unique identifier for your plugin
  • components: Maps component names to import paths
  • Import format: "module.path:object_name"

Reference ImplementationΒΆ

Study the working example: projects/owa-env-example

Getting StartedΒΆ

cd projects/owa-env-example
pip install -e .
owl env list example

# Test it works
python -c "
from owa.core import CALLABLES
result = CALLABLES['example/add'](2, 3)
print(f'2 + 3 = {result}')
"

What It ShowsΒΆ

  • File organization and project structure
  • All three component types with working examples
  • Proper entry point configuration
  • Complete test suite

Project Structure OptionsΒΆ

Choose based on your needs

Everything in one file - good for prototypes:

my-plugin/
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ plugin.py              # All code here
└── tests/

pyproject.toml
[project.entry-points."owa.env.plugins"]
myplugin = "plugin:plugin_spec"

Integrate with existing code:

acme-tools/
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ acme/tools/
β”‚   β”œβ”€β”€ core/
β”‚   └── owa_plugin.py       # Plugin spec
└── tests/

pyproject.toml
[project.entry-points."owa.env.plugins"]
acme_tools = "acme.tools.owa_plugin:plugin_spec"

Follow OWA conventions:

owa-env-myplugin/
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ owa/env/
β”‚   β”œβ”€β”€ myplugin/           # Implementation
β”‚   └── plugins/myplugin.py # Plugin spec
└── tests/

pyproject.toml
[project.entry-points."owa.env.plugins"]
myplugin = "owa.env.plugins.myplugin:plugin_spec"

Best PracticesΒΆ

Naming ConventionsΒΆ

  • Namespace: desktop, my_company (lowercase, underscores)
  • Components: mouse.click, file.read (dots for hierarchy)
  • Package: owa-env-yourplugin

Error HandlingΒΆ

def safe_function(param: str) -> dict:
    try:
        result = do_work(param)
        return {"success": True, "data": result}
    except Exception as e:
        return {"success": False, "error": str(e)}

Resource ManagementΒΆ

class MyRunnable(Runnable):
    def loop(self, *, stop_event):
        try:
            resource = acquire_resource()
            while not stop_event.is_set():
                use_resource(resource)
                stop_event.wait(0.1)  # Small delay
        finally:
            release_resource(resource)

TroubleshootingΒΆ

Common Issues and Solutions

Symptoms: owl env list doesn't show your plugin

Debug steps:

  1. Verify installation:

    pip list | grep your-plugin-name
    

  2. Check entry points:

    python -c "
    try:
        from importlib.metadata import entry_points
    except ImportError:
        from importlib_metadata import entry_points
    
    eps = entry_points(group='owa.env.plugins')
    for ep in eps:
        print(f'{ep.name}: {ep.value}')
    "
    

  3. Test plugin spec import:

    python -c "from your.module.path import plugin_spec; print(plugin_spec.namespace)"
    

Common causes:

  • Plugin not installed (pip install -e .)
  • Entry point name conflicts with existing plugin
  • Incorrect entry point path in pyproject.toml

Symptoms: Validation fails with import errors

Debug command:

owl env docs --validate yourplugin --output-format text

Common causes & solutions:

Problem Solution
Missing dependencies Add them to pyproject.toml dependencies
Wrong import paths Check module.path:object_name format
Circular imports Keep plugin spec separate from implementation
Module not found Ensure module is importable after installation

"Component not callable" errors:

# ❌ Wrong - points to module
"callables": {
    "bad": "mymodule.utils"
}

# βœ… Correct - points to function
"callables": {
    "good": "mymodule.utils:my_function"
}

Listener/Runnable doesn't work:

# βœ… Correct structure
from owa.core import Listener

class MyListener(Listener):  # Must inherit
    def on_configure(self, callback, **kwargs):  # Must implement
        self.callback = callback
        # Your setup code

Common issues:

  • Not inheriting from owa.core.Listener or owa.core.Runnable
  • Missing on_configure() method
  • Not calling super().__init__() in custom __init__

Run these commands to diagnose issues:

# Check if OWA can see your plugin
owl env list yourplugin

# Validate plugin specification
owl env docs --validate yourplugin --strict

# Check for namespace conflicts
owl env stats --namespaces

# Test component loading manually
python -c "
from owa.core import CALLABLES, LISTENERS, RUNNABLES
print('Available namespaces:')
namespaces = set()
for key in list(CALLABLES.keys()) + list(LISTENERS.keys()) + list(RUNNABLES.keys()):
    namespaces.add(key.split('/')[0])
for ns in sorted(namespaces):
    print(f'  - {ns}')
"

Still having issues?

  • Check the OWA GitHub Issues
  • Look at working examples in projects/owa-env-*
  • Ensure you're using compatible versions of dependencies

PublishingΒΆ

PyPI (Recommended):

uv build
uv publish

GitHub/Git:

pip install git+https://github.com/user/owa-env-plugin.git

Local Development:

pip install -e /path/to/plugin

Next StepsΒΆ

Topic Description
Environment Guide Complete system overview and advanced usage patterns
Built-in Plugins Learn from standard, desktop, and GStreamer implementations
CLI Tools Plugin management and exploration commands