dosync 0.6.2__tar.gz → 0.7.0__tar.gz

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 (206) hide show
  1. {dosync-0.6.2 → dosync-0.7.0}/PKG-INFO +177 -13
  2. {dosync-0.6.2 → dosync-0.7.0}/README.md +169 -9
  3. {dosync-0.6.2 → dosync-0.7.0}/dosync/__init__.py +2 -2
  4. {dosync-0.6.2 → dosync-0.7.0}/dosync/adapter_drafting.py +7 -2
  5. {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/__init__.py +14 -30
  6. {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/homeassistant.py +306 -61
  7. {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/matter.py +39 -30
  8. {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/mqtt.py +1 -1
  9. {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/notifications.py +9 -9
  10. {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/shelly.py +37 -28
  11. {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/wiz.py +58 -28
  12. dosync-0.7.0/dosync/audit.py +809 -0
  13. {dosync-0.6.2 → dosync-0.7.0}/dosync/audit_backup.py +16 -12
  14. {dosync-0.6.2 → dosync-0.7.0}/dosync/auth.py +11 -11
  15. {dosync-0.6.2 → dosync-0.7.0}/dosync/certify.py +158 -4
  16. dosync-0.7.0/dosync/composite_executor.py +288 -0
  17. {dosync-0.6.2 → dosync-0.7.0}/dosync/dashboard.html +25 -4
  18. {dosync-0.6.2 → dosync-0.7.0}/dosync/db.py +181 -68
  19. {dosync-0.6.2 → dosync-0.7.0}/dosync/declarative.py +53 -9
  20. {dosync-0.6.2 → dosync-0.7.0}/dosync/discovery.py +4 -4
  21. {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/3d-printer.yaml +2 -2
  22. {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/air-conditioner.yaml +2 -2
  23. {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/building-lighting.json +2 -1
  24. {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/industrial-conveyor.yaml +2 -2
  25. {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/light-generic.yaml +5 -4
  26. {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/television.yaml +2 -2
  27. dosync-0.7.0/dosync/execution.py +425 -0
  28. dosync-0.7.0/dosync/hub.py +999 -0
  29. {dosync-0.6.2 → dosync-0.7.0}/dosync/manage.py +5 -5
  30. {dosync-0.6.2 → dosync-0.7.0}/dosync/mcp_server.py +263 -97
  31. {dosync-0.6.2 → dosync-0.7.0}/dosync/metrics.py +10 -0
  32. {dosync-0.6.2 → dosync-0.7.0}/dosync/models.py +177 -96
  33. dosync-0.7.0/dosync/occupancy.py +105 -0
  34. dosync-0.7.0/dosync/plan_executor.py +347 -0
  35. {dosync-0.6.2 → dosync-0.7.0}/dosync/policies.py +16 -23
  36. dosync-0.7.0/dosync/registry.py +167 -0
  37. dosync-0.7.0/dosync/resolvers.py +1176 -0
  38. dosync-0.7.0/dosync/restore.py +112 -0
  39. {dosync-0.6.2 → dosync-0.7.0}/dosync/security.py +22 -22
  40. {dosync-0.6.2 → dosync-0.7.0}/dosync/server.py +344 -40
  41. dosync-0.7.0/dosync/spec/TAG-VOCABULARY.md +332 -0
  42. dosync-0.7.0/dosync/state_refresh.py +154 -0
  43. dosync-0.7.0/dosync/telemetry.py +99 -0
  44. {dosync-0.6.2 → dosync-0.7.0}/dosync/templates/adapter-draft-prompt.md +8 -5
  45. {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/PKG-INFO +177 -13
  46. {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/SOURCES.txt +50 -1
  47. {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/requires.txt +2 -2
  48. {dosync-0.6.2 → dosync-0.7.0}/pyproject.toml +23 -4
  49. {dosync-0.6.2 → dosync-0.7.0}/tests/test_adapter_drafting.py +29 -0
  50. {dosync-0.6.2 → dosync-0.7.0}/tests/test_adapters.py +8 -4
  51. dosync-0.7.0/tests/test_adapters_speak_the_protocol.py +102 -0
  52. {dosync-0.6.2 → dosync-0.7.0}/tests/test_audit_archive.py +49 -0
  53. dosync-0.7.0/tests/test_audit_chain_ordering.py +476 -0
  54. dosync-0.7.0/tests/test_audit_concurrency.py +82 -0
  55. dosync-0.7.0/tests/test_audit_endpoint_limit.py +40 -0
  56. dosync-0.7.0/tests/test_audit_hashless.py +40 -0
  57. dosync-0.7.0/tests/test_certification_guide.py +44 -0
  58. {dosync-0.6.2 → dosync-0.7.0}/tests/test_certification_honesty.py +50 -7
  59. dosync-0.7.0/tests/test_check_helpers_can_fail.py +31 -0
  60. dosync-0.7.0/tests/test_collaborators_read_the_live_hub.py +51 -0
  61. dosync-0.7.0/tests/test_composite_executor.py +31 -0
  62. {dosync-0.6.2 → dosync-0.7.0}/tests/test_composite_operations.py +6 -2
  63. {dosync-0.6.2 → dosync-0.7.0}/tests/test_composite_orchestration.py +9 -5
  64. {dosync-0.6.2 → dosync-0.7.0}/tests/test_composition_kind_db.py +6 -2
  65. {dosync-0.6.2 → dosync-0.7.0}/tests/test_composition_kind_endpoint.py +6 -2
  66. {dosync-0.6.2 → dosync-0.7.0}/tests/test_composition_routing.py +9 -5
  67. dosync-0.7.0/tests/test_dashboard_uses_live_endpoints.py +34 -0
  68. dosync-0.7.0/tests/test_db_write_serialization.py +72 -0
  69. {dosync-0.6.2 → dosync-0.7.0}/tests/test_declarative_adapters.py +16 -8
  70. dosync-0.7.0/tests/test_declarative_location.py +108 -0
  71. {dosync-0.6.2 → dosync-0.7.0}/tests/test_deployment_env_contract.py +2 -2
  72. dosync-0.7.0/tests/test_device_location.py +245 -0
  73. {dosync-0.6.2 → dosync-0.7.0}/tests/test_direct_action_governance.py +15 -6
  74. dosync-0.7.0/tests/test_direct_action_instrumented.py +86 -0
  75. {dosync-0.6.2 → dosync-0.7.0}/tests/test_documentation_is_navigable.py +64 -0
  76. {dosync-0.6.2 → dosync-0.7.0}/tests/test_explain_consistency.py +5 -1
  77. {dosync-0.6.2 → dosync-0.7.0}/tests/test_explain_resolve_parity.py +54 -24
  78. dosync-0.7.0/tests/test_extraction_preserves_behaviour.py +635 -0
  79. {dosync-0.6.2 → dosync-0.7.0}/tests/test_geo.py +6 -2
  80. dosync-0.7.0/tests/test_ha_read_sensors.py +75 -0
  81. dosync-0.7.0/tests/test_health_recorded_once.py +82 -0
  82. dosync-0.7.0/tests/test_intent_rejections_visible.py +81 -0
  83. dosync-0.7.0/tests/test_location_restricts.py +165 -0
  84. {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_channels.py +6 -2
  85. {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_return_home.py +6 -2
  86. {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_single_reader.py +6 -2
  87. {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_telemetry_closure.py +6 -2
  88. dosync-0.7.0/tests/test_mcp_dynamic_intents.py +267 -0
  89. dosync-0.7.0/tests/test_mcp_http_transport.py +60 -0
  90. dosync-0.7.0/tests/test_mcp_scenarios_are_live.py +78 -0
  91. dosync-0.7.0/tests/test_mcp_speaks_english.py +95 -0
  92. dosync-0.7.0/tests/test_mcp_status.py +27 -0
  93. dosync-0.7.0/tests/test_measurement_tools_are_honest.py +286 -0
  94. {dosync-0.6.2 → dosync-0.7.0}/tests/test_no_operator_data.py +253 -2
  95. dosync-0.7.0/tests/test_no_undefined_names.py +41 -0
  96. dosync-0.7.0/tests/test_occupancy.py +70 -0
  97. dosync-0.7.0/tests/test_openapi_contract.py +62 -0
  98. {dosync-0.6.2 → dosync-0.7.0}/tests/test_operation_guards.py +6 -2
  99. {dosync-0.6.2 → dosync-0.7.0}/tests/test_operation_supervisor.py +6 -2
  100. dosync-0.7.0/tests/test_plan_executor.py +32 -0
  101. {dosync-0.6.2 → dosync-0.7.0}/tests/test_policies.py +7 -4
  102. dosync-0.7.0/tests/test_protocol_version.py +66 -0
  103. dosync-0.7.0/tests/test_registry.py +69 -0
  104. {dosync-0.6.2 → dosync-0.7.0}/tests/test_resolver_scoring.py +145 -1
  105. {dosync-0.6.2 → dosync-0.7.0}/tests/test_route_composer.py +6 -2
  106. dosync-0.7.0/tests/test_schemas_match_the_hub.py +163 -0
  107. {dosync-0.6.2 → dosync-0.7.0}/tests/test_server.py +27 -7
  108. dosync-0.7.0/tests/test_server_enforces_auth.py +27 -0
  109. dosync-0.7.0/tests/test_state_refresh.py +50 -0
  110. dosync-0.7.0/tests/test_tag_vocabulary.py +72 -0
  111. dosync-0.7.0/tests/test_telemetry.py +47 -0
  112. dosync-0.7.0/tests/test_undefined_name_regressions.py +53 -0
  113. {dosync-0.6.2 → dosync-0.7.0}/tests/test_universal_intent_contract.py +17 -4
  114. dosync-0.7.0/tests/test_unknown_location_refused.py +57 -0
  115. {dosync-0.6.2 → dosync-0.7.0}/tests/test_wiring_audit.py +4 -42
  116. dosync-0.7.0/tests/test_wiz_closes_every_bulb.py +76 -0
  117. dosync-0.6.2/dosync/hub.py +0 -3711
  118. dosync-0.6.2/tests/test_mcp_dynamic_intents.py +0 -131
  119. {dosync-0.6.2 → dosync-0.7.0}/LICENSE +0 -0
  120. {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/ble.py +0 -0
  121. {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/declarative.py +0 -0
  122. {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/mavlink.py +0 -0
  123. {dosync-0.6.2 → dosync-0.7.0}/dosync/auth_fastapi.py +0 -0
  124. {dosync-0.6.2 → dosync-0.7.0}/dosync/cert_signing.py +0 -0
  125. {dosync-0.6.2 → dosync-0.7.0}/dosync/cli.py +0 -0
  126. {dosync-0.6.2 → dosync-0.7.0}/dosync/composite_operations.py +0 -0
  127. {dosync-0.6.2 → dosync-0.7.0}/dosync/config_reference.py +0 -0
  128. {dosync-0.6.2 → dosync-0.7.0}/dosync/device_arbiter.py +0 -0
  129. {dosync-0.6.2 → dosync-0.7.0}/dosync/discoverers.py +0 -0
  130. {dosync-0.6.2 → dosync-0.7.0}/dosync/discoverers_mdns.py +0 -0
  131. {dosync-0.6.2 → dosync-0.7.0}/dosync/discoverers_ssdp.py +0 -0
  132. {dosync-0.6.2 → dosync-0.7.0}/dosync/ed25519_pure.py +0 -0
  133. {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/__init__.py +0 -0
  134. {dosync-0.6.2 → dosync-0.7.0}/dosync/executor.py +0 -0
  135. {dosync-0.6.2 → dosync-0.7.0}/dosync/geo.py +0 -0
  136. {dosync-0.6.2 → dosync-0.7.0}/dosync/hub_monitor.py +0 -0
  137. {dosync-0.6.2 → dosync-0.7.0}/dosync/lightweight.py +0 -0
  138. {dosync-0.6.2 → dosync-0.7.0}/dosync/operation_guards.py +0 -0
  139. {dosync-0.6.2 → dosync-0.7.0}/dosync/operation_supervisor.py +0 -0
  140. {dosync-0.6.2 → dosync-0.7.0}/dosync/operations.py +0 -0
  141. {dosync-0.6.2 → dosync-0.7.0}/dosync/paths.py +0 -0
  142. {dosync-0.6.2 → dosync-0.7.0}/dosync/plugins.py +0 -0
  143. {dosync-0.6.2 → dosync-0.7.0}/dosync/policy_config.py +0 -0
  144. {dosync-0.6.2 → dosync-0.7.0}/dosync/py.typed +0 -0
  145. {dosync-0.6.2 → dosync-0.7.0}/dosync/reconciler.py +0 -0
  146. {dosync-0.6.2 → dosync-0.7.0}/dosync/route_composer.py +0 -0
  147. {dosync-0.6.2 → dosync-0.7.0}/dosync/spec_coverage.py +0 -0
  148. {dosync-0.6.2 → dosync-0.7.0}/dosync/validation.py +0 -0
  149. {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/dependency_links.txt +0 -0
  150. {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/entry_points.txt +0 -0
  151. {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/top_level.txt +0 -0
  152. {dosync-0.6.2 → dosync-0.7.0}/setup.cfg +0 -0
  153. {dosync-0.6.2 → dosync-0.7.0}/tests/test_audit_backup.py +0 -0
  154. {dosync-0.6.2 → dosync-0.7.0}/tests/test_audit_chain_integrity.py +0 -0
  155. {dosync-0.6.2 → dosync-0.7.0}/tests/test_audit_provenance.py +0 -0
  156. {dosync-0.6.2 → dosync-0.7.0}/tests/test_auth.py +0 -0
  157. {dosync-0.6.2 → dosync-0.7.0}/tests/test_ble_adapter.py +0 -0
  158. {dosync-0.6.2 → dosync-0.7.0}/tests/test_claim_state_machine.py +0 -0
  159. {dosync-0.6.2 → dosync-0.7.0}/tests/test_dashboard_tells_the_truth.py +0 -0
  160. {dosync-0.6.2 → dosync-0.7.0}/tests/test_db.py +0 -0
  161. {dosync-0.6.2 → dosync-0.7.0}/tests/test_declarative_quarantine.py +0 -0
  162. {dosync-0.6.2 → dosync-0.7.0}/tests/test_deployment_layout.py +0 -0
  163. {dosync-0.6.2 → dosync-0.7.0}/tests/test_device_health.py +0 -0
  164. {dosync-0.6.2 → dosync-0.7.0}/tests/test_device_heartbeat.py +0 -0
  165. {dosync-0.6.2 → dosync-0.7.0}/tests/test_device_identity.py +0 -0
  166. {dosync-0.6.2 → dosync-0.7.0}/tests/test_discovery_adoption.py +0 -0
  167. {dosync-0.6.2 → dosync-0.7.0}/tests/test_drone_policies.py +0 -0
  168. {dosync-0.6.2 → dosync-0.7.0}/tests/test_ed25519_pure.py +0 -0
  169. {dosync-0.6.2 → dosync-0.7.0}/tests/test_emergency_preemption.py +0 -0
  170. {dosync-0.6.2 → dosync-0.7.0}/tests/test_env_loading_is_explicit.py +0 -0
  171. {dosync-0.6.2 → dosync-0.7.0}/tests/test_evaluation_metrics.py +0 -0
  172. {dosync-0.6.2 → dosync-0.7.0}/tests/test_event_loop_migration.py +0 -0
  173. {dosync-0.6.2 → dosync-0.7.0}/tests/test_ha_bridge_hygiene.py +0 -0
  174. {dosync-0.6.2 → dosync-0.7.0}/tests/test_hub_monitor.py +0 -0
  175. {dosync-0.6.2 → dosync-0.7.0}/tests/test_idempotency.py +0 -0
  176. {dosync-0.6.2 → dosync-0.7.0}/tests/test_independent_observation.py +0 -0
  177. {dosync-0.6.2 → dosync-0.7.0}/tests/test_integration_suite.py +0 -0
  178. {dosync-0.6.2 → dosync-0.7.0}/tests/test_lightweight_heartbeat.py +0 -0
  179. {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_adapter.py +0 -0
  180. {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_listener.py +0 -0
  181. {dosync-0.6.2 → dosync-0.7.0}/tests/test_mcp_partial_progress.py +0 -0
  182. {dosync-0.6.2 → dosync-0.7.0}/tests/test_metrics.py +0 -0
  183. {dosync-0.6.2 → dosync-0.7.0}/tests/test_models.py +0 -0
  184. {dosync-0.6.2 → dosync-0.7.0}/tests/test_multihub_endpoints.py +0 -0
  185. {dosync-0.6.2 → dosync-0.7.0}/tests/test_notification_templates.py +0 -0
  186. {dosync-0.6.2 → dosync-0.7.0}/tests/test_operations.py +0 -0
  187. {dosync-0.6.2 → dosync-0.7.0}/tests/test_operations_endpoints.py +0 -0
  188. {dosync-0.6.2 → dosync-0.7.0}/tests/test_operations_persistence.py +0 -0
  189. {dosync-0.6.2 → dosync-0.7.0}/tests/test_operations_wiring.py +0 -0
  190. {dosync-0.6.2 → dosync-0.7.0}/tests/test_panel_polish_2026_07_21.py +0 -0
  191. {dosync-0.6.2 → dosync-0.7.0}/tests/test_policy_config.py +0 -0
  192. {dosync-0.6.2 → dosync-0.7.0}/tests/test_public_claims.py +0 -0
  193. {dosync-0.6.2 → dosync-0.7.0}/tests/test_pushed_verification.py +0 -0
  194. {dosync-0.6.2 → dosync-0.7.0}/tests/test_quickstart_is_runnable.py +0 -0
  195. {dosync-0.6.2 → dosync-0.7.0}/tests/test_reachability_cause.py +0 -0
  196. {dosync-0.6.2 → dosync-0.7.0}/tests/test_recall_benchmark_postpolicy.py +0 -0
  197. {dosync-0.6.2 → dosync-0.7.0}/tests/test_reconciler.py +0 -0
  198. {dosync-0.6.2 → dosync-0.7.0}/tests/test_resolution_wiring.py +0 -0
  199. {dosync-0.6.2 → dosync-0.7.0}/tests/test_resolver_semantics.py +0 -0
  200. {dosync-0.6.2 → dosync-0.7.0}/tests/test_sensor_kind.py +0 -0
  201. {dosync-0.6.2 → dosync-0.7.0}/tests/test_simulation_is_declared.py +0 -0
  202. {dosync-0.6.2 → dosync-0.7.0}/tests/test_telemetry_bridge.py +0 -0
  203. {dosync-0.6.2 → dosync-0.7.0}/tests/test_third_party_adapters.py +0 -0
  204. {dosync-0.6.2 → dosync-0.7.0}/tests/test_transport_discoverers.py +0 -0
  205. {dosync-0.6.2 → dosync-0.7.0}/tests/test_validation.py +0 -0
  206. {dosync-0.6.2 → dosync-0.7.0}/tests/test_validation_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dosync
3
- Version: 0.6.2
3
+ Version: 0.7.0
4
4
  Summary: The semantic layer between AI agents and physical devices
5
5
  Author-email: Rodrigo Giuliani <rgiuliani@dosync.dev>
6
6
  License-Expression: Apache-2.0
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://dosync.dev
8
8
  Project-URL: Repository, https://github.com/giulianireg-spec/dosync-protocol
9
9
  Project-URL: Specification, https://github.com/giulianireg-spec/dosync-protocol/blob/main/spec/DoSync-SPEC-v0.1.md
10
10
  Project-URL: Issues, https://github.com/giulianireg-spec/dosync-protocol/issues
11
- Keywords: iot,ai-agents,protocol,semantic,orchestration,audit,governance,mcp,home-automation,robotics
11
+ Keywords: iot,ai-agents,protocol,semantic,orchestration,audit,governance,mcp,robotics,cyber-physical
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Intended Audience :: System Administrators
@@ -17,7 +17,11 @@ Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
20
22
  Classifier: Topic :: Home Automation
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Classifier: Topic :: System :: Hardware
21
25
  Classifier: Topic :: System :: Distributed Computing
22
26
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
23
27
  Requires-Python: >=3.10
@@ -45,7 +49,7 @@ Requires-Dist: twilio>=8.0.0; extra == "sms"
45
49
  Provides-Extra: mavlink
46
50
  Requires-Dist: pymavlink>=2.4.0; extra == "mavlink"
47
51
  Provides-Extra: mcp
48
- Requires-Dist: mcp>=1.0.0; extra == "mcp"
52
+ Requires-Dist: mcp<2.0,>=1.27.0; extra == "mcp"
49
53
  Provides-Extra: all
50
54
  Requires-Dist: pywizlight>=0.5.0; extra == "all"
51
55
  Requires-Dist: paho-mqtt>=2.0.0; extra == "all"
@@ -53,7 +57,7 @@ Requires-Dist: aiohttp>=3.9.0; extra == "all"
53
57
  Requires-Dist: bleak>=0.21; extra == "all"
54
58
  Requires-Dist: twilio>=8.0.0; extra == "all"
55
59
  Requires-Dist: pymavlink>=2.4.0; extra == "all"
56
- Requires-Dist: mcp>=1.0.0; extra == "all"
60
+ Requires-Dist: mcp<2.0,>=1.27.0; extra == "all"
57
61
  Provides-Extra: dev
58
62
  Requires-Dist: pytest>=7.4; extra == "dev"
59
63
  Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
@@ -65,7 +69,7 @@ Dynamic: license-file
65
69
  > Governance and accountability for AI that acts on physical devices.
66
70
 
67
71
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
68
- [![Protocol](https://img.shields.io/badge/protocol-v0.4-green.svg)](spec/DoSync-SPEC-v0.1.md)
72
+ [![Protocol](https://img.shields.io/badge/protocol-v0.5-green.svg)](spec/DoSync-SPEC-v0.1.md)
69
73
  [![PyPI](https://img.shields.io/pypi/v/dosync.svg)](https://pypi.org/project/dosync/)
70
74
  [![Python](https://img.shields.io/pypi/pyversions/dosync.svg)](https://pypi.org/project/dosync/)
71
75
  [![CI](https://github.com/giulianireg-spec/dosync-protocol/actions/workflows/ci.yml/badge.svg)](https://github.com/giulianireg-spec/dosync-protocol/actions/workflows/ci.yml)
@@ -209,15 +213,14 @@ DoSync earns its place in specific situations — and honestly gets in the way i
209
213
  ## How it works
210
214
 
211
215
  ```
212
- User / AI says: "there is an emergency at home"
216
+ User / AI says: "there is a fire in building B"
213
217
  │
214
218
  DoSync Hub
215
219
  │
216
220
  ┌───────────────┼───────────────┐
217
221
  ▼ ▼ ▼
218
- 💡 All lights 📱 SMS sent 🚨 Alarm
219
- at maximum to family activated
220
- (10 WiZ bulbs) immediately
222
+ 💡 Every light 📱 On-call staff 🚨 Alarm
223
+ at maximum notified activated
221
224
  │ │ │
222
225
  └───────────────┴───────────────┘
223
226
  │
@@ -788,6 +791,167 @@ test, so it is named rather than recommended.
788
791
 
789
792
  ---
790
793
 
794
+ ## Connecting an agent
795
+
796
+ This page has said "MCP" a dozen times — a badge at the top, a section on why
797
+ MCP is the channel rather than the rival, a drone that flew a mission through
798
+ it — and never once explained how to connect an agent. This is that.
799
+
800
+ DoSync ships an MCP server. It is not a separate product: it exposes the hub's
801
+ intents and devices as tools, and every call it makes lands in the same policy
802
+ engine and the same audit chain as a call from `curl`. An agent that reaches a
803
+ device through it is governed exactly like anything else.
804
+
805
+ ### What DoSync provides
806
+
807
+ The server is a module rather than a command, and it needs the MCP SDK, which
808
+ the hub does not install:
809
+
810
+ ```bash
811
+ pip install "mcp>=1.27.0,<2.0" # or: pipx inject dosync "mcp>=1.27.0,<2.0"
812
+ python -m dosync.mcp_server
813
+ ```
814
+
815
+ Both bounds are measured. The upper one: the 2.x SDK changed how tools are
816
+ registered and this server is written against 1.x; without the cap, a fresh
817
+ install gets 2.x and the server exits at import. It says so plainly if that
818
+ happens. The lower one: the HTTP transport needs 1.27.0, and 1.27.0 in turn needs
819
+ pydantic 2.11 and jsonschema 4.20 — so installing the MCP server raises those
820
+ two floors above the hub's own. CI runs the whole suite on exactly those minimums.
821
+
822
+ It speaks over stdin/stdout, so a successful start prints nothing and waits.
823
+ Typing into that terminal will produce JSON parse errors — that is the protocol
824
+ rejecting your keystrokes, not a fault.
825
+
826
+ **When the agent is not on this machine**, stdio cannot help: it assumes the
827
+ client can start the server as a subprocess, which means they share a filesystem.
828
+ A hub in a container, on a Pi across the room, or under a supervisor is not that.
829
+ Serve it over the network instead:
830
+
831
+ ```bash
832
+ DOSYNC_MCP_TRANSPORT=http DOSYNC_TOKEN=<token> python -m dosync.mcp_server
833
+ ```
834
+
835
+ It listens on `127.0.0.1:47210` by default; `DOSYNC_MCP_HOST` and
836
+ `DOSYNC_MCP_PORT` change that. Clients connect to `/mcp`, and `/health` answers
837
+ unauthenticated so a supervisor can check the process without holding a
838
+ credential.
839
+
840
+ **It will not start over HTTP without `DOSYNC_TOKEN`, deliberately.** Over stdio
841
+ the operating system is the boundary — whoever can start the process is already
842
+ on the machine. A port has none, and on a host network it is reachable by every
843
+ device on the LAN. The tools behind it open locks and stop machines.
844
+
845
+ **The token travels in clear text unless you put TLS in front of it.** That is
846
+ true of the hub's own API too and is documented there, but it matters more here:
847
+ this port executes tools rather than answering questions. On a trusted LAN it is
848
+ a considered trade-off; across anything else, terminate TLS in front of it. Idle
849
+ sessions are dropped after 15 minutes by default (`DOSYNC_MCP_SESSION_TIMEOUT`).
850
+
851
+ The transport changes nothing else. Both serve the same tools from the same
852
+ server object, and an agent reaching a device this way goes through the hub's
853
+ REST API exactly as `curl` does — same policy engine, same audit chain.
854
+
855
+ It reads three environment variables:
856
+
857
+ | Variable | Meaning |
858
+ |---|---|
859
+ | `DOSYNC_HUB_URL` | Where the hub is. Default `http://localhost:47200` |
860
+ | `DOSYNC_TOKEN` | The hub's bearer token |
861
+ | `DOSYNC_CA_CERT` | CA certificate, when the hub runs over TLS |
862
+
863
+ That is the whole of DoSync's side. Everything below is about getting a client
864
+ to launch that command — which is the client's business, not the protocol's.
865
+
866
+ ### What the client provides
867
+
868
+ MCP clients are configured with a JSON file naming a command to run and the
869
+ environment to run it in. The shape is standard:
870
+
871
+ ```json
872
+ {
873
+ "mcpServers": {
874
+ "dosync": {
875
+ "command": "/absolute/path/to/python",
876
+ "args": ["-m", "dosync.mcp_server"],
877
+ "env": {
878
+ "DOSYNC_HUB_URL": "http://localhost:47200",
879
+ "DOSYNC_TOKEN": "your-token-here"
880
+ }
881
+ }
882
+ }
883
+ }
884
+ ```
885
+
886
+ **That file holds a credential.** It grants whatever the token grants. This
887
+ project shipped a live token in a public repository for three months, so the
888
+ warning is not rhetorical: keep it out of version control and off shared
889
+ drives.
890
+
891
+ Use an absolute path to the interpreter that has DoSync installed. A client
892
+ launches the command with its own environment, not yours, so `python` may not
893
+ resolve to what you expect. With `pipx`, the interpreter is inside its venv —
894
+ on Linux and macOS `~/.local/share/pipx/venvs/dosync/bin/python`, on Windows
895
+ `%LOCALAPPDATA%\pipx\pipx\venvs\dosync\Scripts\python.exe`.
896
+
897
+ Where the file goes and how it reloads is documented by your client, not here.
898
+
899
+ ### Two things Windows does to this file
900
+
901
+ Neither belongs to any particular client, and both fail without a useful error,
902
+ so they are worth knowing before you spend an afternoon on them.
903
+
904
+ **A packaged application does not see the path you wrote to.** Applications
905
+ installed from the Microsoft Store run with a virtualised filesystem: when they
906
+ read `%APPDATA%\Something`, Windows redirects them to
907
+ `%LOCALAPPDATA%\Packages\<package-id>\LocalCache\Roaming\Something`. A file
908
+ written to the literal path is invisible to the application, and nothing
909
+ reports this — the client simply behaves as though no configuration exists. If
910
+ your client offers a button that opens its configuration folder, use it and
911
+ write there.
912
+
913
+ **PowerShell 5.1 writes a byte-order mark.** `Set-Content -Encoding UTF8` puts
914
+ three bytes at the start of the file that JSON parsers reject, and the error
915
+ names an invisible character: `Unexpected token ''`. Write it without one:
916
+
917
+ ```powershell
918
+ [System.IO.File]::WriteAllText($path, $json, [System.Text.UTF8Encoding]::new($false))
919
+ ```
920
+
921
+ Check the first byte is `123` (`{`) and not `239`:
922
+
923
+ ```powershell
924
+ [System.IO.File]::ReadAllBytes($path)[0]
925
+ ```
926
+
927
+ ### What it looks like when it works
928
+
929
+ *Verified 29 August 2026: Windows 11 ARM64, Python 3.14, DoSync 0.6.2 installed
930
+ with `pipx`, MCP SDK 1.29.1, Claude Desktop from the Microsoft Store. Named
931
+ because a result you cannot reproduce is not evidence — not as a
932
+ recommendation. Any client that speaks MCP works; the details above are what
933
+ that combination required, and clients change theirs without notice.*
934
+
935
+ Asked in plain language which devices were registered, the agent listed six —
936
+ three WiZ bulbs with their actions, and three adopted devices with none. What
937
+ it said about the second group is the part worth reading:
938
+
939
+ > Estos tres últimos están registrados pero sin tags ni acciones definidas — el
940
+ > hub sabe que existen pero no puede actuar sobre ellos todavía.
941
+
942
+ <sub>Quoted verbatim, in the language the model answered in. Translating it
943
+ would misrepresent what was said, which is the point of quoting it.
944
+ ("These last three are registered but with no tags or actions defined — the hub
945
+ knows they exist but cannot act on them yet.")</sub>
946
+
947
+ It did not invent capabilities for devices that have none. That is not the
948
+ model being careful: the hub reports an undeclared device as undeclared, so
949
+ there was nothing to invent from. Then it offered to describe one — the drafting
950
+ flow in [`/v1/devices/{id}/describe`](#keeping-the-hub-running), reached from
951
+ the other end.
952
+
953
+ ---
954
+
791
955
  ## What's built today
792
956
 
793
957
  | Component | Status |
@@ -797,11 +961,11 @@ test, so it is named rather than recommended.
797
961
  | Web dashboard | ✅ |
798
962
  | API key authentication + SHA-256 audit log | ✅ |
799
963
  | Capability-based resolver | ✅ |
800
- | Certification CLI — Standard 33/33 · Emergency 44/44 (signed reports) | ✅ |
964
+ | Certification CLI — Basic 10 · Standard 33 · Emergency 44 · Conformance 65, each run in CI against a live hub (signed reports) | ✅ |
801
965
  | Philips WiZ adapter (UDP local) | ✅ |
802
966
  | Home Assistant bridge (10 domains) | ✅ |
803
967
  | Native MCP server (Claude, ChatGPT, any LLM) | ✅ |
804
- | GPIO adapter — Raspberry Pi 5 (PIR + DHT22) | ✅ |
968
+ | GPIO sensors on a Raspberry Pi 5 (PIR + DHT22), via deployment code over the API | ✅ reference deployment (not shipped) |
805
969
  | SMS notifications via Twilio | ✅ code (requires an active Twilio plan) |
806
970
  | MQTT transport adapter (Mosquitto) | ✅ |
807
971
  | Shelly adapter (HTTP local, Gen1 + Gen2) | ✅ code, not hardware-tested |
@@ -850,8 +1014,8 @@ python3 certify.py --host <hub-ip> --port 47200 --tier standard
850
1014
 
851
1015
  | Language | Location | Author | Certification |
852
1016
  |---|---|---|---|
853
- | Python (reference) | `server.py` | this project | Standard 33/33 · Emergency 44/44 ✅ |
854
- | Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | Standard 33/33, against the v0.3 suite — re-validation against the current 56-test suite pending |
1017
+ | Python (reference) | `server.py` | this project | Conformance 65/65 ✅ (every tier, in CI) |
1018
+ | Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | Standard 33/33, against the v0.3 suite — re-validation against the current 65-check suite pending |
855
1019
 
856
1020
  The Node.js implementation is a **companion** port that validates the protocol
857
1021
  is implementable in a second language against the same certification suite —
@@ -3,7 +3,7 @@
3
3
  > Governance and accountability for AI that acts on physical devices.
4
4
 
5
5
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
6
- [![Protocol](https://img.shields.io/badge/protocol-v0.4-green.svg)](spec/DoSync-SPEC-v0.1.md)
6
+ [![Protocol](https://img.shields.io/badge/protocol-v0.5-green.svg)](spec/DoSync-SPEC-v0.1.md)
7
7
  [![PyPI](https://img.shields.io/pypi/v/dosync.svg)](https://pypi.org/project/dosync/)
8
8
  [![Python](https://img.shields.io/pypi/pyversions/dosync.svg)](https://pypi.org/project/dosync/)
9
9
  [![CI](https://github.com/giulianireg-spec/dosync-protocol/actions/workflows/ci.yml/badge.svg)](https://github.com/giulianireg-spec/dosync-protocol/actions/workflows/ci.yml)
@@ -147,15 +147,14 @@ DoSync earns its place in specific situations — and honestly gets in the way i
147
147
  ## How it works
148
148
 
149
149
  ```
150
- User / AI says: "there is an emergency at home"
150
+ User / AI says: "there is a fire in building B"
151
151
  │
152
152
  DoSync Hub
153
153
  │
154
154
  ┌───────────────┼───────────────┐
155
155
  ▼ ▼ ▼
156
- 💡 All lights 📱 SMS sent 🚨 Alarm
157
- at maximum to family activated
158
- (10 WiZ bulbs) immediately
156
+ 💡 Every light 📱 On-call staff 🚨 Alarm
157
+ at maximum notified activated
159
158
  │ │ │
160
159
  └───────────────┴───────────────┘
161
160
  │
@@ -726,6 +725,167 @@ test, so it is named rather than recommended.
726
725
 
727
726
  ---
728
727
 
728
+ ## Connecting an agent
729
+
730
+ This page has said "MCP" a dozen times — a badge at the top, a section on why
731
+ MCP is the channel rather than the rival, a drone that flew a mission through
732
+ it — and never once explained how to connect an agent. This is that.
733
+
734
+ DoSync ships an MCP server. It is not a separate product: it exposes the hub's
735
+ intents and devices as tools, and every call it makes lands in the same policy
736
+ engine and the same audit chain as a call from `curl`. An agent that reaches a
737
+ device through it is governed exactly like anything else.
738
+
739
+ ### What DoSync provides
740
+
741
+ The server is a module rather than a command, and it needs the MCP SDK, which
742
+ the hub does not install:
743
+
744
+ ```bash
745
+ pip install "mcp>=1.27.0,<2.0" # or: pipx inject dosync "mcp>=1.27.0,<2.0"
746
+ python -m dosync.mcp_server
747
+ ```
748
+
749
+ Both bounds are measured. The upper one: the 2.x SDK changed how tools are
750
+ registered and this server is written against 1.x; without the cap, a fresh
751
+ install gets 2.x and the server exits at import. It says so plainly if that
752
+ happens. The lower one: the HTTP transport needs 1.27.0, and 1.27.0 in turn needs
753
+ pydantic 2.11 and jsonschema 4.20 — so installing the MCP server raises those
754
+ two floors above the hub's own. CI runs the whole suite on exactly those minimums.
755
+
756
+ It speaks over stdin/stdout, so a successful start prints nothing and waits.
757
+ Typing into that terminal will produce JSON parse errors — that is the protocol
758
+ rejecting your keystrokes, not a fault.
759
+
760
+ **When the agent is not on this machine**, stdio cannot help: it assumes the
761
+ client can start the server as a subprocess, which means they share a filesystem.
762
+ A hub in a container, on a Pi across the room, or under a supervisor is not that.
763
+ Serve it over the network instead:
764
+
765
+ ```bash
766
+ DOSYNC_MCP_TRANSPORT=http DOSYNC_TOKEN=<token> python -m dosync.mcp_server
767
+ ```
768
+
769
+ It listens on `127.0.0.1:47210` by default; `DOSYNC_MCP_HOST` and
770
+ `DOSYNC_MCP_PORT` change that. Clients connect to `/mcp`, and `/health` answers
771
+ unauthenticated so a supervisor can check the process without holding a
772
+ credential.
773
+
774
+ **It will not start over HTTP without `DOSYNC_TOKEN`, deliberately.** Over stdio
775
+ the operating system is the boundary — whoever can start the process is already
776
+ on the machine. A port has none, and on a host network it is reachable by every
777
+ device on the LAN. The tools behind it open locks and stop machines.
778
+
779
+ **The token travels in clear text unless you put TLS in front of it.** That is
780
+ true of the hub's own API too and is documented there, but it matters more here:
781
+ this port executes tools rather than answering questions. On a trusted LAN it is
782
+ a considered trade-off; across anything else, terminate TLS in front of it. Idle
783
+ sessions are dropped after 15 minutes by default (`DOSYNC_MCP_SESSION_TIMEOUT`).
784
+
785
+ The transport changes nothing else. Both serve the same tools from the same
786
+ server object, and an agent reaching a device this way goes through the hub's
787
+ REST API exactly as `curl` does — same policy engine, same audit chain.
788
+
789
+ It reads three environment variables:
790
+
791
+ | Variable | Meaning |
792
+ |---|---|
793
+ | `DOSYNC_HUB_URL` | Where the hub is. Default `http://localhost:47200` |
794
+ | `DOSYNC_TOKEN` | The hub's bearer token |
795
+ | `DOSYNC_CA_CERT` | CA certificate, when the hub runs over TLS |
796
+
797
+ That is the whole of DoSync's side. Everything below is about getting a client
798
+ to launch that command — which is the client's business, not the protocol's.
799
+
800
+ ### What the client provides
801
+
802
+ MCP clients are configured with a JSON file naming a command to run and the
803
+ environment to run it in. The shape is standard:
804
+
805
+ ```json
806
+ {
807
+ "mcpServers": {
808
+ "dosync": {
809
+ "command": "/absolute/path/to/python",
810
+ "args": ["-m", "dosync.mcp_server"],
811
+ "env": {
812
+ "DOSYNC_HUB_URL": "http://localhost:47200",
813
+ "DOSYNC_TOKEN": "your-token-here"
814
+ }
815
+ }
816
+ }
817
+ }
818
+ ```
819
+
820
+ **That file holds a credential.** It grants whatever the token grants. This
821
+ project shipped a live token in a public repository for three months, so the
822
+ warning is not rhetorical: keep it out of version control and off shared
823
+ drives.
824
+
825
+ Use an absolute path to the interpreter that has DoSync installed. A client
826
+ launches the command with its own environment, not yours, so `python` may not
827
+ resolve to what you expect. With `pipx`, the interpreter is inside its venv —
828
+ on Linux and macOS `~/.local/share/pipx/venvs/dosync/bin/python`, on Windows
829
+ `%LOCALAPPDATA%\pipx\pipx\venvs\dosync\Scripts\python.exe`.
830
+
831
+ Where the file goes and how it reloads is documented by your client, not here.
832
+
833
+ ### Two things Windows does to this file
834
+
835
+ Neither belongs to any particular client, and both fail without a useful error,
836
+ so they are worth knowing before you spend an afternoon on them.
837
+
838
+ **A packaged application does not see the path you wrote to.** Applications
839
+ installed from the Microsoft Store run with a virtualised filesystem: when they
840
+ read `%APPDATA%\Something`, Windows redirects them to
841
+ `%LOCALAPPDATA%\Packages\<package-id>\LocalCache\Roaming\Something`. A file
842
+ written to the literal path is invisible to the application, and nothing
843
+ reports this — the client simply behaves as though no configuration exists. If
844
+ your client offers a button that opens its configuration folder, use it and
845
+ write there.
846
+
847
+ **PowerShell 5.1 writes a byte-order mark.** `Set-Content -Encoding UTF8` puts
848
+ three bytes at the start of the file that JSON parsers reject, and the error
849
+ names an invisible character: `Unexpected token ''`. Write it without one:
850
+
851
+ ```powershell
852
+ [System.IO.File]::WriteAllText($path, $json, [System.Text.UTF8Encoding]::new($false))
853
+ ```
854
+
855
+ Check the first byte is `123` (`{`) and not `239`:
856
+
857
+ ```powershell
858
+ [System.IO.File]::ReadAllBytes($path)[0]
859
+ ```
860
+
861
+ ### What it looks like when it works
862
+
863
+ *Verified 29 August 2026: Windows 11 ARM64, Python 3.14, DoSync 0.6.2 installed
864
+ with `pipx`, MCP SDK 1.29.1, Claude Desktop from the Microsoft Store. Named
865
+ because a result you cannot reproduce is not evidence — not as a
866
+ recommendation. Any client that speaks MCP works; the details above are what
867
+ that combination required, and clients change theirs without notice.*
868
+
869
+ Asked in plain language which devices were registered, the agent listed six —
870
+ three WiZ bulbs with their actions, and three adopted devices with none. What
871
+ it said about the second group is the part worth reading:
872
+
873
+ > Estos tres últimos están registrados pero sin tags ni acciones definidas — el
874
+ > hub sabe que existen pero no puede actuar sobre ellos todavía.
875
+
876
+ <sub>Quoted verbatim, in the language the model answered in. Translating it
877
+ would misrepresent what was said, which is the point of quoting it.
878
+ ("These last three are registered but with no tags or actions defined — the hub
879
+ knows they exist but cannot act on them yet.")</sub>
880
+
881
+ It did not invent capabilities for devices that have none. That is not the
882
+ model being careful: the hub reports an undeclared device as undeclared, so
883
+ there was nothing to invent from. Then it offered to describe one — the drafting
884
+ flow in [`/v1/devices/{id}/describe`](#keeping-the-hub-running), reached from
885
+ the other end.
886
+
887
+ ---
888
+
729
889
  ## What's built today
730
890
 
731
891
  | Component | Status |
@@ -735,11 +895,11 @@ test, so it is named rather than recommended.
735
895
  | Web dashboard | ✅ |
736
896
  | API key authentication + SHA-256 audit log | ✅ |
737
897
  | Capability-based resolver | ✅ |
738
- | Certification CLI — Standard 33/33 · Emergency 44/44 (signed reports) | ✅ |
898
+ | Certification CLI — Basic 10 · Standard 33 · Emergency 44 · Conformance 65, each run in CI against a live hub (signed reports) | ✅ |
739
899
  | Philips WiZ adapter (UDP local) | ✅ |
740
900
  | Home Assistant bridge (10 domains) | ✅ |
741
901
  | Native MCP server (Claude, ChatGPT, any LLM) | ✅ |
742
- | GPIO adapter — Raspberry Pi 5 (PIR + DHT22) | ✅ |
902
+ | GPIO sensors on a Raspberry Pi 5 (PIR + DHT22), via deployment code over the API | ✅ reference deployment (not shipped) |
743
903
  | SMS notifications via Twilio | ✅ code (requires an active Twilio plan) |
744
904
  | MQTT transport adapter (Mosquitto) | ✅ |
745
905
  | Shelly adapter (HTTP local, Gen1 + Gen2) | ✅ code, not hardware-tested |
@@ -788,8 +948,8 @@ python3 certify.py --host <hub-ip> --port 47200 --tier standard
788
948
 
789
949
  | Language | Location | Author | Certification |
790
950
  |---|---|---|---|
791
- | Python (reference) | `server.py` | this project | Standard 33/33 · Emergency 44/44 ✅ |
792
- | Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | Standard 33/33, against the v0.3 suite — re-validation against the current 56-test suite pending |
951
+ | Python (reference) | `server.py` | this project | Conformance 65/65 ✅ (every tier, in CI) |
952
+ | Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | Standard 33/33, against the v0.3 suite — re-validation against the current 65-check suite pending |
793
953
 
794
954
  The Node.js implementation is a **companion** port that validates the protocol
795
955
  is implementable in a second language against the same certification suite —
@@ -13,5 +13,5 @@ The two numbers move independently on purpose:
13
13
  __version__ this implementation of the hub (semver)
14
14
  __protocol_version__ the wire contract other implementations must match
15
15
  """
16
- __version__ = "0.6.2"
17
- __protocol_version__ = "0.4"
16
+ __version__ = "0.7.0"
17
+ __protocol_version__ = "0.5"
@@ -79,8 +79,13 @@ def _tag_vocabulary(repo_root: Path) -> str:
79
79
  the universal intent resolves on `lock`. A model left to guess would
80
80
  reproduce that on every device it describes.
81
81
  """
82
- for candidate in (repo_root / "spec" / "TAG-VOCABULARY.md",
83
- Path(__file__).parent.parent / "spec" / "TAG-VOCABULARY.md"):
82
+ # The package's own copy first: it is what a `pip install` has. Until 0.7.0
83
+ # the wheel carried no copy, both paths below were missing in every
84
+ # installed hub, and the prompt told the model the vocabulary was
85
+ # unavailable. tests/test_tag_vocabulary.py keeps the copy identical to
86
+ # spec/TAG-VOCABULARY.md.
87
+ for candidate in (Path(__file__).parent / "spec" / "TAG-VOCABULARY.md",
88
+ repo_root / "spec" / "TAG-VOCABULARY.md"):
84
89
  if not candidate.exists():
85
90
  continue
86
91
  text = candidate.read_text(encoding="utf-8")
@@ -6,7 +6,7 @@ Translation layer between the DoSync protocol and real physical devices.
6
6
  Modelo:
7
7
  DoSync Hub → AdapterExecutor → [WiZAdapter | GPIOAdapter | ShellyAdapter | ...]
8
8
 
9
- Para agregar un nuevo dispositivo:
9
+ To add a new device type:
10
10
  1. Crear adapters/mi_marca.py implementando DoSyncAdapter
11
11
  2. Register the device with adapter="my_brand" in its CapabilityManifest
12
12
  3. The hub treats it like any other device — no core changes needed
@@ -52,14 +52,14 @@ class DoSyncAdapter(ABC):
52
52
  """
53
53
  Base interface for physical device adapters.
54
54
 
55
- Cada adapter traduce acciones DoSync al protocolo nativo
56
- del dispositivo (UDP, HTTP, GPIO, BLE, etc.).
55
+ Each adapter translates DoSync actions into the device's native protocol
56
+ (UDP, HTTP, BLE, etc.).
57
57
 
58
- Para implementar un adapter nuevo:
58
+ To implement a new adapter:
59
59
 
60
60
  class MyBrandAdapter(DoSyncAdapter):
61
61
  async def execute(self, action, urgency):
62
- # traducir action.action + action.params al protocolo del dispositivo
62
+ # translate action.action + action.params into the device's protocol
63
63
  return ActionResult(
64
64
  device_id=action.device_id,
65
65
  action=action.action,
@@ -174,16 +174,16 @@ class DoSyncAdapter(ABC):
174
174
  ...
175
175
 
176
176
 
177
- # ── AdapterExecutor — el ejecutor central ────────────────────────────────────
177
+ # ── AdapterExecutor — the central executor ────────────────────────────────────
178
178
 
179
179
  class AdapterExecutor:
180
180
  """
181
- Ejecutor central que delega acciones al adapter correcto
181
+ Central executor that delegates each action to the right adapter
182
182
  based on the 'adapter' field of the device's CapabilityManifest.
183
183
 
184
184
  A device with no registered adapter falls back to the SimulatedExecutor.
185
185
 
186
- Uso:
186
+ Usage:
187
187
  executor = AdapterExecutor(hub)
188
188
  executor.register(WiZAdapter())
189
189
  executor.register(GPIOAdapter())
@@ -195,9 +195,9 @@ class AdapterExecutor:
195
195
  def __init__(self, hub, fallback_to_simulated: bool = True):
196
196
  """
197
197
  Args:
198
- hub: instancia de DoSyncHub
199
- fallback_to_simulated: si True, dispositivos sin adapter
200
- usan SimulatedExecutor en lugar de fallar
198
+ hub: the DoSyncHub instance
199
+ fallback_to_simulated: if True, devices with no adapter
200
+ use SimulatedExecutor instead of failing
201
201
  """
202
202
  self._hub = hub
203
203
  self._adapters: dict[str, DoSyncAdapter] = {}
@@ -220,7 +220,7 @@ class AdapterExecutor:
220
220
 
221
221
 
222
222
  def registered_adapters(self) -> list[str]:
223
- """Lista de adapters registrados."""
223
+ """Names of the registered adapters."""
224
224
  return list(self._adapters.keys())
225
225
 
226
226
  async def execute(self, action: DeviceAction, urgency: Urgency) -> ActionResult:
@@ -269,8 +269,8 @@ class AdapterExecutor:
269
269
  result = await self._adapters[adapter_name].execute(action, urgency)
270
270
  if result.success:
271
271
  self._update_resolver_state(action)
272
- # Device Health Monitor — record the outcome
273
- self._record_health(action, result)
272
+ # Device health is recorded once, by the wrapper every path
273
+ # goes through (DoSyncHub.instrumented), not here as well.
274
274
  return result
275
275
  except Exception as e:
276
276
  log.error(
@@ -283,7 +283,6 @@ class AdapterExecutor:
283
283
  success=False,
284
284
  error=f"Adapter error: {e}",
285
285
  )
286
- self._record_health(action, err_result)
287
286
  return err_result
288
287
 
289
288
  # Fallback. WARNING, not INFO, and the result says it was simulated:
@@ -307,21 +306,6 @@ class AdapterExecutor:
307
306
  error=f"No adapter registered for '{adapter_name}'",
308
307
  )
309
308
 
310
- def _record_health(self, action: DeviceAction, result) -> None:
311
- """Record the outcome in the Device Health Monitor."""
312
- try:
313
- db = getattr(self._hub, 'db', None)
314
- if db:
315
- db.record_execution(
316
- device_id=action.device_id,
317
- action=action.action,
318
- success=result.success,
319
- error=getattr(result, 'error', None),
320
- )
321
- except Exception as _e:
322
- log.warning('DeviceHealthMonitor: failed to record execution for %s: %s',
323
- action.device_id, _e)
324
-
325
309
  def _update_resolver_state(self, action: DeviceAction) -> None:
326
310
  """Tell the StateAwareResolver the new state after a successful action."""
327
311
  from ..hub import StateAwareResolver