dosync 0.7.0__tar.gz → 0.9.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 (241) hide show
  1. {dosync-0.7.0 → dosync-0.9.0}/PKG-INFO +121 -9
  2. {dosync-0.7.0 → dosync-0.9.0}/README.md +120 -8
  3. {dosync-0.7.0 → dosync-0.9.0}/dosync/__init__.py +1 -1
  4. dosync-0.9.0/dosync/adapters/wot.py +136 -0
  5. {dosync-0.7.0 → dosync-0.9.0}/dosync/auth.py +8 -0
  6. {dosync-0.7.0 → dosync-0.9.0}/dosync/certify.py +172 -10
  7. {dosync-0.7.0 → dosync-0.9.0}/dosync/config_reference.py +5 -1
  8. dosync-0.9.0/dosync/container.py +98 -0
  9. {dosync-0.7.0 → dosync-0.9.0}/dosync/dashboard.html +413 -0
  10. {dosync-0.7.0 → dosync-0.9.0}/dosync/db.py +11 -2
  11. {dosync-0.7.0 → dosync-0.9.0}/dosync/declarative.py +12 -2
  12. {dosync-0.7.0 → dosync-0.9.0}/dosync/hub.py +334 -3
  13. dosync-0.9.0/dosync/mcp_cli.py +167 -0
  14. {dosync-0.7.0 → dosync-0.9.0}/dosync/mcp_server.py +89 -46
  15. {dosync-0.7.0 → dosync-0.9.0}/dosync/models.py +90 -0
  16. {dosync-0.7.0 → dosync-0.9.0}/dosync/policies.py +72 -208
  17. {dosync-0.7.0 → dosync-0.9.0}/dosync/resolvers.py +83 -16
  18. dosync-0.9.0/dosync/samuel/__init__.py +52 -0
  19. dosync-0.9.0/dosync/samuel/confirmation.py +42 -0
  20. dosync-0.9.0/dosync/samuel/intake.py +136 -0
  21. dosync-0.9.0/dosync/samuel/oscillation.py +71 -0
  22. dosync-0.9.0/dosync/samuel/policing.py +178 -0
  23. dosync-0.9.0/dosync/samuel/proposals.py +131 -0
  24. dosync-0.9.0/dosync/samuel_certify.py +364 -0
  25. dosync-0.9.0/dosync/samuel_http.py +298 -0
  26. {dosync-0.7.0 → dosync-0.9.0}/dosync/server.py +614 -35
  27. {dosync-0.7.0 → dosync-0.9.0}/dosync/spec/TAG-VOCABULARY.md +1 -0
  28. dosync-0.9.0/dosync/wot_export.py +193 -0
  29. dosync-0.9.0/dosync/wot_import.py +221 -0
  30. {dosync-0.7.0 → dosync-0.9.0}/dosync.egg-info/PKG-INFO +121 -9
  31. {dosync-0.7.0 → dosync-0.9.0}/dosync.egg-info/SOURCES.txt +38 -1
  32. {dosync-0.7.0 → dosync-0.9.0}/dosync.egg-info/entry_points.txt +3 -0
  33. {dosync-0.7.0 → dosync-0.9.0}/pyproject.toml +3 -0
  34. dosync-0.9.0/tests/test_agent_lab_matches_the_hub.py +280 -0
  35. dosync-0.9.0/tests/test_capability_decides_participation.py +84 -0
  36. dosync-0.9.0/tests/test_configuration_credential.py +65 -0
  37. dosync-0.9.0/tests/test_confirmation_hold.py +161 -0
  38. dosync-0.9.0/tests/test_container_and_addon.py +82 -0
  39. {dosync-0.7.0 → dosync-0.9.0}/tests/test_direct_action_governance.py +11 -1
  40. {dosync-0.7.0 → dosync-0.9.0}/tests/test_direct_action_instrumented.py +11 -0
  41. dosync-0.9.0/tests/test_direct_control_is_not_an_agent_path.py +81 -0
  42. dosync-0.9.0/tests/test_emergency_actions.py +111 -0
  43. dosync-0.9.0/tests/test_evaluation_tools_run.py +44 -0
  44. {dosync-0.7.0 → dosync-0.9.0}/tests/test_explain_consistency.py +6 -1
  45. dosync-0.9.0/tests/test_governance_console.py +188 -0
  46. dosync-0.9.0/tests/test_governance_vocabulary.py +34 -0
  47. dosync-0.9.0/tests/test_governed_direct_mode.py +144 -0
  48. {dosync-0.7.0 → dosync-0.9.0}/tests/test_location_restricts.py +1 -1
  49. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mcp_speaks_english.py +1 -1
  50. {dosync-0.7.0 → dosync-0.9.0}/tests/test_measurement_tools_are_honest.py +13 -2
  51. {dosync-0.7.0 → dosync-0.9.0}/tests/test_no_operator_data.py +3 -0
  52. dosync-0.9.0/tests/test_no_oscillation.py +90 -0
  53. dosync-0.9.0/tests/test_operate_device.py +116 -0
  54. {dosync-0.7.0 → dosync-0.9.0}/tests/test_operations_wiring.py +5 -0
  55. dosync-0.9.0/tests/test_operator_credential_distinct.py +101 -0
  56. dosync-0.9.0/tests/test_opposite_actions_and_emergency_precedence.py +143 -0
  57. {dosync-0.7.0 → dosync-0.9.0}/tests/test_policies.py +21 -18
  58. dosync-0.9.0/tests/test_policy_failure_is_not_a_pass.py +100 -0
  59. {dosync-0.7.0 → dosync-0.9.0}/tests/test_resolution_wiring.py +8 -2
  60. {dosync-0.7.0 → dosync-0.9.0}/tests/test_resolver_semantics.py +9 -5
  61. dosync-0.9.0/tests/test_round4_analysis.py +97 -0
  62. dosync-0.9.0/tests/test_samuel_conformance.py +70 -0
  63. dosync-0.9.0/tests/test_samuel_every_door.py +106 -0
  64. dosync-0.9.0/tests/test_samuel_kernel_boundary.py +65 -0
  65. dosync-0.9.0/tests/test_samuel_matches_the_hub.py +76 -0
  66. {dosync-0.7.0 → dosync-0.9.0}/tests/test_schemas_match_the_hub.py +1 -0
  67. dosync-0.9.0/tests/test_thing_description_export.py +58 -0
  68. {dosync-0.7.0 → dosync-0.9.0}/tests/test_universal_intent_contract.py +9 -4
  69. {dosync-0.7.0 → dosync-0.9.0}/tests/test_validation_integration.py +5 -0
  70. dosync-0.9.0/tests/test_wot_import.py +166 -0
  71. dosync-0.9.0/tests/test_wot_interop.py +175 -0
  72. {dosync-0.7.0 → dosync-0.9.0}/LICENSE +0 -0
  73. {dosync-0.7.0 → dosync-0.9.0}/dosync/adapter_drafting.py +0 -0
  74. {dosync-0.7.0 → dosync-0.9.0}/dosync/adapters/__init__.py +0 -0
  75. {dosync-0.7.0 → dosync-0.9.0}/dosync/adapters/ble.py +0 -0
  76. {dosync-0.7.0 → dosync-0.9.0}/dosync/adapters/declarative.py +0 -0
  77. {dosync-0.7.0 → dosync-0.9.0}/dosync/adapters/homeassistant.py +0 -0
  78. {dosync-0.7.0 → dosync-0.9.0}/dosync/adapters/matter.py +0 -0
  79. {dosync-0.7.0 → dosync-0.9.0}/dosync/adapters/mavlink.py +0 -0
  80. {dosync-0.7.0 → dosync-0.9.0}/dosync/adapters/mqtt.py +0 -0
  81. {dosync-0.7.0 → dosync-0.9.0}/dosync/adapters/notifications.py +0 -0
  82. {dosync-0.7.0 → dosync-0.9.0}/dosync/adapters/shelly.py +0 -0
  83. {dosync-0.7.0 → dosync-0.9.0}/dosync/adapters/wiz.py +0 -0
  84. {dosync-0.7.0 → dosync-0.9.0}/dosync/audit.py +0 -0
  85. {dosync-0.7.0 → dosync-0.9.0}/dosync/audit_backup.py +0 -0
  86. {dosync-0.7.0 → dosync-0.9.0}/dosync/auth_fastapi.py +0 -0
  87. {dosync-0.7.0 → dosync-0.9.0}/dosync/cert_signing.py +0 -0
  88. {dosync-0.7.0 → dosync-0.9.0}/dosync/cli.py +0 -0
  89. {dosync-0.7.0 → dosync-0.9.0}/dosync/composite_executor.py +0 -0
  90. {dosync-0.7.0 → dosync-0.9.0}/dosync/composite_operations.py +0 -0
  91. {dosync-0.7.0 → dosync-0.9.0}/dosync/device_arbiter.py +0 -0
  92. {dosync-0.7.0 → dosync-0.9.0}/dosync/discoverers.py +0 -0
  93. {dosync-0.7.0 → dosync-0.9.0}/dosync/discoverers_mdns.py +0 -0
  94. {dosync-0.7.0 → dosync-0.9.0}/dosync/discoverers_ssdp.py +0 -0
  95. {dosync-0.7.0 → dosync-0.9.0}/dosync/discovery.py +0 -0
  96. {dosync-0.7.0 → dosync-0.9.0}/dosync/ed25519_pure.py +0 -0
  97. {dosync-0.7.0 → dosync-0.9.0}/dosync/examples/__init__.py +0 -0
  98. {dosync-0.7.0 → dosync-0.9.0}/dosync/examples/declarative/3d-printer.yaml +0 -0
  99. {dosync-0.7.0 → dosync-0.9.0}/dosync/examples/declarative/air-conditioner.yaml +0 -0
  100. {dosync-0.7.0 → dosync-0.9.0}/dosync/examples/declarative/building-lighting.json +0 -0
  101. {dosync-0.7.0 → dosync-0.9.0}/dosync/examples/declarative/industrial-conveyor.yaml +0 -0
  102. {dosync-0.7.0 → dosync-0.9.0}/dosync/examples/declarative/light-generic.yaml +0 -0
  103. {dosync-0.7.0 → dosync-0.9.0}/dosync/examples/declarative/television.yaml +0 -0
  104. {dosync-0.7.0 → dosync-0.9.0}/dosync/execution.py +0 -0
  105. {dosync-0.7.0 → dosync-0.9.0}/dosync/executor.py +0 -0
  106. {dosync-0.7.0 → dosync-0.9.0}/dosync/geo.py +0 -0
  107. {dosync-0.7.0 → dosync-0.9.0}/dosync/hub_monitor.py +0 -0
  108. {dosync-0.7.0 → dosync-0.9.0}/dosync/lightweight.py +0 -0
  109. {dosync-0.7.0 → dosync-0.9.0}/dosync/manage.py +0 -0
  110. {dosync-0.7.0 → dosync-0.9.0}/dosync/metrics.py +0 -0
  111. {dosync-0.7.0 → dosync-0.9.0}/dosync/occupancy.py +0 -0
  112. {dosync-0.7.0 → dosync-0.9.0}/dosync/operation_guards.py +0 -0
  113. {dosync-0.7.0 → dosync-0.9.0}/dosync/operation_supervisor.py +0 -0
  114. {dosync-0.7.0 → dosync-0.9.0}/dosync/operations.py +0 -0
  115. {dosync-0.7.0 → dosync-0.9.0}/dosync/paths.py +0 -0
  116. {dosync-0.7.0 → dosync-0.9.0}/dosync/plan_executor.py +0 -0
  117. {dosync-0.7.0 → dosync-0.9.0}/dosync/plugins.py +0 -0
  118. {dosync-0.7.0 → dosync-0.9.0}/dosync/policy_config.py +0 -0
  119. {dosync-0.7.0 → dosync-0.9.0}/dosync/py.typed +0 -0
  120. {dosync-0.7.0 → dosync-0.9.0}/dosync/reconciler.py +0 -0
  121. {dosync-0.7.0 → dosync-0.9.0}/dosync/registry.py +0 -0
  122. {dosync-0.7.0 → dosync-0.9.0}/dosync/restore.py +0 -0
  123. {dosync-0.7.0 → dosync-0.9.0}/dosync/route_composer.py +0 -0
  124. {dosync-0.7.0 → dosync-0.9.0}/dosync/security.py +0 -0
  125. {dosync-0.7.0 → dosync-0.9.0}/dosync/spec_coverage.py +0 -0
  126. {dosync-0.7.0 → dosync-0.9.0}/dosync/state_refresh.py +0 -0
  127. {dosync-0.7.0 → dosync-0.9.0}/dosync/telemetry.py +0 -0
  128. {dosync-0.7.0 → dosync-0.9.0}/dosync/templates/adapter-draft-prompt.md +0 -0
  129. {dosync-0.7.0 → dosync-0.9.0}/dosync/validation.py +0 -0
  130. {dosync-0.7.0 → dosync-0.9.0}/dosync.egg-info/dependency_links.txt +0 -0
  131. {dosync-0.7.0 → dosync-0.9.0}/dosync.egg-info/requires.txt +0 -0
  132. {dosync-0.7.0 → dosync-0.9.0}/dosync.egg-info/top_level.txt +0 -0
  133. {dosync-0.7.0 → dosync-0.9.0}/setup.cfg +0 -0
  134. {dosync-0.7.0 → dosync-0.9.0}/tests/test_adapter_drafting.py +0 -0
  135. {dosync-0.7.0 → dosync-0.9.0}/tests/test_adapters.py +0 -0
  136. {dosync-0.7.0 → dosync-0.9.0}/tests/test_adapters_speak_the_protocol.py +0 -0
  137. {dosync-0.7.0 → dosync-0.9.0}/tests/test_audit_archive.py +0 -0
  138. {dosync-0.7.0 → dosync-0.9.0}/tests/test_audit_backup.py +0 -0
  139. {dosync-0.7.0 → dosync-0.9.0}/tests/test_audit_chain_integrity.py +0 -0
  140. {dosync-0.7.0 → dosync-0.9.0}/tests/test_audit_chain_ordering.py +0 -0
  141. {dosync-0.7.0 → dosync-0.9.0}/tests/test_audit_concurrency.py +0 -0
  142. {dosync-0.7.0 → dosync-0.9.0}/tests/test_audit_endpoint_limit.py +0 -0
  143. {dosync-0.7.0 → dosync-0.9.0}/tests/test_audit_hashless.py +0 -0
  144. {dosync-0.7.0 → dosync-0.9.0}/tests/test_audit_provenance.py +0 -0
  145. {dosync-0.7.0 → dosync-0.9.0}/tests/test_auth.py +0 -0
  146. {dosync-0.7.0 → dosync-0.9.0}/tests/test_ble_adapter.py +0 -0
  147. {dosync-0.7.0 → dosync-0.9.0}/tests/test_certification_guide.py +0 -0
  148. {dosync-0.7.0 → dosync-0.9.0}/tests/test_certification_honesty.py +0 -0
  149. {dosync-0.7.0 → dosync-0.9.0}/tests/test_check_helpers_can_fail.py +0 -0
  150. {dosync-0.7.0 → dosync-0.9.0}/tests/test_claim_state_machine.py +0 -0
  151. {dosync-0.7.0 → dosync-0.9.0}/tests/test_collaborators_read_the_live_hub.py +0 -0
  152. {dosync-0.7.0 → dosync-0.9.0}/tests/test_composite_executor.py +0 -0
  153. {dosync-0.7.0 → dosync-0.9.0}/tests/test_composite_operations.py +0 -0
  154. {dosync-0.7.0 → dosync-0.9.0}/tests/test_composite_orchestration.py +0 -0
  155. {dosync-0.7.0 → dosync-0.9.0}/tests/test_composition_kind_db.py +0 -0
  156. {dosync-0.7.0 → dosync-0.9.0}/tests/test_composition_kind_endpoint.py +0 -0
  157. {dosync-0.7.0 → dosync-0.9.0}/tests/test_composition_routing.py +0 -0
  158. {dosync-0.7.0 → dosync-0.9.0}/tests/test_dashboard_tells_the_truth.py +0 -0
  159. {dosync-0.7.0 → dosync-0.9.0}/tests/test_dashboard_uses_live_endpoints.py +0 -0
  160. {dosync-0.7.0 → dosync-0.9.0}/tests/test_db.py +0 -0
  161. {dosync-0.7.0 → dosync-0.9.0}/tests/test_db_write_serialization.py +0 -0
  162. {dosync-0.7.0 → dosync-0.9.0}/tests/test_declarative_adapters.py +0 -0
  163. {dosync-0.7.0 → dosync-0.9.0}/tests/test_declarative_location.py +0 -0
  164. {dosync-0.7.0 → dosync-0.9.0}/tests/test_declarative_quarantine.py +0 -0
  165. {dosync-0.7.0 → dosync-0.9.0}/tests/test_deployment_env_contract.py +0 -0
  166. {dosync-0.7.0 → dosync-0.9.0}/tests/test_deployment_layout.py +0 -0
  167. {dosync-0.7.0 → dosync-0.9.0}/tests/test_device_health.py +0 -0
  168. {dosync-0.7.0 → dosync-0.9.0}/tests/test_device_heartbeat.py +0 -0
  169. {dosync-0.7.0 → dosync-0.9.0}/tests/test_device_identity.py +0 -0
  170. {dosync-0.7.0 → dosync-0.9.0}/tests/test_device_location.py +0 -0
  171. {dosync-0.7.0 → dosync-0.9.0}/tests/test_discovery_adoption.py +0 -0
  172. {dosync-0.7.0 → dosync-0.9.0}/tests/test_documentation_is_navigable.py +0 -0
  173. {dosync-0.7.0 → dosync-0.9.0}/tests/test_drone_policies.py +0 -0
  174. {dosync-0.7.0 → dosync-0.9.0}/tests/test_ed25519_pure.py +0 -0
  175. {dosync-0.7.0 → dosync-0.9.0}/tests/test_emergency_preemption.py +0 -0
  176. {dosync-0.7.0 → dosync-0.9.0}/tests/test_env_loading_is_explicit.py +0 -0
  177. {dosync-0.7.0 → dosync-0.9.0}/tests/test_evaluation_metrics.py +0 -0
  178. {dosync-0.7.0 → dosync-0.9.0}/tests/test_event_loop_migration.py +0 -0
  179. {dosync-0.7.0 → dosync-0.9.0}/tests/test_explain_resolve_parity.py +0 -0
  180. {dosync-0.7.0 → dosync-0.9.0}/tests/test_extraction_preserves_behaviour.py +0 -0
  181. {dosync-0.7.0 → dosync-0.9.0}/tests/test_geo.py +0 -0
  182. {dosync-0.7.0 → dosync-0.9.0}/tests/test_ha_bridge_hygiene.py +0 -0
  183. {dosync-0.7.0 → dosync-0.9.0}/tests/test_ha_read_sensors.py +0 -0
  184. {dosync-0.7.0 → dosync-0.9.0}/tests/test_health_recorded_once.py +0 -0
  185. {dosync-0.7.0 → dosync-0.9.0}/tests/test_hub_monitor.py +0 -0
  186. {dosync-0.7.0 → dosync-0.9.0}/tests/test_idempotency.py +0 -0
  187. {dosync-0.7.0 → dosync-0.9.0}/tests/test_independent_observation.py +0 -0
  188. {dosync-0.7.0 → dosync-0.9.0}/tests/test_integration_suite.py +0 -0
  189. {dosync-0.7.0 → dosync-0.9.0}/tests/test_intent_rejections_visible.py +0 -0
  190. {dosync-0.7.0 → dosync-0.9.0}/tests/test_lightweight_heartbeat.py +0 -0
  191. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mavlink_adapter.py +0 -0
  192. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mavlink_channels.py +0 -0
  193. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mavlink_listener.py +0 -0
  194. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mavlink_return_home.py +0 -0
  195. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mavlink_single_reader.py +0 -0
  196. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mavlink_telemetry_closure.py +0 -0
  197. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mcp_dynamic_intents.py +0 -0
  198. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mcp_http_transport.py +0 -0
  199. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mcp_partial_progress.py +0 -0
  200. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mcp_scenarios_are_live.py +0 -0
  201. {dosync-0.7.0 → dosync-0.9.0}/tests/test_mcp_status.py +0 -0
  202. {dosync-0.7.0 → dosync-0.9.0}/tests/test_metrics.py +0 -0
  203. {dosync-0.7.0 → dosync-0.9.0}/tests/test_models.py +0 -0
  204. {dosync-0.7.0 → dosync-0.9.0}/tests/test_multihub_endpoints.py +0 -0
  205. {dosync-0.7.0 → dosync-0.9.0}/tests/test_no_undefined_names.py +0 -0
  206. {dosync-0.7.0 → dosync-0.9.0}/tests/test_notification_templates.py +0 -0
  207. {dosync-0.7.0 → dosync-0.9.0}/tests/test_occupancy.py +0 -0
  208. {dosync-0.7.0 → dosync-0.9.0}/tests/test_openapi_contract.py +0 -0
  209. {dosync-0.7.0 → dosync-0.9.0}/tests/test_operation_guards.py +0 -0
  210. {dosync-0.7.0 → dosync-0.9.0}/tests/test_operation_supervisor.py +0 -0
  211. {dosync-0.7.0 → dosync-0.9.0}/tests/test_operations.py +0 -0
  212. {dosync-0.7.0 → dosync-0.9.0}/tests/test_operations_endpoints.py +0 -0
  213. {dosync-0.7.0 → dosync-0.9.0}/tests/test_operations_persistence.py +0 -0
  214. {dosync-0.7.0 → dosync-0.9.0}/tests/test_panel_polish_2026_07_21.py +0 -0
  215. {dosync-0.7.0 → dosync-0.9.0}/tests/test_plan_executor.py +0 -0
  216. {dosync-0.7.0 → dosync-0.9.0}/tests/test_policy_config.py +0 -0
  217. {dosync-0.7.0 → dosync-0.9.0}/tests/test_protocol_version.py +0 -0
  218. {dosync-0.7.0 → dosync-0.9.0}/tests/test_public_claims.py +0 -0
  219. {dosync-0.7.0 → dosync-0.9.0}/tests/test_pushed_verification.py +0 -0
  220. {dosync-0.7.0 → dosync-0.9.0}/tests/test_quickstart_is_runnable.py +0 -0
  221. {dosync-0.7.0 → dosync-0.9.0}/tests/test_reachability_cause.py +0 -0
  222. {dosync-0.7.0 → dosync-0.9.0}/tests/test_recall_benchmark_postpolicy.py +0 -0
  223. {dosync-0.7.0 → dosync-0.9.0}/tests/test_reconciler.py +0 -0
  224. {dosync-0.7.0 → dosync-0.9.0}/tests/test_registry.py +0 -0
  225. {dosync-0.7.0 → dosync-0.9.0}/tests/test_resolver_scoring.py +0 -0
  226. {dosync-0.7.0 → dosync-0.9.0}/tests/test_route_composer.py +0 -0
  227. {dosync-0.7.0 → dosync-0.9.0}/tests/test_sensor_kind.py +0 -0
  228. {dosync-0.7.0 → dosync-0.9.0}/tests/test_server.py +0 -0
  229. {dosync-0.7.0 → dosync-0.9.0}/tests/test_server_enforces_auth.py +0 -0
  230. {dosync-0.7.0 → dosync-0.9.0}/tests/test_simulation_is_declared.py +0 -0
  231. {dosync-0.7.0 → dosync-0.9.0}/tests/test_state_refresh.py +0 -0
  232. {dosync-0.7.0 → dosync-0.9.0}/tests/test_tag_vocabulary.py +0 -0
  233. {dosync-0.7.0 → dosync-0.9.0}/tests/test_telemetry.py +0 -0
  234. {dosync-0.7.0 → dosync-0.9.0}/tests/test_telemetry_bridge.py +0 -0
  235. {dosync-0.7.0 → dosync-0.9.0}/tests/test_third_party_adapters.py +0 -0
  236. {dosync-0.7.0 → dosync-0.9.0}/tests/test_transport_discoverers.py +0 -0
  237. {dosync-0.7.0 → dosync-0.9.0}/tests/test_undefined_name_regressions.py +0 -0
  238. {dosync-0.7.0 → dosync-0.9.0}/tests/test_unknown_location_refused.py +0 -0
  239. {dosync-0.7.0 → dosync-0.9.0}/tests/test_validation.py +0 -0
  240. {dosync-0.7.0 → dosync-0.9.0}/tests/test_wiring_audit.py +0 -0
  241. {dosync-0.7.0 → dosync-0.9.0}/tests/test_wiz_closes_every_bulb.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dosync
