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.
- {dosync-0.6.2 → dosync-0.7.0}/PKG-INFO +177 -13
- {dosync-0.6.2 → dosync-0.7.0}/README.md +169 -9
- {dosync-0.6.2 → dosync-0.7.0}/dosync/__init__.py +2 -2
- {dosync-0.6.2 → dosync-0.7.0}/dosync/adapter_drafting.py +7 -2
- {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/__init__.py +14 -30
- {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/homeassistant.py +306 -61
- {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/matter.py +39 -30
- {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/mqtt.py +1 -1
- {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/notifications.py +9 -9
- {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/shelly.py +37 -28
- {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/wiz.py +58 -28
- dosync-0.7.0/dosync/audit.py +809 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/audit_backup.py +16 -12
- {dosync-0.6.2 → dosync-0.7.0}/dosync/auth.py +11 -11
- {dosync-0.6.2 → dosync-0.7.0}/dosync/certify.py +158 -4
- dosync-0.7.0/dosync/composite_executor.py +288 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/dashboard.html +25 -4
- {dosync-0.6.2 → dosync-0.7.0}/dosync/db.py +181 -68
- {dosync-0.6.2 → dosync-0.7.0}/dosync/declarative.py +53 -9
- {dosync-0.6.2 → dosync-0.7.0}/dosync/discovery.py +4 -4
- {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/3d-printer.yaml +2 -2
- {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/air-conditioner.yaml +2 -2
- {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/building-lighting.json +2 -1
- {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/industrial-conveyor.yaml +2 -2
- {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/light-generic.yaml +5 -4
- {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/declarative/television.yaml +2 -2
- dosync-0.7.0/dosync/execution.py +425 -0
- dosync-0.7.0/dosync/hub.py +999 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/manage.py +5 -5
- {dosync-0.6.2 → dosync-0.7.0}/dosync/mcp_server.py +263 -97
- {dosync-0.6.2 → dosync-0.7.0}/dosync/metrics.py +10 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/models.py +177 -96
- dosync-0.7.0/dosync/occupancy.py +105 -0
- dosync-0.7.0/dosync/plan_executor.py +347 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/policies.py +16 -23
- dosync-0.7.0/dosync/registry.py +167 -0
- dosync-0.7.0/dosync/resolvers.py +1176 -0
- dosync-0.7.0/dosync/restore.py +112 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/security.py +22 -22
- {dosync-0.6.2 → dosync-0.7.0}/dosync/server.py +344 -40
- dosync-0.7.0/dosync/spec/TAG-VOCABULARY.md +332 -0
- dosync-0.7.0/dosync/state_refresh.py +154 -0
- dosync-0.7.0/dosync/telemetry.py +99 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/templates/adapter-draft-prompt.md +8 -5
- {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/PKG-INFO +177 -13
- {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/SOURCES.txt +50 -1
- {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/requires.txt +2 -2
- {dosync-0.6.2 → dosync-0.7.0}/pyproject.toml +23 -4
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_adapter_drafting.py +29 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_adapters.py +8 -4
- dosync-0.7.0/tests/test_adapters_speak_the_protocol.py +102 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_audit_archive.py +49 -0
- dosync-0.7.0/tests/test_audit_chain_ordering.py +476 -0
- dosync-0.7.0/tests/test_audit_concurrency.py +82 -0
- dosync-0.7.0/tests/test_audit_endpoint_limit.py +40 -0
- dosync-0.7.0/tests/test_audit_hashless.py +40 -0
- dosync-0.7.0/tests/test_certification_guide.py +44 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_certification_honesty.py +50 -7
- dosync-0.7.0/tests/test_check_helpers_can_fail.py +31 -0
- dosync-0.7.0/tests/test_collaborators_read_the_live_hub.py +51 -0
- dosync-0.7.0/tests/test_composite_executor.py +31 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_composite_operations.py +6 -2
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_composite_orchestration.py +9 -5
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_composition_kind_db.py +6 -2
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_composition_kind_endpoint.py +6 -2
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_composition_routing.py +9 -5
- dosync-0.7.0/tests/test_dashboard_uses_live_endpoints.py +34 -0
- dosync-0.7.0/tests/test_db_write_serialization.py +72 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_declarative_adapters.py +16 -8
- dosync-0.7.0/tests/test_declarative_location.py +108 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_deployment_env_contract.py +2 -2
- dosync-0.7.0/tests/test_device_location.py +245 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_direct_action_governance.py +15 -6
- dosync-0.7.0/tests/test_direct_action_instrumented.py +86 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_documentation_is_navigable.py +64 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_explain_consistency.py +5 -1
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_explain_resolve_parity.py +54 -24
- dosync-0.7.0/tests/test_extraction_preserves_behaviour.py +635 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_geo.py +6 -2
- dosync-0.7.0/tests/test_ha_read_sensors.py +75 -0
- dosync-0.7.0/tests/test_health_recorded_once.py +82 -0
- dosync-0.7.0/tests/test_intent_rejections_visible.py +81 -0
- dosync-0.7.0/tests/test_location_restricts.py +165 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_channels.py +6 -2
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_return_home.py +6 -2
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_single_reader.py +6 -2
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_telemetry_closure.py +6 -2
- dosync-0.7.0/tests/test_mcp_dynamic_intents.py +267 -0
- dosync-0.7.0/tests/test_mcp_http_transport.py +60 -0
- dosync-0.7.0/tests/test_mcp_scenarios_are_live.py +78 -0
- dosync-0.7.0/tests/test_mcp_speaks_english.py +95 -0
- dosync-0.7.0/tests/test_mcp_status.py +27 -0
- dosync-0.7.0/tests/test_measurement_tools_are_honest.py +286 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_no_operator_data.py +253 -2
- dosync-0.7.0/tests/test_no_undefined_names.py +41 -0
- dosync-0.7.0/tests/test_occupancy.py +70 -0
- dosync-0.7.0/tests/test_openapi_contract.py +62 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_operation_guards.py +6 -2
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_operation_supervisor.py +6 -2
- dosync-0.7.0/tests/test_plan_executor.py +32 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_policies.py +7 -4
- dosync-0.7.0/tests/test_protocol_version.py +66 -0
- dosync-0.7.0/tests/test_registry.py +69 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_resolver_scoring.py +145 -1
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_route_composer.py +6 -2
- dosync-0.7.0/tests/test_schemas_match_the_hub.py +163 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_server.py +27 -7
- dosync-0.7.0/tests/test_server_enforces_auth.py +27 -0
- dosync-0.7.0/tests/test_state_refresh.py +50 -0
- dosync-0.7.0/tests/test_tag_vocabulary.py +72 -0
- dosync-0.7.0/tests/test_telemetry.py +47 -0
- dosync-0.7.0/tests/test_undefined_name_regressions.py +53 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_universal_intent_contract.py +17 -4
- dosync-0.7.0/tests/test_unknown_location_refused.py +57 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_wiring_audit.py +4 -42
- dosync-0.7.0/tests/test_wiz_closes_every_bulb.py +76 -0
- dosync-0.6.2/dosync/hub.py +0 -3711
- dosync-0.6.2/tests/test_mcp_dynamic_intents.py +0 -131
- {dosync-0.6.2 → dosync-0.7.0}/LICENSE +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/ble.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/declarative.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/adapters/mavlink.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/auth_fastapi.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/cert_signing.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/cli.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/composite_operations.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/config_reference.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/device_arbiter.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/discoverers.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/discoverers_mdns.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/discoverers_ssdp.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/ed25519_pure.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/examples/__init__.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/executor.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/geo.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/hub_monitor.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/lightweight.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/operation_guards.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/operation_supervisor.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/operations.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/paths.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/plugins.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/policy_config.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/py.typed +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/reconciler.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/route_composer.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/spec_coverage.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync/validation.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/dependency_links.txt +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/entry_points.txt +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/dosync.egg-info/top_level.txt +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/setup.cfg +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_audit_backup.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_audit_chain_integrity.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_audit_provenance.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_auth.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_ble_adapter.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_claim_state_machine.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_dashboard_tells_the_truth.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_db.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_declarative_quarantine.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_deployment_layout.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_device_health.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_device_heartbeat.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_device_identity.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_discovery_adoption.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_drone_policies.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_ed25519_pure.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_emergency_preemption.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_env_loading_is_explicit.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_evaluation_metrics.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_event_loop_migration.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_ha_bridge_hygiene.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_hub_monitor.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_idempotency.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_independent_observation.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_integration_suite.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_lightweight_heartbeat.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_adapter.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_mavlink_listener.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_mcp_partial_progress.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_metrics.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_models.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_multihub_endpoints.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_notification_templates.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_operations.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_operations_endpoints.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_operations_persistence.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_operations_wiring.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_panel_polish_2026_07_21.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_policy_config.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_public_claims.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_pushed_verification.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_quickstart_is_runnable.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_reachability_cause.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_recall_benchmark_postpolicy.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_reconciler.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_resolution_wiring.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_resolver_semantics.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_sensor_kind.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_simulation_is_declared.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_telemetry_bridge.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_third_party_adapters.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_transport_discoverers.py +0 -0
- {dosync-0.6.2 → dosync-0.7.0}/tests/test_validation.py +0 -0
- {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.
|
|
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,
|
|
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
|
|
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
|
|
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)
|
|
68
|
-
[](spec/DoSync-SPEC-v0.1.md)
|
|
69
73
|
[](https://pypi.org/project/dosync/)
|
|
70
74
|
[](https://pypi.org/project/dosync/)
|
|
71
75
|
[](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
|
|
216
|
+
User / AI says: "there is a fire in building B"
|
|
213
217
|
│
|
|
214
218
|
DoSync Hub
|
|
215
219
|
│
|
|
216
220
|
┌───────────────┼───────────────┐
|
|
217
221
|
▼ ▼ ▼
|
|
218
|
-
💡
|
|
219
|
-
at maximum
|
|
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
|
|
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
|
|
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 |
|
|
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
|
|
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)
|
|
6
|
-
[](spec/DoSync-SPEC-v0.1.md)
|
|
7
7
|
[](https://pypi.org/project/dosync/)
|
|
8
8
|
[](https://pypi.org/project/dosync/)
|
|
9
9
|
[](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
|
|
150
|
+
User / AI says: "there is a fire in building B"
|
|
151
151
|
│
|
|
152
152
|
DoSync Hub
|
|
153
153
|
│
|
|
154
154
|
┌───────────────┼───────────────┐
|
|
155
155
|
▼ ▼ ▼
|
|
156
|
-
💡
|
|
157
|
-
at maximum
|
|
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
|
|
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
|
|
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 |
|
|
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
|
|
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.
|
|
17
|
-
__protocol_version__ = "0.
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
55
|
+
Each adapter translates DoSync actions into the device's native protocol
|
|
56
|
+
(UDP, HTTP, BLE, etc.).
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
To implement a new adapter:
|
|
59
59
|
|
|
60
60
|
class MyBrandAdapter(DoSyncAdapter):
|
|
61
61
|
async def execute(self, action, urgency):
|
|
62
|
-
#
|
|
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 —
|
|
177
|
+
# ── AdapterExecutor — the central executor ────────────────────────────────────
|
|
178
178
|
|
|
179
179
|
class AdapterExecutor:
|
|
180
180
|
"""
|
|
181
|
-
|
|
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
|
-
|
|
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:
|
|
199
|
-
fallback_to_simulated:
|
|
200
|
-
|
|
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
|
-
"""
|
|
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
|
|
273
|
-
|
|
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
|