We're building the universal runtime for embodied AI and we want your help. Whether you're adding a new LLM provider, writing a hardware driver, building a messaging integration, or fixing a typo — every contribution counts.
Join the community:
- Discord: discord.gg/jMjA8B26Bq — ask questions, share builds, get help
- GitHub Issues: Report bugs or request features
- Twitter/X: @opencastor
- PyPI: pypi.org/project/opencastor
- Community Hub: opencastor.com/hub — browse and share robot recipes
The fastest way to contribute. A recipe is a RCAN config + optional scripts for a specific robot:
castor hub share --submitSee community-recipes/ for examples. Recipes are submitted as GitHub Pull Requests.
Open an issue: https://github.com/craigm26/OpenCastor/issues
Include your OS, hardware, OpenCastor version (castor --version), and the full error output.
Add support for a new AI provider in castor/providers/. See existing providers for the interface. It's a clean 50-line pattern.
We target high coverage. Pick any area with < 80% coverage and add tests. Run:
pytest tests/ --cov=castor --cov-report=term-missingDocs live in site/ (HTML) and README.md. Community recipe docs are in docs/community-recipes.md.
git clone https://github.com/craigm26/OpenCastor.git
cd OpenCastor
pip install -e ".[channels,dev]"
cp .env.example .env
# Edit .env with your API keysProviders live in castor/providers/. Every provider follows the same pattern:
- Create
castor/providers/<name>_provider.py - Subclass
BaseProviderfromcastor/providers/base.py - Implement
__init__(self, config):- Resolve API key: environment variable first, then
config.get("api_key") - Initialize the SDK client
- Resolve API key: environment variable first, then
- Implement
think(self, image_bytes: bytes, instruction: str) -> Thought:- Encode the image (base64 for most providers)
- Call the model with
self.system_promptand the instruction - Parse the response with
self._clean_json(text) - Return a
Thought(raw_text, action_dict)
- Register in
castor/providers/__init__.py:- Import your class
- Add an
elifbranch inget_provider()
- Add the SDK to
pyproject.tomldependencies andrequirements.txt
Example skeleton:
import os
import logging
from .base import BaseProvider, Thought
logger = logging.getLogger("OpenCastor.MyProvider")
class MyProvider(BaseProvider):
def __init__(self, config):
super().__init__(config)
api_key = os.getenv("MY_PROVIDER_API_KEY") or config.get("api_key")
if not api_key:
raise ValueError("MY_PROVIDER_API_KEY not found")
# Initialize your SDK client here
def think(self, image_bytes: bytes, instruction: str) -> Thought:
try:
# Call your model, get response text
text = "..."
action = self._clean_json(text)
return Thought(text, action)
except Exception as e:
logger.error(f"MyProvider error: {e}")
return Thought(f"Error: {e}", None)Drivers live in castor/drivers/. Every driver gracefully degrades to mock mode
when hardware libraries are unavailable.
- Create
castor/drivers/<name>.py - Subclass
DriverBasefromcastor/drivers/base.py - Implement
move(),stop(),close() - Use try/except for SDK imports with a module-level
HAS_<NAME>boolean - Add the protocol mapping in
get_driver()incastor/main.py - Add the SDK to
pyproject.tomlandrequirements.txt
Key conventions:
- Always provide mock mode when hardware SDK is missing
- Clamp values to safe physical ranges
- Log with
logging.getLogger("OpenCastor.<Name>")
Channels live in castor/channels/. Each channel receives commands from users on
a messaging platform and forwards them to the robot's brain.
- Create
castor/channels/<name>.py - Subclass
BaseChannelfromcastor/channels/base.py - Implement:
start()-- connect to the platform (bot login, webhook setup)stop()-- disconnect gracefullysend_message(chat_id, text)-- send a reply back to the user_on_message(chat_id, text)callback via the base class
- Register in
castor/channels/__init__.py - Add environment variables to
.env.example - Add the SDK to
pyproject.tomloptional dependencies
OpenCastor provides 41 commands via castor <command>. Before adding a new
command, familiarize yourself with the existing ones:
| Group | Commands |
|---|---|
| Setup | wizard, quickstart, configure, install-service, learn |
| Run | run, gateway, dashboard, demo, shell, repl |
| Diagnostics | doctor, fix, status, logs, lint, benchmark, test |
| Hardware | test-hardware, calibrate, record, replay, watch |
| Config | migrate, backup, restore, export, diff, profile |
| Safety | approvals, privacy, audit |
| Network | discover, fleet, network, schedule, token |
| Advanced | search, plugins, plugin, upgrade, update-check |
- Create a handler function
cmd_<name>(args) -> Noneincastor/cli.py - Add a subparser in
main()with help text and epilog example - Register in the
commandsdict:"<name>": cmd_<name> - Lazy-import the implementation module inside the handler (keeps startup fast)
- Add the command to the group table above and to
CHANGELOG.md - Write tests in
tests/test_cli.py
Plugins extend OpenCastor with custom commands, drivers, providers, and hooks without modifying the core codebase.
Every plugin must ship a plugin.json manifest alongside the .py file.
This is a security requirement — plugins without a manifest are silently skipped
at load time.
{
"name": "my_plugin",
"version": "1.0.0",
"author": "Your Name",
"hooks": ["on_startup"],
"commands": ["my-cmd"],
"sha256": "<hex SHA-256 digest of my_plugin.py>"
}| Field | Required | Description |
|---|---|---|
name |
✅ | Plugin identifier (must match the .py filename stem) |
version |
✅ | Semver string, e.g. "1.0.0" |
author |
✅ | Plugin author name or contact |
hooks |
✅ | List of hook events registered (may be empty []) |
commands |
✅ | List of CLI command names registered (may be empty []) |
sha256 |
optional | SHA-256 hex digest of the .py file for integrity verification |
Compute the SHA-256 digest to include in your manifest:
python -c "import hashlib; print(hashlib.sha256(open('my_plugin.py','rb').read()).hexdigest())"# my_plugin.py
def register(registry):
registry.add_command("my-cmd", my_handler, help="My custom command")
registry.add_hook("on_startup", my_startup_fn)
def my_handler(args):
print("Hello from my plugin!")
def my_startup_fn(config):
print("Robot booting up!")Use castor plugin install to fetch a plugin and record provenance in
~/.opencastor/plugins.lock:
# From a URL (fetches both .py and .json manifest automatically)
castor plugin install https://example.com/my_plugin.py
# From a local path
castor plugin install /path/to/my_plugin.pyThe installer:
- Downloads/copies the
.pyfile and theplugin.jsonmanifest - Validates the manifest (required fields + optional SHA-256 check)
- Writes both files to
~/.opencastor/plugins/ - Records
source,installed_at, andsha256in~/.opencastor/plugins.lock
Plugins placed directly in ~/.opencastor/plugins/ without using
castor plugin install must still have a valid plugin.json manifest or they
will be skipped with a warning.
castor pluginsShows each plugin's load status, manifest presence, version, and install source.
- PEP 8 with 100-char line length
- snake_case for functions and variables
- Type hints on public method signatures
- Docstrings on classes and non-trivial methods
- Lazy imports for optional hardware/channel SDKs (import inside try/except or constructor)
- Structured logging with per-module loggers:
logging.getLogger("OpenCastor.<Module>")
We use Ruff for linting:
ruff check castor/
ruff format castor/pytest tests/Tests go in the tests/ directory, mirroring the castor/ package structure.
- Fork the repo and create your branch from
main - Add or update tests for your changes
- Ensure RCAN configs still validate:
python .github/scripts/validate_rcan.py --schema <schema> --dir . - Run the linter:
ruff check castor/ - Open a PR with a clear description of what and why
- Driver Adapters: ODrive, VESC, ROS2 bridges, ESP32 serial
- New Brains: Mistral, Grok, Cohere, local vision models
- Messaging Channels: Matrix, Signal, Google Chat, iMessage
- Sim-to-Real: Gazebo/MuJoCo integration
- Latency Optimization: Reducing time-to-first-token for real-time reflex arcs
- Tests: Unit tests, integration tests, hardware mock tests
By contributing, you agree that your contributions will be licensed under the Apache 2.0 License.