dosync 0.8.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.
- {dosync-0.8.0 → dosync-0.9.0}/PKG-INFO +121 -9
- {dosync-0.8.0 → dosync-0.9.0}/README.md +120 -8
- {dosync-0.8.0 → dosync-0.9.0}/dosync/__init__.py +1 -1
- dosync-0.9.0/dosync/adapters/wot.py +136 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/auth.py +8 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/certify.py +88 -9
- {dosync-0.8.0 → dosync-0.9.0}/dosync/config_reference.py +5 -1
- dosync-0.9.0/dosync/container.py +98 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/dashboard.html +413 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/db.py +11 -2
- {dosync-0.8.0 → dosync-0.9.0}/dosync/hub.py +320 -14
- dosync-0.9.0/dosync/mcp_cli.py +167 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/mcp_server.py +65 -46
- {dosync-0.8.0 → dosync-0.9.0}/dosync/models.py +18 -1
- {dosync-0.8.0 → dosync-0.9.0}/dosync/policies.py +72 -208
- {dosync-0.8.0 → dosync-0.9.0}/dosync/resolvers.py +18 -73
- dosync-0.9.0/dosync/samuel/__init__.py +52 -0
- dosync-0.9.0/dosync/samuel/confirmation.py +42 -0
- dosync-0.9.0/dosync/samuel/intake.py +136 -0
- dosync-0.9.0/dosync/samuel/oscillation.py +71 -0
- dosync-0.9.0/dosync/samuel/policing.py +178 -0
- dosync-0.9.0/dosync/samuel/proposals.py +131 -0
- dosync-0.9.0/dosync/samuel_certify.py +364 -0
- dosync-0.9.0/dosync/samuel_http.py +298 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/server.py +562 -80
- {dosync-0.8.0 → dosync-0.9.0}/dosync/spec/TAG-VOCABULARY.md +1 -0
- dosync-0.9.0/dosync/wot_export.py +193 -0
- dosync-0.9.0/dosync/wot_import.py +221 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync.egg-info/PKG-INFO +121 -9
- {dosync-0.8.0 → dosync-0.9.0}/dosync.egg-info/SOURCES.txt +30 -1
- {dosync-0.8.0 → dosync-0.9.0}/dosync.egg-info/entry_points.txt +3 -0
- {dosync-0.8.0 → dosync-0.9.0}/pyproject.toml +3 -0
- dosync-0.9.0/tests/test_agent_lab_matches_the_hub.py +280 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_capability_decides_participation.py +24 -0
- dosync-0.9.0/tests/test_configuration_credential.py +65 -0
- dosync-0.9.0/tests/test_confirmation_hold.py +161 -0
- dosync-0.9.0/tests/test_container_and_addon.py +82 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_direct_action_governance.py +11 -1
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_direct_action_instrumented.py +11 -0
- dosync-0.9.0/tests/test_direct_control_is_not_an_agent_path.py +81 -0
- dosync-0.9.0/tests/test_governance_console.py +188 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_governance_vocabulary.py +3 -2
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_governed_direct_mode.py +1 -1
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_location_restricts.py +1 -1
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mcp_speaks_english.py +1 -1
- dosync-0.9.0/tests/test_no_oscillation.py +90 -0
- dosync-0.9.0/tests/test_operate_device.py +116 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_operations_wiring.py +5 -0
- dosync-0.9.0/tests/test_operator_credential_distinct.py +101 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_opposite_actions_and_emergency_precedence.py +48 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_policies.py +21 -18
- dosync-0.9.0/tests/test_policy_failure_is_not_a_pass.py +100 -0
- dosync-0.9.0/tests/test_round4_analysis.py +97 -0
- dosync-0.9.0/tests/test_samuel_conformance.py +70 -0
- dosync-0.9.0/tests/test_samuel_every_door.py +106 -0
- dosync-0.9.0/tests/test_samuel_kernel_boundary.py +65 -0
- dosync-0.9.0/tests/test_samuel_matches_the_hub.py +76 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_thing_description_export.py +6 -3
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_universal_intent_contract.py +9 -4
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_validation_integration.py +5 -0
- dosync-0.9.0/tests/test_wot_import.py +166 -0
- dosync-0.9.0/tests/test_wot_interop.py +175 -0
- dosync-0.8.0/tests/test_agent_lab_matches_the_hub.py +0 -130
- {dosync-0.8.0 → dosync-0.9.0}/LICENSE +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/adapter_drafting.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/adapters/__init__.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/adapters/ble.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/adapters/declarative.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/adapters/homeassistant.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/adapters/matter.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/adapters/mavlink.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/adapters/mqtt.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/adapters/notifications.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/adapters/shelly.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/adapters/wiz.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/audit.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/audit_backup.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/auth_fastapi.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/cert_signing.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/cli.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/composite_executor.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/composite_operations.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/declarative.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/device_arbiter.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/discoverers.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/discoverers_mdns.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/discoverers_ssdp.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/discovery.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/ed25519_pure.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/examples/__init__.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/examples/declarative/3d-printer.yaml +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/examples/declarative/air-conditioner.yaml +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/examples/declarative/building-lighting.json +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/examples/declarative/industrial-conveyor.yaml +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/examples/declarative/light-generic.yaml +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/examples/declarative/television.yaml +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/execution.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/executor.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/geo.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/hub_monitor.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/lightweight.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/manage.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/metrics.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/occupancy.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/operation_guards.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/operation_supervisor.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/operations.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/paths.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/plan_executor.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/plugins.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/policy_config.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/py.typed +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/reconciler.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/registry.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/restore.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/route_composer.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/security.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/spec_coverage.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/state_refresh.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/telemetry.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/templates/adapter-draft-prompt.md +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync/validation.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync.egg-info/dependency_links.txt +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync.egg-info/requires.txt +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/dosync.egg-info/top_level.txt +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/setup.cfg +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_adapter_drafting.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_adapters.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_adapters_speak_the_protocol.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_audit_archive.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_audit_backup.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_audit_chain_integrity.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_audit_chain_ordering.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_audit_concurrency.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_audit_endpoint_limit.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_audit_hashless.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_audit_provenance.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_auth.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_ble_adapter.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_certification_guide.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_certification_honesty.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_check_helpers_can_fail.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_claim_state_machine.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_collaborators_read_the_live_hub.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_composite_executor.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_composite_operations.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_composite_orchestration.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_composition_kind_db.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_composition_kind_endpoint.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_composition_routing.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_dashboard_tells_the_truth.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_dashboard_uses_live_endpoints.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_db.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_db_write_serialization.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_declarative_adapters.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_declarative_location.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_declarative_quarantine.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_deployment_env_contract.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_deployment_layout.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_device_health.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_device_heartbeat.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_device_identity.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_device_location.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_discovery_adoption.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_documentation_is_navigable.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_drone_policies.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_ed25519_pure.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_emergency_actions.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_emergency_preemption.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_env_loading_is_explicit.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_evaluation_metrics.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_evaluation_tools_run.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_event_loop_migration.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_explain_consistency.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_explain_resolve_parity.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_extraction_preserves_behaviour.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_geo.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_ha_bridge_hygiene.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_ha_read_sensors.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_health_recorded_once.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_hub_monitor.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_idempotency.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_independent_observation.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_integration_suite.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_intent_rejections_visible.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_lightweight_heartbeat.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mavlink_adapter.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mavlink_channels.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mavlink_listener.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mavlink_return_home.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mavlink_single_reader.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mavlink_telemetry_closure.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mcp_dynamic_intents.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mcp_http_transport.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mcp_partial_progress.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mcp_scenarios_are_live.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_mcp_status.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_measurement_tools_are_honest.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_metrics.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_models.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_multihub_endpoints.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_no_operator_data.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_no_undefined_names.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_notification_templates.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_occupancy.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_openapi_contract.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_operation_guards.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_operation_supervisor.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_operations.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_operations_endpoints.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_operations_persistence.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_panel_polish_2026_07_21.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_plan_executor.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_policy_config.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_protocol_version.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_public_claims.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_pushed_verification.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_quickstart_is_runnable.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_reachability_cause.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_recall_benchmark_postpolicy.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_reconciler.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_registry.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_resolution_wiring.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_resolver_scoring.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_resolver_semantics.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_route_composer.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_schemas_match_the_hub.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_sensor_kind.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_server.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_server_enforces_auth.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_simulation_is_declared.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_state_refresh.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_tag_vocabulary.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_telemetry.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_telemetry_bridge.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_third_party_adapters.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_transport_discoverers.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_undefined_name_regressions.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_unknown_location_refused.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_validation.py +0 -0
- {dosync-0.8.0 → dosync-0.9.0}/tests/test_wiring_audit.py +0 -0
- {dosync-0.8.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.
|
|
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
|
|
679
|
-
|
|
680
|
-
docker
|
|
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
|
|
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
|
|
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
|
|
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
|
|
613
|
-
|
|
614
|
-
docker
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -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)
|