opencastor 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (438) hide show
  1. castor/__init__.py +43 -0
  2. castor/__main__.py +6 -0
  3. castor/action_validator.py +295 -0
  4. castor/agent_tools.py +632 -0
  5. castor/agents/__init__.py +61 -0
  6. castor/agents/base.py +138 -0
  7. castor/agents/communicator.py +197 -0
  8. castor/agents/guardian.py +233 -0
  9. castor/agents/manipulator_agent.py +97 -0
  10. castor/agents/navigator.py +326 -0
  11. castor/agents/observer.py +311 -0
  12. castor/agents/orchestrator.py +309 -0
  13. castor/agents/registry.py +147 -0
  14. castor/agents/shared_state.py +349 -0
  15. castor/api.py +10562 -0
  16. castor/api_errors.py +76 -0
  17. castor/apikeys.py +196 -0
  18. castor/approvals.py +221 -0
  19. castor/attestation_generator.py +334 -0
  20. castor/audit.py +449 -0
  21. castor/auth/__init__.py +226 -0
  22. castor/auth/jwt_pqc.py +156 -0
  23. castor/auth/m2m_trusted.py +230 -0
  24. castor/auth/provider_auth.py +353 -0
  25. castor/auth/robot_handshake.py +104 -0
  26. castor/auth.py +218 -0
  27. castor/auth_jwt.py +328 -0
  28. castor/authority.py +364 -0
  29. castor/avoidance.py +272 -0
  30. castor/backup.py +193 -0
  31. castor/battery.py +132 -0
  32. castor/behaviors.py +2449 -0
  33. castor/benchmark.py +164 -0
  34. castor/benchmarker.py +254 -0
  35. castor/brain/__init__.py +19 -0
  36. castor/brain/autodream.py +272 -0
  37. castor/brain/autodream_issues.py +113 -0
  38. castor/brain/autodream_runner.py +338 -0
  39. castor/brain/compaction.py +73 -0
  40. castor/brain/memory_schema.py +292 -0
  41. castor/brain/robot_context.py +152 -0
  42. castor/calibrate.py +389 -0
  43. castor/camera.py +462 -0
  44. castor/channels/__init__.py +164 -0
  45. castor/channels/base.py +382 -0
  46. castor/channels/discord_channel.py +165 -0
  47. castor/channels/homeassistant_channel.py +211 -0
  48. castor/channels/matrix_channel.py +116 -0
  49. castor/channels/mqtt_channel.py +480 -0
  50. castor/channels/rcan_mqtt_transport.py +184 -0
  51. castor/channels/scope_resolver.py +162 -0
  52. castor/channels/session.py +240 -0
  53. castor/channels/signal_channel.py +276 -0
  54. castor/channels/slack_channel.py +201 -0
  55. castor/channels/teams_channel.py +126 -0
  56. castor/channels/telegram_channel.py +149 -0
  57. castor/channels/whatsapp.py +9 -0
  58. castor/channels/whatsapp_neonize.py +540 -0
  59. castor/channels/whatsapp_twilio.py +105 -0
  60. castor/claude_proxy.py +203 -0
  61. castor/cli.py +9117 -0
  62. castor/client.py +451 -0
  63. castor/cloud/__init__.py +3 -0
  64. castor/cloud/bridge.py +2410 -0
  65. castor/cloud/consent_manager.py +258 -0
  66. castor/cloud/firestore_models.py +151 -0
  67. castor/command_interpreter.py +144 -0
  68. castor/commands/__init__.py +1 -0
  69. castor/commands/benchmark.py +385 -0
  70. castor/commands/compete.py +173 -0
  71. castor/commands/deploy.py +270 -0
  72. castor/commands/hub.py +462 -0
  73. castor/commands/leaderboard.py +167 -0
  74. castor/commands/recommend.py +378 -0
  75. castor/commands/research.py +225 -0
  76. castor/commands/season.py +103 -0
  77. castor/commands/swarm.py +589 -0
  78. castor/commands/update.py +286 -0
  79. castor/competitions/__init__.py +1 -0
  80. castor/competitions/bracket_season.py +463 -0
  81. castor/competitions/models.py +335 -0
  82. castor/competitions/sprint.py +325 -0
  83. castor/competitions/threshold_race.py +468 -0
  84. castor/compliance.py +204 -0
  85. castor/components.py +305 -0
  86. castor/confidence_gate.py +114 -0
  87. castor/config_history.py +186 -0
  88. castor/configure.py +379 -0
  89. castor/conformance.py +2176 -0
  90. castor/connectivity.py +132 -0
  91. castor/context.py +444 -0
  92. castor/contribute/__init__.py +1 -0
  93. castor/contribute/coordinator.py +550 -0
  94. castor/contribute/credits.py +284 -0
  95. castor/contribute/fleet.py +123 -0
  96. castor/contribute/hardware_profile.py +25 -0
  97. castor/contribute/harness_eval.py +374 -0
  98. castor/contribute/runner.py +177 -0
  99. castor/contribute/work_unit.py +43 -0
  100. castor/crash.py +137 -0
  101. castor/crypto/__init__.py +17 -0
  102. castor/crypto/pqc.py +345 -0
  103. castor/daemon.py +657 -0
  104. castor/dashboard.py +1745 -0
  105. castor/dashboard_memory_timeline.py +284 -0
  106. castor/dashboard_tui.py +690 -0
  107. castor/delegation.py +77 -0
  108. castor/demo.py +706 -0
  109. castor/deploy_ota.py +298 -0
  110. castor/depth.py +222 -0
  111. castor/detection.py +302 -0
  112. castor/diff.py +123 -0
  113. castor/doctor.py +1026 -0
  114. castor/drivers/__init__.py +195 -0
  115. castor/drivers/acb_driver.py +700 -0
  116. castor/drivers/arduino_driver.py +223 -0
  117. castor/drivers/base.py +166 -0
  118. castor/drivers/battery_driver.py +550 -0
  119. castor/drivers/can_transport.py +121 -0
  120. castor/drivers/composite.py +272 -0
  121. castor/drivers/dynamixel.py +172 -0
  122. castor/drivers/esp32_ble_driver.py +261 -0
  123. castor/drivers/esp32_websocket.py +232 -0
  124. castor/drivers/ev3dev_driver.py +287 -0
  125. castor/drivers/feetech_driver.py +358 -0
  126. castor/drivers/gpio_driver.py +155 -0
  127. castor/drivers/imu_driver.py +1607 -0
  128. castor/drivers/ipc.py +246 -0
  129. castor/drivers/lidar_driver.py +1564 -0
  130. castor/drivers/odrive_driver.py +328 -0
  131. castor/drivers/pca9685.py +335 -0
  132. castor/drivers/picamera2_driver.py +233 -0
  133. castor/drivers/reachy_driver.py +249 -0
  134. castor/drivers/ros2_driver.py +188 -0
  135. castor/drivers/simulation_driver.py +274 -0
  136. castor/drivers/spike_driver.py +219 -0
  137. castor/drivers/stepper_driver.py +170 -0
  138. castor/drivers/thermal_driver.py +251 -0
  139. castor/drivers/worker.py +29 -0
  140. castor/dual_model.py +470 -0
  141. castor/embedding_interpreter.py +577 -0
  142. castor/episode_search.py +196 -0
  143. castor/eu_register.py +89 -0
  144. castor/eval_harness.py +354 -0
  145. castor/export.py +295 -0
  146. castor/finetune.py +287 -0
  147. castor/firmware.py +441 -0
  148. castor/fix.py +200 -0
  149. castor/fleet/__init__.py +0 -0
  150. castor/fleet/group_policy.py +183 -0
  151. castor/fleet.py +163 -0
  152. castor/fleet_telemetry.py +508 -0
  153. castor/fria.py +368 -0
  154. castor/fs/__init__.py +307 -0
  155. castor/fs/context.py +331 -0
  156. castor/fs/memory.py +326 -0
  157. castor/fs/namespace.py +235 -0
  158. castor/fs/permissions.py +374 -0
  159. castor/fs/proc.py +205 -0
  160. castor/fs/safety.py +822 -0
  161. castor/generate_sdk.py +457 -0
  162. castor/geofence.py +301 -0
  163. castor/gestures.py +263 -0
  164. castor/hailo_vision.py +417 -0
  165. castor/hardware/__init__.py +0 -0
  166. castor/hardware/so_arm101/__init__.py +11 -0
  167. castor/hardware/so_arm101/assembly_guide.py +120 -0
  168. castor/hardware/so_arm101/cli.py +387 -0
  169. castor/hardware/so_arm101/config_generator.py +134 -0
  170. castor/hardware/so_arm101/constants.py +176 -0
  171. castor/hardware/so_arm101/lerobot_bridge.py +231 -0
  172. castor/hardware/so_arm101/motor_setup.py +261 -0
  173. castor/hardware/so_arm101/port_finder.py +200 -0
  174. castor/hardware/so_arm101/rcan_bridge.py +91 -0
  175. castor/hardware/so_arm101/safety_bridge.py +152 -0
  176. castor/hardware/so_arm101/vision.py +49 -0
  177. castor/hardware_detect.py +1356 -0
  178. castor/harness/__init__.py +110 -0
  179. castor/harness/circuit_breaker.py +120 -0
  180. castor/harness/context_compressor.py +125 -0
  181. castor/harness/core.py +975 -0
  182. castor/harness/cost_meter.py +145 -0
  183. castor/harness/default_harness.yaml +166 -0
  184. castor/harness/dlq.py +146 -0
  185. castor/harness/memory.py +151 -0
  186. castor/harness/pattern.py +99 -0
  187. castor/harness/prompt_guard.py +91 -0
  188. castor/harness/rollback.py +140 -0
  189. castor/harness/security.py +123 -0
  190. castor/harness/span_tracer.py +196 -0
  191. castor/harness/visual_planner.py +367 -0
  192. castor/harness/working_memory.py +87 -0
  193. castor/harness.py +986 -0
  194. castor/healthcheck.py +588 -0
  195. castor/hitl_gate.py +139 -0
  196. castor/hooks/__init__.py +5 -0
  197. castor/hooks/default_hooks.py +79 -0
  198. castor/hooks/runner.py +99 -0
  199. castor/hotword.py +325 -0
  200. castor/hub.py +605 -0
  201. castor/i18n.py +314 -0
  202. castor/idle.py +385 -0
  203. castor/ina219.py +154 -0
  204. castor/incidents.py +111 -0
  205. castor/inference/__init__.py +5 -0
  206. castor/inference/streaming.py +287 -0
  207. castor/init_config.py +211 -0
  208. castor/init_wizard.py +142 -0
  209. castor/instructions_for_use.py +135 -0
  210. castor/learn.py +216 -0
  211. castor/learner/__init__.py +58 -0
  212. castor/learner/alma.py +227 -0
  213. castor/learner/apply_stage.py +226 -0
  214. castor/learner/dev_stage.py +167 -0
  215. castor/learner/episode.py +52 -0
  216. castor/learner/episode_store.py +151 -0
  217. castor/learner/patches.py +150 -0
  218. castor/learner/pm_stage.py +318 -0
  219. castor/learner/qa_stage.py +197 -0
  220. castor/learner/sisyphus.py +171 -0
  221. castor/lint.py +328 -0
  222. castor/llmfit.py +493 -0
  223. castor/llmfit_helper.py +362 -0
  224. castor/loa.py +132 -0
  225. castor/logs.py +190 -0
  226. castor/logs_viewer.py +126 -0
  227. castor/main.py +1854 -0
  228. castor/mcp_auth.py +104 -0
  229. castor/mcp_fleet.py +262 -0
  230. castor/mcp_server.py +780 -0
  231. castor/memory/__init__.py +20 -0
  232. castor/memory/compaction.py +129 -0
  233. castor/memory/consolidator.py +501 -0
  234. castor/memory/episode.py +1584 -0
  235. castor/memory/episodic.py +169 -0
  236. castor/memory/replay.py +324 -0
  237. castor/memory_search.py +285 -0
  238. castor/mesh.py +429 -0
  239. castor/metrics.py +1087 -0
  240. castor/migrate.py +527 -0
  241. castor/mission.py +384 -0
  242. castor/nav.py +97 -0
  243. castor/network.py +228 -0
  244. castor/offline_fallback.py +200 -0
  245. castor/optimizer.py +635 -0
  246. castor/orchestration/__init__.py +0 -0
  247. castor/orchestration/thread_context.py +60 -0
  248. castor/peripherals.py +1093 -0
  249. castor/personalities.py +284 -0
  250. castor/plugins.py +468 -0
  251. castor/pointcloud.py +235 -0
  252. castor/privacy.py +163 -0
  253. castor/privacy_mode.py +129 -0
  254. castor/profiles/__init__.py +215 -0
  255. castor/profiles/arduino/__init__.py +0 -0
  256. castor/profiles/arduino/uno.yaml +13 -0
  257. castor/profiles/coral/__init__.py +0 -0
  258. castor/profiles/coral/tpu-inference.yaml +16 -0
  259. castor/profiles/hlabs/__init__.py +1 -0
  260. castor/profiles/hlabs/acb-arm-3dof.yaml +35 -0
  261. castor/profiles/hlabs/acb-biped-6dof.yaml +65 -0
  262. castor/profiles/hlabs/acb-single.yaml +21 -0
  263. castor/profiles/lerobot/__init__.py +0 -0
  264. castor/profiles/lerobot/aloha.yaml +35 -0
  265. castor/profiles/lerobot/koch-arm.yaml +20 -0
  266. castor/profiles/lerobot/so-arm101-bimanual.yaml +36 -0
  267. castor/profiles/lerobot/so-arm101-follower.yaml +22 -0
  268. castor/profiles/lerobot/so-arm101-leader.yaml +22 -0
  269. castor/profiles/odrive/__init__.py +0 -0
  270. castor/profiles/odrive/differential.yaml +14 -0
  271. castor/profiles/pollen/__init__.py +0 -0
  272. castor/profiles/pollen/reachy-mini.yaml +13 -0
  273. castor/profiles/pollen/reachy2.yaml +13 -0
  274. castor/prompt_cache.py +192 -0
  275. castor/provider_fallback.py +197 -0
  276. castor/providers/__init__.py +111 -0
  277. castor/providers/anthropic_provider.py +360 -0
  278. castor/providers/apple_preflight.py +192 -0
  279. castor/providers/apple_provider.py +226 -0
  280. castor/providers/base.py +578 -0
  281. castor/providers/clap_embedding_provider.py +208 -0
  282. castor/providers/clip_embedding_provider.py +217 -0
  283. castor/providers/consensus_provider.py +398 -0
  284. castor/providers/deepseek_provider.py +166 -0
  285. castor/providers/embedding_backend.py +41 -0
  286. castor/providers/failover.py +290 -0
  287. castor/providers/gated.py +328 -0
  288. castor/providers/gemini_embedding_provider.py +241 -0
  289. castor/providers/google_provider.py +259 -0
  290. castor/providers/grok_provider.py +163 -0
  291. castor/providers/groq_provider.py +134 -0
  292. castor/providers/huggingface_provider.py +334 -0
  293. castor/providers/imagebind_provider.py +157 -0
  294. castor/providers/kv_compression.py +93 -0
  295. castor/providers/llamacpp_provider.py +350 -0
  296. castor/providers/mistral_provider.py +166 -0
  297. castor/providers/mlx_provider.py +288 -0
  298. castor/providers/ollama_provider.py +648 -0
  299. castor/providers/onnx_provider.py +309 -0
  300. castor/providers/openai_provider.py +184 -0
  301. castor/providers/openrouter_provider.py +166 -0
  302. castor/providers/pool_provider.py +1241 -0
  303. castor/providers/sentence_transformers_provider.py +201 -0
  304. castor/providers/taalas_provider.py +215 -0
  305. castor/providers/task_router.py +78 -0
  306. castor/providers/vertex_provider.py +211 -0
  307. castor/providers/vla_provider.py +248 -0
  308. castor/py.typed +0 -0
  309. castor/quantum_commitment.py +189 -0
  310. castor/rate_limiting.py +289 -0
  311. castor/rcan/__init__.py +71 -0
  312. castor/rcan/capabilities.py +120 -0
  313. castor/rcan/commitment_chain.py +299 -0
  314. castor/rcan/config_safety.py +278 -0
  315. castor/rcan/http_transport.py +96 -0
  316. castor/rcan/invoke.py +308 -0
  317. castor/rcan/jwt_auth.py +216 -0
  318. castor/rcan/key_rotation.py +226 -0
  319. castor/rcan/mdns.py +255 -0
  320. castor/rcan/message.py +443 -0
  321. castor/rcan/message_signing.py +210 -0
  322. castor/rcan/node_broadcaster.py +105 -0
  323. castor/rcan/node_resolver.py +273 -0
  324. castor/rcan/parallel_invoke.py +126 -0
  325. castor/rcan/rbac.py +455 -0
  326. castor/rcan/registry.py +467 -0
  327. castor/rcan/router.py +271 -0
  328. castor/rcan/ruri.py +154 -0
  329. castor/rcan/sdk_bridge.py +342 -0
  330. castor/rcan/sdk_compat.py +108 -0
  331. castor/rcan/telemetry_fields.py +67 -0
  332. castor/rcan/verification.py +79 -0
  333. castor/rcan3/__init__.py +40 -0
  334. castor/rcan3/castor_harness.py +35 -0
  335. castor/rcan3/compliance.py +177 -0
  336. castor/rcan3/harness_protocol.py +51 -0
  337. castor/rcan3/identity.py +155 -0
  338. castor/rcan3/reader.py +87 -0
  339. castor/rcan3/rrf_client.py +94 -0
  340. castor/rcan3/signer.py +69 -0
  341. castor/rcan_generator.py +227 -0
  342. castor/record.py +185 -0
  343. castor/recorder.py +414 -0
  344. castor/registry.py +364 -0
  345. castor/repl.py +129 -0
  346. castor/response_cache.py +321 -0
  347. castor/rrf_cmd.py +185 -0
  348. castor/runtime_stats.py +203 -0
  349. castor/safety/__init__.py +32 -0
  350. castor/safety/anti_subversion.py +312 -0
  351. castor/safety/authorization.py +408 -0
  352. castor/safety/bounds.py +643 -0
  353. castor/safety/monitor.py +490 -0
  354. castor/safety/p66_manifest.py +602 -0
  355. castor/safety/protocol.py +846 -0
  356. castor/safety/state.py +222 -0
  357. castor/safety_benchmark.py +306 -0
  358. castor/safety_telemetry.py +160 -0
  359. castor/sbom.py +356 -0
  360. castor/schedule.py +214 -0
  361. castor/sdk/__init__.py +9 -0
  362. castor/secret_provider.py +248 -0
  363. castor/security_posture.py +149 -0
  364. castor/services/__init__.py +1 -0
  365. castor/services/rrf_poller.py +239 -0
  366. castor/setup_catalog.py +785 -0
  367. castor/setup_service.py +1248 -0
  368. castor/shell.py +227 -0
  369. castor/sim.py +275 -0
  370. castor/sim_bridge.py +315 -0
  371. castor/skills/__init__.py +5 -0
  372. castor/skills/contribute.py +193 -0
  373. castor/skills/loader.py +532 -0
  374. castor/skills/rcan_skills.py +252 -0
  375. castor/slam.py +300 -0
  376. castor/snapshot.py +210 -0
  377. castor/specialists/__init__.py +48 -0
  378. castor/specialists/base_specialist.py +65 -0
  379. castor/specialists/dock.py +246 -0
  380. castor/specialists/manipulator.py +185 -0
  381. castor/specialists/responder.py +171 -0
  382. castor/specialists/scout.py +296 -0
  383. castor/specialists/task_planner.py +247 -0
  384. castor/stream.py +226 -0
  385. castor/swarm/__init__.py +54 -0
  386. castor/swarm/consensus.py +338 -0
  387. castor/swarm/coordinator.py +276 -0
  388. castor/swarm/events.py +33 -0
  389. castor/swarm/oak_worker.py +141 -0
  390. castor/swarm/patch_sync.py +160 -0
  391. castor/swarm/peer.py +170 -0
  392. castor/swarm/shared_memory.py +148 -0
  393. castor/swarm/worker.py +209 -0
  394. castor/swarm_memory.py +275 -0
  395. castor/system_info.py +294 -0
  396. castor/telemetry/__init__.py +414 -0
  397. castor/telemetry/prometheus.py +212 -0
  398. castor/templates/__init__.py +36 -0
  399. castor/templates/fria.html.j2 +164 -0
  400. castor/templates/geofence.html +364 -0
  401. castor/templates/systemd/__init__.py +0 -0
  402. castor/templates/systemd/castor-gateway.service.tmpl +29 -0
  403. castor/templates/systemd/opencastor-attestation.service.tmpl +18 -0
  404. castor/test_hardware.py +255 -0
  405. castor/thought_log.py +93 -0
  406. castor/tiered_brain.py +470 -0
  407. castor/timelapse.py +251 -0
  408. castor/tools.py +340 -0
  409. castor/tools_ext/__init__.py +13 -0
  410. castor/tools_ext/permissions.py +68 -0
  411. castor/tools_ext/profiles.py +85 -0
  412. castor/trajectory.py +390 -0
  413. castor/tts/__init__.py +48 -0
  414. castor/tts/elevenlabs_backend.py +188 -0
  415. castor/tts_local.py +229 -0
  416. castor/update_check.py +158 -0
  417. castor/updater.py +97 -0
  418. castor/usage.py +402 -0
  419. castor/voice.py +1061 -0
  420. castor/voice_loop.py +305 -0
  421. castor/watch.py +388 -0
  422. castor/watchdog.py +111 -0
  423. castor/watermark.py +63 -0
  424. castor/web_wizard/__init__.py +5 -0
  425. castor/web_wizard/server.py +831 -0
  426. castor/web_wizard.py +537 -0
  427. castor/webhooks.py +269 -0
  428. castor/webrtc_audio.py +336 -0
  429. castor/wizard.py +3174 -0
  430. castor/workspace.py +234 -0
  431. castor/world/__init__.py +5 -0
  432. castor/world/model.py +122 -0
  433. opencastor-3.0.0.dist-info/METADATA +426 -0
  434. opencastor-3.0.0.dist-info/RECORD +438 -0
  435. opencastor-3.0.0.dist-info/WHEEL +5 -0
  436. opencastor-3.0.0.dist-info/entry_points.txt +3 -0
  437. opencastor-3.0.0.dist-info/licenses/LICENSE +189 -0
  438. opencastor-3.0.0.dist-info/top_level.txt +1 -0