3
- Version: 0.7.0
3
+ Version: 0.9.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
@@ -271,10 +271,14 @@ its first step.
271
271
  ### Install and run — five minutes, no hardware
272
272
 
273
273
  ```bash
274
- pipx install dosync # recommended
274
+ pipx install "dosync[mcp]" # recommended; [mcp] adds the agent connection
275
275
  dosync-hub
276
276
  ```
277
277
 
278
+ Prefer a container, or run Home Assistant? See [Docker — one
279
+ command](#docker--one-command) and [Home Assistant add-on](#home-assistant-add-on):
280
+ all three end in the same dashboard.
281
+
278
282
  <details>
279
283
  <summary><b>On Windows</b> — pipx is not installed with Python there</summary>
280
284
 
@@ -316,6 +320,15 @@ Paste the API key the hub printed on startup, press **Scan**, and the hub
316
320
  searches every transport it can reach — WiFi broadcast, mDNS, SSDP, Bluetooth —
317
321
  and shows what answered. Name anything you want to keep and it is adopted.
318
322
 
323
+ **Governance is on the same page.** **⏸** lists actions an agent asked for
324
+ that you marked as needing a person — confirm or deny them there. **Governance**
325
+ is where you say where each device is (`kitchen`, `plant-1/line-3`,
326
+ `ward-2/room-12` — any path), what it does in an emergency, and which actions
327
+ must wait for you. Releasing a held action or changing those marks takes the
328
+ *operator credential* (`DOSYNC_OPERATOR_TOKEN`, set where the hub starts), which
329
+ your agent never holds. **Agent** connects the agent you use (see
330
+ [Connecting an agent](#connecting-an-agent)).
331
+
319
332
  **Start here.** Discovering and adopting a device needs no terminal and no
320
333
  JSON: a 3D printer, a television and a Bluetooth sensor were adopted this way
321
334
  on the reference deployment without a line being typed. The API calls below are
@@ -672,14 +685,38 @@ hub.
672
685
  a network you do not control, do not click through — that is exactly the case
673
686
  the warning exists for.
674
687
 
675
- ### Docker
688
+ ### Docker — one command
676
689
 
677
690
  ```bash
678
- docker run -p 47200:47200 dosync/hub # published image
679
- # or, from a clone:
680
- docker compose up
691
+ docker run -d --name dosync -p 47200:47200 -v dosync-data:/data \
692
+ ghcr.io/giulianireg-spec/dosync-protocol:latest
693
+ docker logs dosync # the hub's token, printed on first start
681
694
  ```
682
695
 
696
+ Then open `http://localhost:47200`. The image is built for amd64 and arm64 (a
697
+ Raspberry Pi), includes the MCP server (`docker exec dosync dosync-mcp
698
+ --print-config`), keeps its database, keys and audit chain in the `/data`
699
+ volume, and has **authentication on**: a container listens on the network. On
700
+ Linux add `--network host` so discovery (mDNS, SSDP, broadcast) sees your
701
+ network — a bridged container only sees itself. Settings are the same
702
+ environment variables as anywhere (`-e DOSYNC_OPERATOR_TOKEN=…`), and a
703
+ `policies.json` placed in the volume is loaded. From a clone, `docker compose up`
704
+ runs a demo with sample devices.
705
+
706
+ ### Home Assistant add-on
707
+
708
+ In Home Assistant: **Settings → Add-ons → Add-on store → ⋮ → Repositories**, add
709
+
710
+ ```
711
+ https://github.com/giulianireg-spec/dosync-protocol
712
+ ```
713
+
714
+ install **DoSync**, start it, read the token in its **Log** tab and press **Open
715
+ web UI**. It runs the same image, and its Home Assistant bridge connects through
716
+ the Supervisor — no long-lived token to create. Your entities appear as devices,
717
+ governed by your rules; the operator token, who configures, and the confirmation
718
+ timeout are options of the add-on.
719
+
683
720
  ### From source (development)
684
721
 
685
722
  ```bash
@@ -793,6 +830,27 @@ test, so it is named rather than recommended.
793
830
 
794
831
  ## Connecting an agent
795
832
 
833
+ **The short way — no paths to work out.** Install the MCP part once, then let
834
+ DoSync write your agent's configuration:
835
+
836
+ ```bash
837
+ pipx install "dosync[mcp]" # or, if DoSync is already installed:
838
+ # pipx inject dosync "mcp>=1.27.0,<2.0"
839
+ dosync-mcp --check --token <your token> # says what is missing, in words
840
+ dosync-mcp --print-config --token <your token> # the block to paste into your agent
841
+ ```
842
+
843
+ Or open the dashboard and press **Agent**: it shows the same block, with this
844
+ hub's address and your token already filled in, and where common agents keep
845
+ the file. Any MCP client works — Claude Desktop, Claude Code, Cursor or another;
846
+ the protocol does not pick your agent. When the agent runs on a different
847
+ computer from the hub, install DoSync there too and add `--hub-url
848
+ http://<hub address>:47200`. CI runs exactly this path (pipx → hub → dashboard →
849
+ agent) on Linux and macOS on every change.
850
+
851
+ Everything below explains what that command writes, for when you want to do it
852
+ by hand.
853
+
796
854
  This page has said "MCP" a dozen times — a badge at the top, a section on why
797
855
  MCP is the channel rather than the rival, a drone that flew a mission through
798
856
  it — and never once explained how to connect an agent. This is that.
@@ -952,6 +1010,58 @@ the other end.
952
1010
 
953
1011
  ---
954
1012
 
1013
+ ### What an agent can do — and how to change it
1014
+
1015
+ Out of the box, an agent connected through MCP can do three things:
1016
+
1017
+ - **Pursue a goal** — `dosync_fire_intent` with a class (`ensure_safety`,
1018
+ `notify`, … or one your deployment registers) and let the hub choose the
1019
+ devices.
1020
+ - **Choose the devices itself** — the same tool with `proposed_actions`; the
1021
+ hub runs only the proposals that pass its rules.
1022
+ - **Operate a named device** — `dosync_control_device`: *"turn off the hall
1023
+ light"*, *"stop line 2"*, *"fly the drone to these coordinates"*. It fires the
1024
+ universal class `operate_device`, which grants any action the device declares,
1025
+ so nothing has to be configured first.
1026
+
1027
+ All three are checked by the hub before anything moves. The device must declare
1028
+ the action; a place the agent names confines it; no device receives two
1029
+ opposite actions (`lock`/`unlock`, `turn_on`/`turn_off`, …); in an emergency, a
1030
+ device that declared its emergency actions does only those; your policies
1031
+ apply; and every action — and every refusal, with its reason — is in the audit
1032
+ log.
1033
+
1034
+ What is **not** narrowed by default is *authority*: `operate_device` grants any
1035
+ action a device declares. You decide how much of that an agent gets:
1036
+
1037
+ | You want | Do this |
1038
+ |---|---|
1039
+ | Agents to act only through goals you defined | Block the class in your policy file: `{"type": "block_intent", "intent_classes": ["operate_device"], "reason": "Agents act through our goals"}` |
1040
+ | Agents to keep their hands off some devices | `{"type": "device_exclusion", "intent_classes": ["operate_device"], "excluded_device_ids": ["front-door-lock"]}` |
1041
+ | Your own scripts to call one device with no intent around it | `DOSYNC_DIRECT_CONTROL=operator` and `DOSYNC_OPERATOR_TOKEN=<secret>`; send it as `X-DoSync-Operator-Token`. **Never give this token to an agent.** |
1042
+ | The raw path open, for development | `DOSYNC_DIRECT_CONTROL=on` — the hub says so in `/v1/status` |
1043
+
1044
+ (Policy files are JSON with `"version": 1` and a `"policies"` list, loaded from
1045
+ `DOSYNC_POLICIES`.)
1046
+
1047
+ Devices described by a **W3C WoT Thing Description** join the same way: `POST
1048
+ /v1/things` with the TD (and, as the operator, the place it is in). The hub maps
1049
+ its actions, reports how it mapped each one, and governs it like any other
1050
+ device — see spec §5.9.
1051
+
1052
+ And the other way: **every device the hub governs is published as a W3C WoT
1053
+ Thing Description**, so any WoT consumer — one that has never heard of DoSync —
1054
+ can find and use it. `GET /.well-known/wot` describes the directory, `GET
1055
+ /v1/things` lists the TDs, and each action's form points at the hub, never at the
1056
+ device: invoking it runs through the same rules as an agent's request (an action
1057
+ you marked comes back `202 held_for_confirmation` and waits for you in ⏸).
1058
+ Tested in CI with an independent consumer, Eclipse Thingweb node-wot — see spec
1059
+ §5.10.
1060
+
1061
+ The raw HTTP path, `POST /v1/device/action`, is **closed by default**. It names
1062
+ a device and an action with no intent around it, so the hub's guarantees cannot
1063
+ apply to it; it exists for an operator, not an agent.
1064
+
955
1065
  ## What's built today
956
1066
 
957
1067
  | Component | Status |
@@ -961,7 +1071,8 @@ the other end.
961
1071
  | Web dashboard | ✅ |
962
1072
  | API key authentication + SHA-256 audit log | ✅ |
963
1073
  | Capability-based resolver | ✅ |
964
- | Certification CLI — Basic 10 · Standard 33 · Emergency 44 · Conformance 65, each run in CI against a live hub (signed reports) | ✅ |
1074
+ | Certification CLI — Basic 10 · Standard 33 · Emergency 44 · Conformance 72, each run in CI against a live hub (signed reports) | ✅ |
1075
+ | W3C WoT: import Thing Descriptions, publish governed ones, WoT Discovery; interop tested with node-wot | ✅ |
965
1076
  | Philips WiZ adapter (UDP local) | ✅ |
966
1077
  | Home Assistant bridge (10 domains) | ✅ |
967
1078
  | Native MCP server (Claude, ChatGPT, any LLM) | ✅ |
@@ -1014,8 +1125,8 @@ python3 certify.py --host <hub-ip> --port 47200 --tier standard
1014
1125
 
1015
1126
  | Language | Location | Author | Certification |
1016
1127
  |---|---|---|---|
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 |
1128
+ | Python (reference) | `server.py` | this project | Conformance 70/70 ✅ (every tier, in CI) |
1129
+ | 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 70-check suite pending |
1019
1130
 
1020
1131
  The Node.js implementation is a **companion** port that validates the protocol
1021
1132
  is implementable in a second language against the same certification suite —
@@ -1051,6 +1162,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, including the C
1051
1162
  ## Specification
1052
1163
 
1053
1164
  - [spec/DoSync-SPEC-v0.1.md](spec/DoSync-SPEC-v0.1.md) — full protocol specification
1165
+ - [spec/SAMUEL-SPEC-v0.2.md](spec/SAMUEL-SPEC-v0.2.md) — **SAMUEL**, Standard for Agent Mediation Under Enforceable Limits: what an AI agent may do to physical devices, within limits the operator sets and the hub enforces — nine guarantees independent of how devices are described (DoSync manifests, WoT Thing Descriptions), every refusal reason, and the certification checks that show conformance. DoSync is its reference implementation
1054
1166
  - [spec/RESOLVER-SPEC-v0.3.md](spec/RESOLVER-SPEC-v0.3.md) — resolver interface + external resolver protocol
1055
1167
  - [DESIGN-PRINCIPLES.md](DESIGN-PRINCIPLES.md) — architectural decisions and rationale
1056
1168
  - [COMPATIBILITY.md](docs/COMPATIBILITY.md) — backward compatibility guarantees
@@ -205,10 +205,14 @@ its first step.
205
205
  ### Install and run — five minutes, no hardware
206
206
 
207
207
  ```bash
208
- pipx install dosync # recommended
208
+ pipx install "dosync[mcp]" # recommended; [mcp] adds the agent connection
209
209
  dosync-hub
210
210
  ```
211
211
 
212
+ Prefer a container, or run Home Assistant? See [Docker — one
213
+ command](#docker--one-command) and [Home Assistant add-on](#home-assistant-add-on):
214
+ all three end in the same dashboard.
215
+
212
216
  <details>
213
217
  <summary><b>On Windows</b> — pipx is not installed with Python there</summary>
214
218
 
@@ -250,6 +254,15 @@ Paste the API key the hub printed on startup, press **Scan**, and the hub
250
254
  searches every transport it can reach — WiFi broadcast, mDNS, SSDP, Bluetooth —
251
255
  and shows what answered. Name anything you want to keep and it is adopted.
252
256
 
257
+ **Governance is on the same page.** **⏸** lists actions an agent asked for
258
+ that you marked as needing a person — confirm or deny them there. **Governance**
259
+ is where you say where each device is (`kitchen`, `plant-1/line-3`,
260
+ `ward-2/room-12` — any path), what it does in an emergency, and which actions
261
+ must wait for you. Releasing a held action or changing those marks takes the
262
+ *operator credential* (`DOSYNC_OPERATOR_TOKEN`, set where the hub starts), which
263
+ your agent never holds. **Agent** connects the agent you use (see
264
+ [Connecting an agent](#connecting-an-agent)).
265
+
253
266
  **Start here.** Discovering and adopting a device needs no terminal and no
254
267
  JSON: a 3D printer, a television and a Bluetooth sensor were adopted this way
255
268
  on the reference deployment without a line being typed. The API calls below are
@@ -606,14 +619,38 @@ hub.
606
619
  a network you do not control, do not click through — that is exactly the case
607
620
  the warning exists for.
608
621
 
609
- ### Docker
622
+ ### Docker — one command
610
623
 
611
624
  ```bash
612
- docker run -p 47200:47200 dosync/hub # published image
613
- # or, from a clone:
614
- docker compose up
625
+ docker run -d --name dosync -p 47200:47200 -v dosync-data:/data \
626
+ ghcr.io/giulianireg-spec/dosync-protocol:latest
627
+ docker logs dosync # the hub's token, printed on first start
615
628
  ```
616
629
 
630
+ Then open `http://localhost:47200`. The image is built for amd64 and arm64 (a
631
+ Raspberry Pi), includes the MCP server (`docker exec dosync dosync-mcp
632
+ --print-config`), keeps its database, keys and audit chain in the `/data`
633
+ volume, and has **authentication on**: a container listens on the network. On
634
+ Linux add `--network host` so discovery (mDNS, SSDP, broadcast) sees your
635
+ network — a bridged container only sees itself. Settings are the same
636
+ environment variables as anywhere (`-e DOSYNC_OPERATOR_TOKEN=…`), and a
637
+ `policies.json` placed in the volume is loaded. From a clone, `docker compose up`
638
+ runs a demo with sample devices.
639
+
640
+ ### Home Assistant add-on
641
+
642
+ In Home Assistant: **Settings → Add-ons → Add-on store → ⋮ → Repositories**, add
643
+
644
+ ```
645
+ https://github.com/giulianireg-spec/dosync-protocol
646
+ ```
647
+
648
+ install **DoSync**, start it, read the token in its **Log** tab and press **Open
649
+ web UI**. It runs the same image, and its Home Assistant bridge connects through
650
+ the Supervisor — no long-lived token to create. Your entities appear as devices,
651
+ governed by your rules; the operator token, who configures, and the confirmation
652
+ timeout are options of the add-on.
653
+
617
654
  ### From source (development)
618
655
 
619
656
  ```bash
@@ -727,6 +764,27 @@ test, so it is named rather than recommended.
727
764
 
728
765
  ## Connecting an agent
729
766
 
767
+ **The short way — no paths to work out.** Install the MCP part once, then let
768
+ DoSync write your agent's configuration:
769
+
770
+ ```bash
771
+ pipx install "dosync[mcp]" # or, if DoSync is already installed:
772
+ # pipx inject dosync "mcp>=1.27.0,<2.0"
773
+ dosync-mcp --check --token <your token> # says what is missing, in words
774
+ dosync-mcp --print-config --token <your token> # the block to paste into your agent
775
+ ```
776
+
777
+ Or open the dashboard and press **Agent**: it shows the same block, with this
778
+ hub's address and your token already filled in, and where common agents keep
779
+ the file. Any MCP client works — Claude Desktop, Claude Code, Cursor or another;
780
+ the protocol does not pick your agent. When the agent runs on a different
781
+ computer from the hub, install DoSync there too and add `--hub-url
782
+ http://<hub address>:47200`. CI runs exactly this path (pipx → hub → dashboard →
783
+ agent) on Linux and macOS on every change.
784
+
785
+ Everything below explains what that command writes, for when you want to do it
786
+ by hand.
787
+
730
788
  This page has said "MCP" a dozen times — a badge at the top, a section on why
731
789
  MCP is the channel rather than the rival, a drone that flew a mission through
732
790
  it — and never once explained how to connect an agent. This is that.
@@ -886,6 +944,58 @@ the other end.
886
944
 
887
945
  ---
888
946
 
947
+ ### What an agent can do — and how to change it
948
+
949
+ Out of the box, an agent connected through MCP can do three things:
950
+
951
+ - **Pursue a goal** — `dosync_fire_intent` with a class (`ensure_safety`,
952
+ `notify`, … or one your deployment registers) and let the hub choose the
953
+ devices.
954
+ - **Choose the devices itself** — the same tool with `proposed_actions`; the
955
+ hub runs only the proposals that pass its rules.
956
+ - **Operate a named device** — `dosync_control_device`: *"turn off the hall
957
+ light"*, *"stop line 2"*, *"fly the drone to these coordinates"*. It fires the
958
+ universal class `operate_device`, which grants any action the device declares,
959
+ so nothing has to be configured first.
960
+
961
+ All three are checked by the hub before anything moves. The device must declare
962
+ the action; a place the agent names confines it; no device receives two
963
+ opposite actions (`lock`/`unlock`, `turn_on`/`turn_off`, …); in an emergency, a
964
+ device that declared its emergency actions does only those; your policies
965
+ apply; and every action — and every refusal, with its reason — is in the audit
966
+ log.
967
+
968
+ What is **not** narrowed by default is *authority*: `operate_device` grants any
969
+ action a device declares. You decide how much of that an agent gets:
970
+
971
+ | You want | Do this |
972
+ |---|---|
973
+ | Agents to act only through goals you defined | Block the class in your policy file: `{"type": "block_intent", "intent_classes": ["operate_device"], "reason": "Agents act through our goals"}` |
974
+ | Agents to keep their hands off some devices | `{"type": "device_exclusion", "intent_classes": ["operate_device"], "excluded_device_ids": ["front-door-lock"]}` |
975
+ | Your own scripts to call one device with no intent around it | `DOSYNC_DIRECT_CONTROL=operator` and `DOSYNC_OPERATOR_TOKEN=<secret>`; send it as `X-DoSync-Operator-Token`. **Never give this token to an agent.** |
976
+ | The raw path open, for development | `DOSYNC_DIRECT_CONTROL=on` — the hub says so in `/v1/status` |
977
+
978
+ (Policy files are JSON with `"version": 1` and a `"policies"` list, loaded from
979
+ `DOSYNC_POLICIES`.)
980
+
981
+ Devices described by a **W3C WoT Thing Description** join the same way: `POST
982
+ /v1/things` with the TD (and, as the operator, the place it is in). The hub maps
983
+ its actions, reports how it mapped each one, and governs it like any other
984
+ device — see spec §5.9.
985
+
986
+ And the other way: **every device the hub governs is published as a W3C WoT
987
+ Thing Description**, so any WoT consumer — one that has never heard of DoSync —
988
+ can find and use it. `GET /.well-known/wot` describes the directory, `GET
989
+ /v1/things` lists the TDs, and each action's form points at the hub, never at the
990
+ device: invoking it runs through the same rules as an agent's request (an action
991
+ you marked comes back `202 held_for_confirmation` and waits for you in ⏸).
992
+ Tested in CI with an independent consumer, Eclipse Thingweb node-wot — see spec
993
+ §5.10.
994
+
995
+ The raw HTTP path, `POST /v1/device/action`, is **closed by default**. It names
996
+ a device and an action with no intent around it, so the hub's guarantees cannot
997
+ apply to it; it exists for an operator, not an agent.
998
+
889
999
  ## What's built today
890
1000
 
891
1001
  | Component | Status |
@@ -895,7 +1005,8 @@ the other end.
895
1005
  | Web dashboard | ✅ |
896
1006
  | API key authentication + SHA-256 audit log | ✅ |
897
1007
  | Capability-based resolver | ✅ |
898
- | Certification CLI — Basic 10 · Standard 33 · Emergency 44 · Conformance 65, each run in CI against a live hub (signed reports) | ✅ |
1008
+ | Certification CLI — Basic 10 · Standard 33 · Emergency 44 · Conformance 72, each run in CI against a live hub (signed reports) | ✅ |
1009
+ | W3C WoT: import Thing Descriptions, publish governed ones, WoT Discovery; interop tested with node-wot | ✅ |
899
1010
  | Philips WiZ adapter (UDP local) | ✅ |
900
1011
  | Home Assistant bridge (10 domains) | ✅ |
901
1012
  | Native MCP server (Claude, ChatGPT, any LLM) | ✅ |
@@ -948,8 +1059,8 @@ python3 certify.py --host <hub-ip> --port 47200 --tier standard
948
1059
 
949
1060
  | Language | Location | Author | Certification |
950
1061
  |---|---|---|---|
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 |
1062
+ | Python (reference) | `server.py` | this project | Conformance 70/70 ✅ (every tier, in CI) |
1063
+ | 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 70-check suite pending |
953
1064
 
954
1065
  The Node.js implementation is a **companion** port that validates the protocol
955
1066
  is implementable in a second language against the same certification suite —
@@ -985,6 +1096,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, including the C
985
1096
  ## Specification
986
1097
 
987
1098
  - [spec/DoSync-SPEC-v0.1.md](spec/DoSync-SPEC-v0.1.md) — full protocol specification
1099
+ - [spec/SAMUEL-SPEC-v0.2.md](spec/SAMUEL-SPEC-v0.2.md) — **SAMUEL**, Standard for Agent Mediation Under Enforceable Limits: what an AI agent may do to physical devices, within limits the operator sets and the hub enforces — nine guarantees independent of how devices are described (DoSync manifests, WoT Thing Descriptions), every refusal reason, and the certification checks that show conformance. DoSync is its reference implementation
988
1100
  - [spec/RESOLVER-SPEC-v0.3.md](spec/RESOLVER-SPEC-v0.3.md) — resolver interface + external resolver protocol
989
1101
  - [DESIGN-PRINCIPLES.md](DESIGN-PRINCIPLES.md) — architectural decisions and rationale
990
1102
  - [COMPATIBILITY.md](docs/COMPATIBILITY.md) — backward compatibility guarantees
@@ -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.7.0"
16
+ __version__ = "0.9.0"
17
17
  __protocol_version__ = "0.5"
@@ -0,0 +1,136 @@
1
+ """Execute the actions of imported WoT Things through their TD's HTTP forms.
2
+
3
+ A Thing imported by dosync/wot_import.py carries, in its adapter_config, each
4
+ governed action's affordance: an action (invokeaction) or a writable property
5
+ (writeproperty), with the forms of its TD. This adapter performs them with the
6
+ WoT HTTP binding defaults -- POST to invoke an action, PUT to write a property,
7
+ GET to read one, JSON bodies -- after the hub has already decided, under its
8
+ guarantees and the operator's policies, that the action may run.
9
+
10
+ Only HTTP(S) forms are executed. A Thing whose TD offers only CoAP or MQTT forms
11
+ is imported and governed, and its actions are refused here with that reason,
12
+ rather than pretended. Security: ``nosec`` is assumed unless the operator gives
13
+ a bearer token for the Thing in adapter_config["bearer_token_env"] (the name of
14
+ an environment variable, never the token itself).
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import json
20
+ import os
21
+ import re
22
+ import urllib.error
23
+ import urllib.request
24
+ from urllib.parse import quote
25
+
26
+ from . import DoSyncAdapter
27
+ from ..models import ActionResult
28
+
29
+
30
+ _TEMPLATE = re.compile(r"\{([?&/#+.;]?)([^}]*)\}")
31
+
32
+
33
+ def expand(href: str, params: dict) -> tuple[str, dict]:
34
+ """Expand the RFC 6570 templates TD forms use for uriVariables -- ``{var}``
35
+ in a path and ``{?a,b}`` / ``{&a}`` in a query -- from the action's params.
36
+ Variables not given are dropped; the params used are removed from the body.
37
+ Found against node-wot's own example Thing: its counter's form is
38
+ ``.../actions/increment{?step}``, and the literal template is a 404."""
39
+ rest = dict(params)
40
+
41
+ def fill(m):
42
+ op, names = m.group(1), [n.strip() for n in m.group(2).split(",") if n.strip()]
43
+ given = [(n, rest.pop(n)) for n in names if n in rest]
44
+ if op in ("?", "&"):
45
+ if not given:
46
+ return ""
47
+ q = "&".join(f"{quote(str(n))}={quote(str(v))}" for n, v in given)
48
+ return ("?" if op == "?" else "&") + q
49
+ return ",".join(quote(str(v), safe="") for _, v in given)
50
+ return _TEMPLATE.sub(fill, href), rest
51
+
52
+
53
+ def _http_form(forms: list[dict]) -> dict | None:
54
+ return next((f for f in forms if str(f.get("href", "")).startswith(("http://", "https://"))), None)
55
+
56
+
57
+ def _body_for(affordance: dict, params: dict):
58
+ if affordance["kind"] == "property":
59
+ return params.get("value", next(iter(params.values()), None)) if params else None
60
+ schema = affordance.get("input") or {}
61
+ if not params:
62
+ return None
63
+ if isinstance(schema, dict) and schema.get("type") not in (None, "object") and len(params) == 1:
64
+ return next(iter(params.values()))
65
+ return params
66
+
67
+
68
+ class WotHttpAdapter(DoSyncAdapter):
69
+ def __init__(self, hub=None, timeout: float = 10.0):
70
+ self._hub = hub
71
+ self._timeout = timeout
72
+
73
+ @property
74
+ def adapter_name(self) -> str:
75
+ return "wot"
76
+
77
+ async def connect(self, config) -> bool:
78
+ return True
79
+
80
+ def _config(self, device_id: str) -> dict:
81
+ device = self._hub.registry.get(device_id) if self._hub else None
82
+ return dict(getattr(device, "adapter_config", None) or {})
83
+
84
+ def _request(self, method: str, url: str, body, config: dict):
85
+ data = None if body is None else json.dumps(body).encode()
86
+ req = urllib.request.Request(url, data=data, method=method)
87
+ if data is not None:
88
+ req.add_header("Content-Type", "application/json")
89
+ env = config.get("bearer_token_env")
90
+ if env and os.environ.get(env):
91
+ req.add_header("Authorization", f"Bearer {os.environ[env]}")
92
+ with urllib.request.urlopen(req, timeout=self._timeout) as resp:
93
+ text = resp.read().decode(errors="replace")
94
+ return resp.status, text
95
+
96
+ async def execute(self, action, urgency) -> ActionResult:
97
+ config = self._config(action.device_id)
98
+ if action.action == "read_sensors":
99
+ readings, errors = {}, []
100
+ for st, spec in (config.get("sensors") or {}).items():
101
+ form = _http_form(spec.get("forms") or [])
102
+ if not form:
103
+ continue
104
+ try:
105
+ status, text = await asyncio.to_thread(self._request, form.get("method") or "GET",
106
+ expand(form["href"], {})[0], None, config)
107
+ readings[st] = json.loads(text) if text else None
108
+ except Exception as e:
109
+ errors.append(f"{st}: {type(e).__name__}: {e}")
110
+ return ActionResult(device_id=action.device_id, action=action.action, success=not errors,
111
+ response={"readings": readings}, error="; ".join(errors) or None)
112
+ affordance = (config.get("affordances") or {}).get(action.action)
113
+ if not affordance:
114
+ return ActionResult(device_id=action.device_id, action=action.action, success=False,
115
+ error=f"the Thing's TD has no affordance for '{action.action}'")
116
+ form = _http_form(affordance.get("forms") or [])
117
+ if not form:
118
+ return ActionResult(device_id=action.device_id, action=action.action, success=False,
119
+ error="the Thing's TD offers no HTTP form for this affordance "
120
+ "(only HTTP is executed; the action is governed but not run)")
121
+ method = form.get("method") or ("PUT" if affordance["kind"] == "property" else "POST")
122
+ url, rest = expand(form["href"], dict(action.params or {}))
123
+ body = _body_for(affordance, rest)
124
+ try:
125
+ status, text = await asyncio.to_thread(self._request, method, url, body, config)
126
+ return ActionResult(device_id=action.device_id, action=action.action, success=200 <= status < 300,
127
+ response={"status": status, "body": text[:500]})
128
+ except urllib.error.HTTPError as e:
129
+ return ActionResult(device_id=action.device_id, action=action.action, success=False,
130
+ error=f"HTTP {e.code}: {e.read().decode(errors='replace')[:200]}")
131
+ except Exception as e:
132
+ return ActionResult(device_id=action.device_id, action=action.action, success=False,
133
+ error=f"{type(e).__name__}: {e}")
134
+
135
+ async def get_state(self, device_id: str) -> dict | None:
136
+ return None
@@ -96,6 +96,14 @@ class AuthManager:
96
96
  "offline at full speed.")
97
97
  else:
98
98
  token = secrets.token_urlsafe(32)
99
+ operator = os.environ.get("DOSYNC_OPERATOR_TOKEN", "")
100
+ if operator and secrets.compare_digest(token, operator):
101
+ # H5: an agent key equal to the operator credential would hand the
102
+ # agent the operator's authority (SAMUEL §3: they MUST differ).
103
+ raise ValueError(
104
+ "That value is this hub's operator credential. An agent key must be "
105
+ "different: whoever holds the operator credential releases held "
106
+ "actions, and the agent must not be able to.")
99
107
  key_hash = hash_token(token)
100
108
  self.db.save_api_key(key_hash, label)
101
109
  log.info("New API key generated: label='%s'", label)