Detailed documentation for all major subsystems.
All LLM adapters subclass BaseProvider. Key methods:
| Method | Signature | Description |
|---|---|---|
think |
(image_bytes, instruction) -> Thought |
Single inference call |
think_stream |
(image_bytes, instruction) -> Iterator[str] |
Streaming token output |
health_check |
() -> dict |
Returns {ok, latency_ms, error} |
get_usage_stats |
() -> dict |
Token/cost stats (Anthropic/OpenAI implement; base returns {}) |
- Constructor resolves API key: env var →
.env→ RCAN config think()encodes image as base64 (OpenAI/Anthropic) or raw bytes (Google)- Every
think()andthink_stream()call passes throughself._check_instruction_safety(instruction)at the top — returns a blockingThoughton prompt injection detection - System prompt forces strict JSON output only
_clean_json()strips markdown fences from LLM responsesthink_stream()yields text chunks; all providers implement it (Anthropic CLI path yields a single chunk)_caps: List[str]— RCAN capability names (e.g.["nav","teleop","vision"]); set byapi.pyafter brain init fromrcan_protocol.capabilities; injected intobuild_messaging_prompt()so the brain knows which action types to use_robot_name: str— robot name frommetadata.robot_name; set byapi.pyafter brain init; used inbuild_messaging_prompt()persona line
| Provider | File | Key Env Var |
|---|---|---|
| Google Gemini | google_provider.py |
GOOGLE_API_KEY |
| OpenAI GPT-4.1 | openai_provider.py |
OPENAI_API_KEY |
| Anthropic Claude | anthropic_provider.py |
ANTHROPIC_API_KEY |
| Local Ollama | ollama_provider.py |
OLLAMA_BASE_URL |
| HuggingFace Hub | huggingface_provider.py |
HF CLI auth |
| llama.cpp | llamacpp_provider.py |
Local binary |
| Apple MLX | mlx_provider.py |
Local (macOS only) |
| Apple Foundation Models | apple_provider.py |
none (on-device preflight) |
| Google Vertex AI | vertex_provider.py |
VERTEX_PROJECT |
| OpenRouter (100+ models) | openrouter_provider.py |
OPENROUTER_API_KEY |
| Groq LPU | groq_provider.py |
GROQ_API_KEY |
| Sentence Transformers | sentence_transformers_provider.py |
none (local) |
| VLA (OpenVLA/Octo/pi0) | vla_provider.py |
none (local) |
| ONNX Runtime | onnx_provider.py |
ONNX_MODEL_PATH |
| Kimi (Moonshot AI) | kimi_provider.py |
MOONSHOT_API_KEY |
| MiniMax | minimax_provider.py |
MINIMAX_API_KEY |
| Qwen3 (via Ollama) | qwen_provider.py |
OLLAMA_BASE_URL |
Factory: get_provider(config) in castor/providers/__init__.py.
Apple-specific setup/runtime notes:
castor/providers/apple_preflight.pychecks OS/version/arch, SDK import, Xcode, and model readiness.- Normalized preflight reasons:
APPLE_INTELLIGENCE_NOT_ENABLED,DEVICE_NOT_ELIGIBLE,MODEL_NOT_READY,UNKNOWN. castor/providers/apple_provider.pymaps setup profile IDs (apple-balanced,apple-creative,apple-tagging) to Foundation Models use-case/guardrail enums.
CLI wizard and web wizard now share one setup decision engine.
- Single source of truth for:
- provider specs and display order
- model profiles per provider
- curated stack profiles (
apple_native,mlx_local_vision,ollama_universal_local) - hardware preset labels
- Exposes helper APIs used by wizard/auth/configure/conformance/lint layers.
- Powers setup API v2 endpoints:
GET /setup/api/catalogPOST /setup/api/preflightPOST /setup/api/generate-config
- Keeps compatibility routes (
/setup/api/save-config,/setup/api/test-provider) but routes through shared logic. - Handles Apple SDK auto-install (explicit opt-in), preflight rerun, and fallback recommendations.
SQLite-backed store for all brain decisions.
| Property | Value |
|---|---|
| Default DB path | ~/.castor/memory.db |
| Override | CASTOR_MEMORY_DB env var |
| Max episodes | 10,000 (FIFO eviction when full) |
| Method | Description |
|---|---|
log_episode(instruction, image_hash, thought, latency_ms) |
Record a brain decision |
query_recent(limit) |
Fetch N most recent episodes |
get_episode(id) |
Fetch single episode by ID |
export_jsonl() |
Export all episodes as JSONL string |
clear() |
Delete all episodes |
hash_image(bytes) |
SHA-256 hash of image bytes |
count() |
Total episode count |
Called in the perception-action loop after every brain decision. Also exposed via GET /api/memory/episodes, GET /api/memory/export, and DELETE /api/memory/episodes.
Stdlib-only Prometheus implementation — no external dependencies.
get_registry()— singleton accessor- 13 pre-registered metrics including:
loop_latency_ms,brain_calls_total,motor_commands_total,errors_total - Exposed at
GET /api/metricsas Prometheus text format
| Function | Description |
|---|---|
record_loop(latency_ms, robot) |
Record perception-action loop timing |
record_command(action_type) |
Increment motor command counter |
record_error(source) |
Increment error counter |
update_status(running, paused) |
Update runtime state gauges |
Named callable tools the LLM brain can invoke.
| Built-in Tool | Description |
|---|---|
get_status |
Return current robot status |
take_snapshot |
Capture camera frame |
announce_text |
Speak text via TTS |
get_distance |
Read distance sensor |
registry.call(name, /, **kwargs) # name is positional-only
registry.call_from_dict(tool_call) # OpenAI or Anthropic format
registry.to_openai_tools() # Schema for OpenAI function calling
registry.to_anthropic_tools() # Schema for Anthropic tool useImportant: call(name, /, **kwargs) uses a positional-only name parameter (Python 3.10+ syntax). This avoids TypeError: got multiple values for argument 'name' when a tool has its own parameter named name.
call_from_dict() handles:
- OpenAI format:
argumentsis a JSON string - Anthropic format:
inputis a dict
Register custom tools from RCAN agent.tools list via _register_from_config().
Routes action dict keys to sub-drivers via RCAN routing: config.
# Example RCAN routing config
drivers:
- protocol: composite
routing:
wheels: pca9685
arm: dynamixel- Each sub-driver handles a specific action namespace
_NullDriverfallback for unknown protocols (logs + no-ops)health_check()aggregates sub-driver health; reports"degraded"if any sub-driver fails
All hardware drivers subclass DriverBase. Methods:
| Method | Description |
|---|---|
move(action) |
Execute a motor action |
stop() |
Halt all motors |
close() |
Clean up hardware connections |
health_check() |
Returns {ok, mode, error} |
- Hardware SDKs imported in
try/exceptwith module-levelHAS_<NAME>boolean - Drivers degrade to mock mode when SDK is missing (log actions, no physical output)
- Values clamped to safe physical ranges (Dynamixel: 0–4095 ticks; PCA9685: duty cycle limits)
health_check()returns{ok: bool, mode: "hardware"|"mock", error: str|None}close()must be called infinallyblocks for clean shutdown
| Component | File | Function |
|---|---|---|
check_input_safety |
anti_subversion.py |
Scans every instruction; returns ScanVerdict.BLOCK on prompt injection |
_check_instruction_safety |
base.py (providers) |
Called at top of every think()/think_stream(); returns blocking Thought on BLOCK |
BoundsChecker |
bounds.py |
Validates motor commands against joint/force/workspace limits |
WorkAuthority |
authorization.py |
Approves/denies WorkOrder requests with full audit trail |
GuardianAgent |
agents/guardian.py |
Safety meta-agent with veto authority + e-stop trigger |
Instruction → check_input_safety() → ScanVerdict
BLOCK → return blocking Thought (no hardware movement)
ALLOW → provider.think() → BoundsChecker → motor command
↑
GuardianAgent veto
Unix-inspired filesystem with capability-based permissions.
| Path | Purpose |
|---|---|
/dev/motor |
Motor control nodes |
/etc/config |
Robot configuration |
/var/log |
Log storage |
/tmp |
Temporary data |
/proc |
Read-only runtime introspection |
/mnt |
External mounts |
| Capability | Purpose |
|---|---|
CAP_MOTOR_WRITE |
Write to motor nodes |
CAP_ESTOP |
Trigger emergency stop |
CAP_SAFETY_OVERRIDE |
Clear e-stop, override bounds |
| Tier | Purpose |
|---|---|
episodic |
Recorded robot episodes |
semantic |
Facts and knowledge base |
procedural |
Learned behaviors |
fs = CastorFS()
fs.read("/etc/config/robot_name")
fs.write("/dev/motor/speed", 0.5) # Requires CAP_MOTOR_WRITE
fs.estop() # Propagates to all drivers
fs.clear_estop() # Requires CAP_SAFETY_OVERRIDEContextWindow: sliding multi-turn context for agentsPipeline: Unix-pipe-style operation chainingProcFS: read-only runtime introspection at/proc
Transparent fallback on quota/credit errors.
| Feature | Detail |
|---|---|
| Trigger | ProviderQuotaError (HF HTTP 402/429 or quota keywords: credits, quota, rate limit) |
| Default cooldown | 3600s before retrying primary |
| Startup check | probe_fallback() health-checks backup at startup |
| Priority | Takes priority over offline_fallback in _get_active_brain() |
provider_fallback:
enabled: true
provider: ollama # ollama | google | openai | anthropic | llamacpp | mlx | apple
model: llama3.2:3b
quota_cooldown_s: 3600
alert_channel: telegram # Optional: notify on switchProviderQuotaErrordefined incastor/providers/base.py; hasprovider_nameandhttp_statusattrsProviderFallbackManager.health_check()delegates toget_active_provider().health_check()/api/statushealth check routes through_get_active_brain()with 30-second cache to avoid flooding dead endpoints
Auto-switches to local inference on connectivity loss.
| Feature | Detail |
|---|---|
| Monitor | ConnectivityMonitor checks internet reachability |
| Local providers | Ollama, llama.cpp, MLX, Apple Foundation Models |
| Alert | Notifies via configured channel when switching |
offline_fallback:
enabled: true
provider: ollama
model: llama3.2:3b
check_interval_s: 30
alert_channel: telegramUsage pattern:
# Use this instead of state.brain.think(...)
state.offline_fallback.get_active_provider().think(image, instruction)RCAN Spec — current version 1.3.
| Component | Description |
|---|---|
| RURI | rcan://domain.robot-name.id addressing |
| RBAC | 5 roles: CREATOR > OWNER > LEASEE > USER > GUEST |
| JWT | RCANTokenManager signs/verifies tokens (POST /api/auth/token) |
| mDNS | Optional auto-discovery via _rcan._tcp; updates discovered_peers dict |
| Router | MessageRouter dispatches RCANMessage envelopes by type and RURI |
| Role | Level | Permissions |
|---|---|---|
CREATOR |
5 | Full control, config write |
OWNER |
4 | All operations |
LEASEE |
3 | Temporary operator access |
USER |
2 | Commands, no config |
GUEST |
1 | Status read only |
All agents inherit BaseAgent and communicate via SharedState pub/sub event bus.
| Agent | Role |
|---|---|
OrchestratorAgent |
Master; resolves multi-agent input to a single RCAN action |
GuardianAgent |
Safety meta-agent; veto authority over all motor commands |
ObserverAgent |
Parses vision output, publishes scene detections |
NavigatorAgent |
Path planning (potential fields algorithm) |
CommunicatorAgent |
Routes NL intent from messaging channels |
ManipulatorAgent |
Arm and gripper control |
Spawns, monitors, and automatically restarts agents. All lifecycle managed here.
1. RECORD → observation + action + outcome tuples
2. PM → analyze episodes, find failure patterns
3. DEV → generate patches (ConfigPatch / PromptPatch / BehaviorPatch)
4. QA → validate patches; suggest retry or approve
5. APPLY → deploy approved patches live
ImprovementResult.stage_durations (dict):
pm_ms— PM stage durationdev_ms_attempt0,dev_ms_attempt1, ... — Dev stage (one key per retry attempt)qa_ms_attempt0,qa_ms_attempt1, ... — QA stage (one key per retry attempt)apply_ms— Apply stage duration
SisyphusStats.avg_duration_ms — average across applied/rejected episodes.
| Type | Purpose |
|---|---|
ConfigPatch |
Modify RCAN config values |
PromptPatch |
Modify system prompt |
BehaviorPatch |
Modify behavior YAML sequences |
PMStage,DevStage,QAStagestore provider asself._provider(not.provider)ALMAConsolidation(learner/alma.py) aggregates patches from multiple swarm robots
| Component | File | Purpose |
|---|---|---|
SwarmCoordinator |
coordinator.py |
Distributes tasks across SwarmPeer robots |
SwarmConsensus |
consensus.py |
Majority-vote protocol for shared decisions |
SharedMemory |
shared_memory.py |
Distributed key-value store |
PatchSync |
patch_sync.py |
Incremental RCAN config sync across robots |
SwarmPeer |
peer.py |
Remote robot proxy with HTTP client |
ALMAConsolidation |
learner/alma.py |
Aggregates patches from multiple robots |
nodes:
- name: alex
host: alex.local # mDNS hostname
ip: 192.168.68.91 # Static IP fallback
port: 8000
token: <OPENCASTOR_API_TOKEN>
rcan: ~/OpenCastor/alex.rcan.yaml
tags: [rpi5, camera, i2c, rover]
added: "2026-02-21"Manages N simultaneous camera captures.
| Method | Description |
|---|---|
get_frame(camera_id) |
Get frame from specific camera |
get_composite() |
Get composite view |
| Mode | Description |
|---|---|
tile |
Side-by-side grid of all cameras |
primary_only |
Single primary camera (backwards compatible) |
most_recent |
Frame from most recently updated camera |
depth_overlay |
RGB + OAK-D depth overlay combined |
CAMERA_INDEX env var selects primary camera (backwards compatible).
GET /api/stream/mjpeg?camera=id streams a specific camera.
CameraTrack(VideoStreamTrack)wraps OpenCV capturePOST /api/stream/webrtc/offer— SDP offer/answer exchange via aiortc- ICE server config in RCAN:
network.ice_servers - Graceful fallback to MJPEG if aiortc not installed
- Connects via neonize QR-code scan (pin:
neonize==0.3.13.post0) - Dispatches incoming messages to
_handle_channel_message("whatsapp", chat_id, text) - Group routing — two optional RCAN config keys control which groups the bot responds to:
group_jids: ["<JID>"]— exact JID allowlist (fast path, no API call); preferred for productiongroup_name_filter: "substring"— case-insensitive substring match on group subject; fetched once per JID and cached in_group_name_cache- If neither is set, all groups are accepted (with a log tip to set one)
self_chat_mode: false— skip messages the bot sent to itself in DMs (group messages always pass)dm_policy: open|closed— whether to respond to direct messages
channels:
whatsapp:
enabled: true
group_policy: open
dm_policy: open
self_chat_mode: false
group_jids:
- "120363407179315671" # JID logged on first message; use for precision
# group_name_filter: alex # alternative: substring match on group name- Polls HA websocket for
input_text.castor_commandstate changes - Auth:
HA_LONG_LIVED_TOKENenv var - Exposes
switch.castor_<name>andsensor.castor_last_actionentities - RCAN config:
channels: homeassistant: ha_url: http://homeassistant.local:8123 ha_token: "${HA_LONG_LIVED_TOKEN}" entity_id: input_text.castor_command
- Discovers robots via mDNS
_rcan._tcp state.fleet_peers:ruri → {ip, port, last_seen}GET /api/fleet— lists all discovered peersPOST /api/fleet/{ruri}/command— proxies commands via RCAN bearer tokenGET /api/fleet/{ruri}/status— proxies status fetch
Continuous OODA loop:
1. OBSERVE → Capture camera frame via OpenCV
2. ORIENT → check_input_safety() (anti-subversion)
3. DECIDE → provider.think(frame, instruction) → Thought
4. ACT → Thought.action → motor commands → driver.move()
5. TELEMETRY → get_registry().record_loop(latency)
6. MEMORY → EpisodeMemory().log_episode(...)
7. PAUSE? → Check VFS /proc/paused flag (set by POST /api/runtime/pause)
8. ESTOP? → Check VFS estop state before next iteration
Listenerclass incastor/main.py— SpeechRecognition-based voice inputlisten_once() -> Optional[str]- Gated by
HAS_SRboolean (graceful degradation when SpeechRecognition missing)
Speaker._split_sentences(text, max_chunk=500)— sentence-chunked TTS- 150ms pause between sentences
- No 200-char truncation (full text spoken)
- Environment variable (e.g.,
GOOGLE_API_KEY) .envfile (loaded via python-dotenv)- RCAN config fallback (e.g.,
config["api_key"])
| Function | Description |
|---|---|
resolve_provider_key(provider, config) |
Get API key for a provider |
resolve_channel_credentials(channel, config) |
Get all credentials for a channel |
list_available_providers() |
Dict of provider → readiness status |
list_available_channels() |
Dict of channel → readiness status |
check_provider_ready(provider) |
Boolean readiness check |
check_channel_ready(channel) |
Boolean readiness check |
OPENCASTOR_USERS=user:pass:role,user2:pass2:role2(SHA-256 passwords)JWT_SECRET→OPENCASTOR_API_TOKEN→ random fallback for signing- Roles:
admin(3) > operator(2) > viewer(1)
SQLite-backed LRU cache to avoid redundant API calls.
| Property | Default |
|---|---|
| DB path | ~/.castor/response_cache.db (CASTOR_CACHE_DB) |
| TTL | 3600 s (CASTOR_CACHE_MAX_AGE) |
| Max entries | 10,000 (CASTOR_CACHE_MAX_SIZE) |
| Eviction | LRU (oldest first) |
ResponseCache— singleton, thread-safe SQLite cache;.get(),.put(),.clear(),.stats()CachedProvider— transparent wrapper around anyBaseProvider; delegates on miss, returns storedThoughton hit
Key: SHA-256(instruction_utf8 + md5_hex(image_bytes)).
from castor.response_cache import get_cache, CachedProvider
cached_brain = CachedProvider(brain, get_cache())
thought = cached_brain.think(image_bytes, instruction)REST: GET /api/cache/stats, POST /api/cache/clear, POST /api/cache/enable, POST /api/cache/disable.
Fuses LiDAR and OAK-D depth readings to enforce safe distances.
| Zone | Default | Action |
|---|---|---|
| E-stop | < 200 mm | driver.stop() immediately |
| Slow | < 500 mm | Scale linear component by slow_factor (default 0.3) |
Sensor priority: LiDAR (RPLidar) → OAK-D depth → mock fallback.
REST: GET /api/avoidance/status, POST /api/avoidance/configure.
Supports MPU6050, BNO055, ICM-42688 via smbus2. Auto-detects by probing I2C addresses on startup.
imu = IMUDriver(config)
reading = imu.read()
# -> {accel_g: {x,y,z}, gyro_dps: {x,y,z}, mag_uT, temp_c, mode}Mock mode returns simulated values when smbus2 is unavailable.
REST: GET /api/imu/reading, GET /api/imu/health.
Supports RPLidar A1/A2/C1/S2 via the rplidar Python SDK.
lidar = LidarDriver(config)
scan = lidar.scan() # -> [{angle_deg, distance_mm, quality}, ...]
obs = lidar.obstacles() # -> {min_distance_mm, sectors: {front,right,back,left}}Mock mode generates synthetic scan data when rplidar is unavailable.
REST: GET /api/lidar/scan, GET /api/lidar/obstacles, GET /api/lidar/health.
Captures 3D point clouds from OAK-D stereo depth frames (or simulated data).
PointCloudCapture.capture() -> list[dict]—[{x,y,z}]in metresexport_ply(points) -> bytes— PLY file export for MeshLab / Open3Dget_stats(points) -> dict— count, centroid, bounding box- Open3D used if available (
HAS_OPEN3D); detected viaimportlib.util.find_spec
REST: GET /api/depth/pointcloud, GET /api/depth/pointcloud.ply, GET /api/depth/pointcloud/stats.
Real-time object detection with annotation overlays.
- Backends: YOLOv8 (
ultralytics), HuggingFace DETR, mock fallback - 80 COCO classes; configurable
confidence_threshold ObjectDetector.detect(frame) -> list[Detection]—{class, confidence, bbox: {x,y,w,h}}annotate(frame, detections) -> JPEG bytes— draws bounding boxes + labels
REST: GET /api/detection/frame, GET /api/detection/latest, POST /api/detection/configure.
Generates and imports simulation config files from RCAN specs.
SimBridge.generate_sim_config(rcan, sim="mujoco") -> str— MJCF/SDF stringSimBridge.export_to_file(rcan, fmt, output_path) -> Path- Supports: MuJoCo (MJCF), Gazebo (SDF/URDF), Webots (WBT)
- MuJoCo detected via
importlib.util.find_spec; H5PY for HDF5 trajectory export
REST: GET /api/sim/formats, POST /api/sim/export, POST /api/sim/import, GET /api/sim/config.
Wake-word detection pipeline using Porcupine.
- Background thread listens for wake word (
PORCUPINE_ACCESS_KEYrequired) - On wake: transcribes audio via
castor/voice.py(Whisper → Google SR) - Sends transcript to brain via REST
/api/command - Robot responds and speaks via TTS
State machine: idle → waiting → listening → thinking → back to waiting.
Lambda fix: lambda e=woke: e.set() (properly binds loop variable).
Injects persona tone into every brain prompt.
| Profile | System Prefix |
|---|---|
friendly |
Warm, encouraging assistant |
military |
Terse, mission-focused |
scientist |
Analytical, data-driven |
child |
Simple vocabulary, curious |
pirate |
Nautical metaphors, "Arrr!" |
chef |
Cooking/food metaphors |
REST: GET /api/personality, POST /api/personality/set.
Exports episode memory as JSONL training data.
- OpenAI format:
{"messages": [{"role": "user", "content": ...}, {"role": "assistant", "content": ...}]} - Anthropic format:
{"prompt": ..., "completion": ...} - Filters episodes with valid instruction + action pairs
- CLI:
castor export-finetune [--output FILE] [--limit N] [--provider openai|anthropic] - REST:
GET /api/finetune/export?limit=500&provider=openai
Provides isolated namespaces for multi-robot deployments.
- Each workspace has its own RCAN config, episode memory, and usage DB
- JWT availability detected via
importlib.util.find_spec("jwt") WorkspaceManager.create(name),.switch(name),.list(),.delete(name)- Stored at
~/.castor/workspaces/(override:CASTOR_WORKSPACE_DIR)
REST: GET /api/workspace/list, POST /api/workspace/create, POST /api/workspace/switch.
Zero-dependency TypeScript client for browser and Node.js environments.
import { CastorClient } from '@opencastor/sdk';
const client = new CastorClient({
baseUrl: 'http://robot.local:8000',
token: process.env.OPENCASTOR_API_TOKEN,
});
await client.command({ instruction: 'go forward 1 metre' });
const status = await client.status();
for await (const chunk of client.stream({ instruction: 'describe what you see' })) {
process.stdout.write(chunk);
}Methods: command(), stream(), status(), stop(), health(), listRecordings(), getDepthFrame().
Controls NEMA 17/23 stepper motors via DRV8825, TMC2209, or A4988 driver boards.
- Step/direction GPIO-based control
- Configurable microstep resolution (1, 2, 4, 8, 16, 32)
- Mock mode when RPi.GPIO / gpiod unavailable
move({"steps": N, "direction": 1})— positive = forward
Direct GPIO pin control on Raspberry Pi via RPi.GPIO (BCM/BOARD) or gpiod (libgpiod).
- Pin mappings from RCAN
drivers[].pinsconfig move({"pin_name": value})for digital output- Mock mode on non-RPi hardware
High-performance brushless motor control for ODrive and VESC controllers.
- USB or CAN bus communication
- Velocity, position, and torque modes
move({"left": float, "right": float})— normalized -1.0 to 1.0- Mock mode when
odrivelibrary unavailable
Connects to Gazebo (ROS2/gazebo_msgs), Webots (REST API), or pure mock mode.
- Protocol identifiers:
simulation,gazebo,webots - Publishes motion to simulator; reads back pose
- Enables sim-to-real transfer without code changes
Reads voltage, current, and state-of-charge from an INA219 I2C sensor.
- Auto-detected on addresses
0x40–0x4F - Low-battery alert: publishes to
GET /api/battery - Triggers e-stop if voltage drops below
min_voltage_v(RCAN config) - Mock mode returns synthetic readings when smbus2 unavailable
2D occupancy grid mapping using wheel odometry combined with LiDAR scans.
SlamMapper.update(odom, scan)— incremental updateget_map() -> dict—{width, height, resolution_m, data, robot_pose}- Occupancy grid exposed as REST API:
GET /api/slam/map - Dashboard renders live occupancy grid on HTML5 canvas
Zero-cloud-egress enforcement mode. When active:
- All outbound LLM API calls are blocked
- Only local providers (Ollama, llama.cpp, MLX, Apple Foundation Models, ONNX) are allowed
- Camera frames are never transmitted externally
- Audit log records every blocked egress attempt
from castor.privacy_mode import get_privacy_manager
pm = get_privacy_manager()
pm.enable() # blocks all cloud providers
pm.disable()
pm.status() # -> {active, blocked_count, ...}Generates valid RCAN YAML config from a natural language hardware description.
- Uses the active brain provider to interpret the description
- Validates output against
config/rcan.schema.json - Falls back to nearest preset template if validation fails
castor config generate --description "two-wheeled Raspberry Pi rover with USB camera"REST: POST /api/config/generate → {rcan_yaml: string, preset_used: string}
Sends robot status and command responses to a Microsoft Teams channel via incoming webhooks.
- Outbound:
TEAMS_WEBHOOK_URL— formatted Adaptive Cards - Bot auth:
TEAMS_APP_ID,TEAMS_APP_PASSWORD,TEAMS_TENANT_ID - Incoming commands via Bot Framework webhook (optional)
Connects to a Matrix homeserver (matrix.org or self-hosted) via matrix-nio.
- Env vars:
MATRIX_HOMESERVER_URL,MATRIX_USER_ID,MATRIX_ACCESS_TOKEN - E2E encryption supported (if libolm installed)
- Responds to commands in any joined room