castor/__init__.py ADDED
@@ -0,0 +1,43 @@
1
+ """OpenCastor: The Universal Runtime for Embodied AI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try:
6
+ from importlib.metadata import version as _pkg_version
7
+
8
+ __version__ = _pkg_version("opencastor")
9
+ except Exception:
10
+ __version__ = "3.0.0" # fallback
11
+
12
+
13
+ def initialize_safety(safety_layer, config: dict):
14
+ """Initialize and wire the full Protocol 66 safety stack.
15
+
16
+ Creates a :class:`~castor.safety.monitor.SensorMonitor` from the
17
+ ``config["monitor"]`` section and connects it to *safety_layer* via
18
+ :func:`~castor.safety.monitor.wire_safety_layer`. Call this once during
19
+ runtime startup, then call ``monitor.start()`` to begin polling.
20
+
21
+ Args:
22
+ safety_layer: A :class:`~castor.fs.safety.SafetyLayer` (or
23
+ ``CastorFS.safety``) instance.
24
+ config: Runtime config dict (uses ``config["monitor"]`` sub-key).
25
+
26
+ Returns:
27
+ The started :class:`~castor.safety.monitor.SensorMonitor` instance.
28
+ """
29
+ from castor.safety.monitor import MonitorThresholds, SensorMonitor, wire_safety_layer
30
+
31
+ monitor_cfg = config.get("monitor", {})
32
+ thresholds_cfg = monitor_cfg.get("thresholds", {})
33
+ thresholds = MonitorThresholds(**thresholds_cfg) if thresholds_cfg else None
34
+ monitor = SensorMonitor(
35
+ thresholds=thresholds,
36
+ interval=float(monitor_cfg.get("interval", 5.0)),
37
+ consecutive_critical=int(monitor_cfg.get("consecutive_critical", 3)),
38
+ )
39
+ wire_safety_layer(monitor, safety_layer)
40
+ return monitor
41
+
42
+
43
+ __all__ = ["__version__", "initialize_safety"]
castor/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow running OpenCastor as ``python -m castor``."""
2
+
3
+ from castor.cli import _friendly_error_handler
4
+
5
+ if __name__ == "__main__":
6
+ _friendly_error_handler()
@@ -0,0 +1,295 @@
1
+ """
2
+ Structured action validation for OpenCastor (#271).
3
+
4
+ Validates parsed robot action dicts against built-in or custom JSON schemas.
5
+ Uses jsonschema (>=4.20.0, already a core dependency) for schema enforcement.
6
+
7
+ Usage::
8
+
9
+ from castor.action_validator import validate_action, ActionValidator
10
+
11
+ # Validate a move action
12
+ result = validate_action({"type": "move", "linear": 0.5, "angular": 0.0})
13
+ if not result.valid:
14
+ for err in result.errors:
15
+ print(f"ERROR: {err}")
16
+
17
+ # Custom schemas merged with built-ins
18
+ validator = ActionValidator(custom_schemas={"spray": {
19
+ "type": "object",
20
+ "required": ["type"],
21
+ "properties": {
22
+ "type": {"type": "string"},
23
+ "duration_s": {"type": "number", "minimum": 0},
24
+ },
25
+ }})
26
+ result = validator.validate({"type": "spray", "duration_s": 2.0})
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import logging
32
+ import threading
33
+ from dataclasses import dataclass, field
34
+ from typing import Any, Optional
35
+
36
+ logger = logging.getLogger("OpenCastor.ActionValidator")
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Built-in JSON schemas for the core action vocabulary
40
+ # ---------------------------------------------------------------------------
41
+
42
+ _ACTION_SCHEMAS: dict[str, dict] = {
43
+ "move": {
44
+ "type": "object",
45
+ "required": ["type"],
46
+ "properties": {
47
+ "type": {"type": "string"},
48
+ "linear": {"type": "number"},
49
+ "angular": {"type": "number"},
50
+ "linear_x": {"type": "number"}, # legacy alias
51
+ "angular_z": {"type": "number"}, # legacy alias
52
+ "speed": {"type": "number", "minimum": 0, "maximum": 1},
53
+ "duration_s": {"type": "number", "minimum": 0},
54
+ },
55
+ },
56
+ "stop": {
57
+ "type": "object",
58
+ "required": ["type"],
59
+ "properties": {
60
+ "type": {"type": "string"},
61
+ },
62
+ "additionalProperties": True,
63
+ },
64
+ "wait": {
65
+ "type": "object",
66
+ "required": ["type"],
67
+ "properties": {
68
+ "type": {"type": "string"},
69
+ "duration_s": {"type": "number", "minimum": 0},
70
+ },
71
+ },
72
+ "grip": {
73
+ "type": "object",
74
+ "required": ["type"],
75
+ "properties": {
76
+ "type": {"type": "string"},
77
+ "position": {"type": "number", "minimum": 0, "maximum": 1},
78
+ "force": {"type": "number", "minimum": 0, "maximum": 1},
79
+ },
80
+ },
81
+ "nav_waypoint": {
82
+ "type": "object",
83
+ "required": ["type"],
84
+ "properties": {
85
+ "type": {"type": "string"},
86
+ "distance_m": {"type": "number", "minimum": 0},
87
+ "heading_deg": {"type": "number"},
88
+ "speed": {"type": "number", "minimum": 0, "maximum": 1},
89
+ },
90
+ },
91
+ }
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Data classes
96
+ # ---------------------------------------------------------------------------
97
+
98
+
99
+ @dataclass
100
+ class ValidationResult:
101
+ """Result of validating a single action dict."""
102
+
103
+ valid: bool
104
+ action_type: str
105
+ errors: list[str] = field(default_factory=list)
106
+ warnings: list[str] = field(default_factory=list)
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # ActionValidator
111
+ # ---------------------------------------------------------------------------
112
+
113
+
114
+ class ActionValidator:
115
+ """Validate robot action dicts against built-in or custom JSON schemas.
116
+
117
+ Args:
118
+ custom_schemas: Dict mapping action type name → JSON schema dict.
119
+ These are merged with the built-in schemas; custom
120
+ entries take precedence over built-ins of the same name.
121
+ """
122
+
123
+ def __init__(self, custom_schemas: Optional[dict[str, dict]] = None) -> None:
124
+ self._schemas: dict[str, dict] = {**_ACTION_SCHEMAS}
125
+ self._schema_sources: dict[str, str] = {t: "builtin" for t in _ACTION_SCHEMAS}
126
+ if custom_schemas:
127
+ self._schemas.update(custom_schemas)
128
+ for t in custom_schemas:
129
+ self._schema_sources[t] = "rcan_config"
130
+
131
+ # ------------------------------------------------------------------
132
+ # Public API
133
+ # ------------------------------------------------------------------
134
+
135
+ def validate(self, action: dict[str, Any]) -> ValidationResult:
136
+ """Validate an action dict.
137
+
138
+ Args:
139
+ action: Parsed action dictionary (e.g. ``{"type": "move", "linear": 0.5}``).
140
+
141
+ Returns:
142
+ A :class:`ValidationResult` describing whether the action is valid,
143
+ any hard errors, and any non-fatal warnings.
144
+ """
145
+ # Step 1 — must be a non-None dict
146
+ if action is None or not isinstance(action, dict):
147
+ return ValidationResult(
148
+ valid=False,
149
+ action_type="",
150
+ errors=["action must be a dict"],
151
+ )
152
+
153
+ # Step 2 — must contain a "type" key with a non-None value
154
+ action_type = action.get("type")
155
+ if not action_type:
156
+ return ValidationResult(
157
+ valid=False,
158
+ action_type="",
159
+ errors=["missing 'type' field"],
160
+ )
161
+
162
+ action_type = str(action_type)
163
+
164
+ # Step 3 — look up schema; unknown types pass with a warning
165
+ schema = self._schemas.get(action_type)
166
+ if schema is None:
167
+ logger.debug("Unknown action type '%s' — skipping validation", action_type)
168
+ return ValidationResult(
169
+ valid=True,
170
+ action_type=action_type,
171
+ warnings=[f"unknown action type '{action_type}' — skipping validation"],
172
+ )
173
+
174
+ # Step 4 — attempt jsonschema validation (lazy import)
175
+ try:
176
+ import jsonschema # noqa: PLC0415
177
+ except ImportError:
178
+ logger.warning(
179
+ "jsonschema not installed — skipping schema validation for '%s'", action_type
180
+ )
181
+ return ValidationResult(
182
+ valid=True,
183
+ action_type=action_type,
184
+ warnings=["jsonschema not installed — skipping schema validation"],
185
+ )
186
+
187
+ try:
188
+ jsonschema.validate(action, schema)
189
+ except jsonschema.ValidationError as exc:
190
+ return ValidationResult(
191
+ valid=False,
192
+ action_type=action_type,
193
+ errors=[exc.message],
194
+ )
195
+
196
+ # Step 5 — warn about unknown properties when additionalProperties is
197
+ # not explicitly set to True
198
+ warnings: list[str] = []
199
+ schema_props = schema.get("properties", {})
200
+ allows_additional = schema.get("additionalProperties", False) is True
201
+ if schema_props and not allows_additional:
202
+ for key in action:
203
+ if key not in schema_props:
204
+ warnings.append(f"unknown field '{key}' for action type '{action_type}'")
205
+ logger.debug("Action '%s' contains unknown field '%s'", action_type, key)
206
+
207
+ return ValidationResult(valid=True, action_type=action_type, warnings=warnings)
208
+
209
+ def known_types(self) -> list[str]:
210
+ """Return a sorted list of known action type names."""
211
+ return sorted(self._schemas.keys())
212
+
213
+ def schema_source_for(self, action_type: str) -> str:
214
+ """Return the source of the schema for *action_type*.
215
+
216
+ Returns:
217
+ ``"builtin"`` — schema is part of OpenCastor's built-in vocabulary.
218
+ ``"rcan_config"`` — schema was loaded from the RCAN ``action_schemas`` block.
219
+ ``"unknown"`` — no schema registered for this action type.
220
+ """
221
+ return self._schema_sources.get(action_type, "unknown")
222
+
223
+
224
+ # ---------------------------------------------------------------------------
225
+ # Module-level singleton helpers
226
+ # ---------------------------------------------------------------------------
227
+
228
+ _default_validator: Optional[ActionValidator] = None
229
+ _validator_lock = threading.Lock()
230
+
231
+
232
+ def get_validator(custom_schemas: Optional[dict[str, dict]] = None) -> ActionValidator:
233
+ """Return the singleton :class:`ActionValidator`.
234
+
235
+ If *custom_schemas* is provided the singleton is (re-)created with those
236
+ schemas merged in. Subsequent calls without *custom_schemas* return the
237
+ previously created instance.
238
+ """
239
+ global _default_validator
240
+ with _validator_lock:
241
+ if _default_validator is None or custom_schemas is not None:
242
+ _default_validator = ActionValidator(custom_schemas=custom_schemas)
243
+ return _default_validator
244
+
245
+
246
+ def validate_action(action: dict[str, Any]) -> ValidationResult:
247
+ """Validate *action* using the default singleton :class:`ActionValidator`.
248
+
249
+ This is the most convenient entry-point for one-off validation::
250
+
251
+ from castor.action_validator import validate_action
252
+
253
+ result = validate_action({"type": "move", "linear": 0.5})
254
+ assert result.valid
255
+
256
+ Args:
257
+ action: Parsed action dictionary.
258
+
259
+ Returns:
260
+ :class:`ValidationResult`
261
+ """
262
+ return get_validator().validate(action)
263
+
264
+
265
+ def init_from_config(config: dict[str, Any]) -> ActionValidator:
266
+ """Initialise the singleton validator with custom schemas from RCAN *config*.
267
+
268
+ Reads the ``action_schemas`` mapping from the RCAN config dict and merges
269
+ any entries with the built-in action schemas. Custom entries take
270
+ precedence over built-ins with the same action type name.
271
+
272
+ Usage in RCAN config::
273
+
274
+ action_schemas:
275
+ spray:
276
+ type: object
277
+ required: [type]
278
+ properties:
279
+ type: {type: string}
280
+ duration_s: {type: number, minimum: 0}
281
+
282
+ Args:
283
+ config: RCAN agent config dict (the top-level mapping).
284
+
285
+ Returns:
286
+ The (re-)initialised :class:`ActionValidator` singleton.
287
+ """
288
+ raw: dict[str, dict] = config.get("action_schemas") or {}
289
+ if raw:
290
+ logger.info(
291
+ "ActionValidator: loading %d custom schema(s) from RCAN config: %s",
292
+ len(raw),
293
+ list(raw.keys()),
294
+ )
295
+ return get_validator(custom_schemas=raw if raw else None)