dosync 0.4.2__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. {dosync-0.4.2 → dosync-0.5.0}/PKG-INFO +114 -39
  2. {dosync-0.4.2 → dosync-0.5.0}/README.md +111 -37
  3. {dosync-0.4.2 → dosync-0.5.0}/dosync/__init__.py +1 -1
  4. {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/__init__.py +26 -20
  5. {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/homeassistant.py +27 -21
  6. {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/matter.py +10 -10
  7. dosync-0.5.0/dosync/adapters/notifications.py +241 -0
  8. {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/shelly.py +11 -11
  9. {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/wiz.py +24 -20
  10. {dosync-0.4.2 → dosync-0.5.0}/dosync/auth.py +17 -17
  11. {dosync-0.4.2 → dosync-0.5.0}/dosync/certify.py +2 -2
  12. {dosync-0.4.2 → dosync-0.5.0}/dosync/cli.py +18 -0
  13. {dosync-0.4.2 → dosync-0.5.0}/dosync/dashboard.html +50 -11
  14. {dosync-0.4.2 → dosync-0.5.0}/dosync/db.py +34 -34
  15. {dosync-0.4.2 → dosync-0.5.0}/dosync/declarative.py +2 -1
  16. dosync-0.5.0/dosync/discoverers.py +99 -0
  17. dosync-0.5.0/dosync/discoverers_mdns.py +181 -0
  18. dosync-0.5.0/dosync/discoverers_ssdp.py +294 -0
  19. {dosync-0.4.2 → dosync-0.5.0}/dosync/discovery.py +25 -13
  20. {dosync-0.4.2 → dosync-0.5.0}/dosync/executor.py +15 -3
  21. {dosync-0.4.2 → dosync-0.5.0}/dosync/hub.py +216 -23
  22. {dosync-0.4.2 → dosync-0.5.0}/dosync/manage.py +1 -1
  23. {dosync-0.4.2 → dosync-0.5.0}/dosync/mcp_server.py +74 -65
  24. {dosync-0.4.2 → dosync-0.5.0}/dosync/models.py +33 -5
  25. {dosync-0.4.2 → dosync-0.5.0}/dosync/operation_guards.py +1 -1
  26. dosync-0.5.0/dosync/paths.py +224 -0
  27. {dosync-0.4.2 → dosync-0.5.0}/dosync/policy_config.py +3 -1
  28. {dosync-0.4.2 → dosync-0.5.0}/dosync/security.py +33 -32
  29. {dosync-0.4.2 → dosync-0.5.0}/dosync/server.py +226 -35
  30. {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/PKG-INFO +114 -39
  31. {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/SOURCES.txt +16 -0
  32. {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/requires.txt +2 -1
  33. {dosync-0.4.2 → dosync-0.5.0}/pyproject.toml +13 -1
  34. {dosync-0.4.2 → dosync-0.5.0}/tests/test_adapters.py +2 -2
  35. dosync-0.5.0/tests/test_dashboard_tells_the_truth.py +68 -0
  36. {dosync-0.4.2 → dosync-0.5.0}/tests/test_deployment_env_contract.py +21 -0
  37. dosync-0.5.0/tests/test_deployment_layout.py +221 -0
  38. dosync-0.5.0/tests/test_env_loading_is_explicit.py +68 -0
  39. dosync-0.5.0/tests/test_evaluation_metrics.py +157 -0
  40. {dosync-0.4.2 → dosync-0.5.0}/tests/test_explain_consistency.py +34 -3
  41. dosync-0.5.0/tests/test_explain_resolve_parity.py +232 -0
  42. {dosync-0.4.2 → dosync-0.5.0}/tests/test_ha_bridge_hygiene.py +1 -1
  43. dosync-0.5.0/tests/test_no_operator_data.py +265 -0
  44. dosync-0.5.0/tests/test_notification_templates.py +82 -0
  45. {dosync-0.4.2 → dosync-0.5.0}/tests/test_policy_config.py +7 -1
  46. dosync-0.5.0/tests/test_public_claims.py +96 -0
  47. dosync-0.5.0/tests/test_quickstart_is_runnable.py +142 -0
  48. {dosync-0.4.2 → dosync-0.5.0}/tests/test_recall_benchmark_postpolicy.py +6 -6
  49. {dosync-0.4.2 → dosync-0.5.0}/tests/test_resolver_scoring.py +15 -1
  50. {dosync-0.4.2 → dosync-0.5.0}/tests/test_sensor_kind.py +5 -5
  51. {dosync-0.4.2 → dosync-0.5.0}/tests/test_server.py +42 -0
  52. dosync-0.5.0/tests/test_simulation_is_declared.py +295 -0
  53. dosync-0.5.0/tests/test_transport_discoverers.py +468 -0
  54. dosync-0.5.0/tests/test_universal_intent_contract.py +202 -0
  55. {dosync-0.4.2 → dosync-0.5.0}/tests/test_validation_integration.py +5 -1
  56. dosync-0.4.2/dosync/adapters/notifications.py +0 -166
  57. {dosync-0.4.2 → dosync-0.5.0}/LICENSE +0 -0
  58. {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/ble.py +0 -0
  59. {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/declarative.py +0 -0
  60. {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/mavlink.py +0 -0
  61. {dosync-0.4.2 → dosync-0.5.0}/dosync/adapters/mqtt.py +0 -0
  62. {dosync-0.4.2 → dosync-0.5.0}/dosync/audit_backup.py +0 -0
  63. {dosync-0.4.2 → dosync-0.5.0}/dosync/auth_fastapi.py +0 -0
  64. {dosync-0.4.2 → dosync-0.5.0}/dosync/cert_signing.py +0 -0
  65. {dosync-0.4.2 → dosync-0.5.0}/dosync/composite_operations.py +0 -0
  66. {dosync-0.4.2 → dosync-0.5.0}/dosync/config_reference.py +0 -0
  67. {dosync-0.4.2 → dosync-0.5.0}/dosync/device_arbiter.py +0 -0
  68. {dosync-0.4.2 → dosync-0.5.0}/dosync/ed25519_pure.py +0 -0
  69. {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/__init__.py +0 -0
  70. {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/3d-printer.yaml +0 -0
  71. {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/air-conditioner.yaml +0 -0
  72. {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/building-lighting.json +0 -0
  73. {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/industrial-conveyor.yaml +0 -0
  74. {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/light-generic.yaml +0 -0
  75. {dosync-0.4.2 → dosync-0.5.0}/dosync/examples/declarative/television.yaml +0 -0
  76. {dosync-0.4.2 → dosync-0.5.0}/dosync/geo.py +0 -0
  77. {dosync-0.4.2 → dosync-0.5.0}/dosync/hub_monitor.py +0 -0
  78. {dosync-0.4.2 → dosync-0.5.0}/dosync/lightweight.py +0 -0
  79. {dosync-0.4.2 → dosync-0.5.0}/dosync/metrics.py +0 -0
  80. {dosync-0.4.2 → dosync-0.5.0}/dosync/operation_supervisor.py +0 -0
  81. {dosync-0.4.2 → dosync-0.5.0}/dosync/operations.py +0 -0
  82. {dosync-0.4.2 → dosync-0.5.0}/dosync/plugins.py +0 -0
  83. {dosync-0.4.2 → dosync-0.5.0}/dosync/policies.py +0 -0
  84. {dosync-0.4.2 → dosync-0.5.0}/dosync/py.typed +0 -0
  85. {dosync-0.4.2 → dosync-0.5.0}/dosync/reconciler.py +0 -0
  86. {dosync-0.4.2 → dosync-0.5.0}/dosync/route_composer.py +0 -0
  87. {dosync-0.4.2 → dosync-0.5.0}/dosync/spec_coverage.py +0 -0
  88. {dosync-0.4.2 → dosync-0.5.0}/dosync/validation.py +0 -0
  89. {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/dependency_links.txt +0 -0
  90. {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/entry_points.txt +0 -0
  91. {dosync-0.4.2 → dosync-0.5.0}/dosync.egg-info/top_level.txt +0 -0
  92. {dosync-0.4.2 → dosync-0.5.0}/setup.cfg +0 -0
  93. {dosync-0.4.2 → dosync-0.5.0}/tests/test_audit_archive.py +0 -0
  94. {dosync-0.4.2 → dosync-0.5.0}/tests/test_audit_backup.py +0 -0
  95. {dosync-0.4.2 → dosync-0.5.0}/tests/test_audit_chain_integrity.py +0 -0
  96. {dosync-0.4.2 → dosync-0.5.0}/tests/test_audit_provenance.py +0 -0
  97. {dosync-0.4.2 → dosync-0.5.0}/tests/test_auth.py +0 -0
  98. {dosync-0.4.2 → dosync-0.5.0}/tests/test_ble_adapter.py +0 -0
  99. {dosync-0.4.2 → dosync-0.5.0}/tests/test_certification_honesty.py +0 -0
  100. {dosync-0.4.2 → dosync-0.5.0}/tests/test_claim_state_machine.py +0 -0
  101. {dosync-0.4.2 → dosync-0.5.0}/tests/test_composite_operations.py +0 -0
  102. {dosync-0.4.2 → dosync-0.5.0}/tests/test_composite_orchestration.py +0 -0
  103. {dosync-0.4.2 → dosync-0.5.0}/tests/test_composition_kind_db.py +0 -0
  104. {dosync-0.4.2 → dosync-0.5.0}/tests/test_composition_kind_endpoint.py +0 -0
  105. {dosync-0.4.2 → dosync-0.5.0}/tests/test_composition_routing.py +0 -0
  106. {dosync-0.4.2 → dosync-0.5.0}/tests/test_db.py +0 -0
  107. {dosync-0.4.2 → dosync-0.5.0}/tests/test_declarative_adapters.py +0 -0
  108. {dosync-0.4.2 → dosync-0.5.0}/tests/test_declarative_quarantine.py +0 -0
  109. {dosync-0.4.2 → dosync-0.5.0}/tests/test_device_health.py +0 -0
  110. {dosync-0.4.2 → dosync-0.5.0}/tests/test_device_heartbeat.py +0 -0
  111. {dosync-0.4.2 → dosync-0.5.0}/tests/test_direct_action_governance.py +0 -0
  112. {dosync-0.4.2 → dosync-0.5.0}/tests/test_discovery_adoption.py +0 -0
  113. {dosync-0.4.2 → dosync-0.5.0}/tests/test_drone_policies.py +0 -0
  114. {dosync-0.4.2 → dosync-0.5.0}/tests/test_ed25519_pure.py +0 -0
  115. {dosync-0.4.2 → dosync-0.5.0}/tests/test_emergency_preemption.py +0 -0
  116. {dosync-0.4.2 → dosync-0.5.0}/tests/test_event_loop_migration.py +0 -0
  117. {dosync-0.4.2 → dosync-0.5.0}/tests/test_geo.py +0 -0
  118. {dosync-0.4.2 → dosync-0.5.0}/tests/test_hub_monitor.py +0 -0
  119. {dosync-0.4.2 → dosync-0.5.0}/tests/test_idempotency.py +0 -0
  120. {dosync-0.4.2 → dosync-0.5.0}/tests/test_independent_observation.py +0 -0
  121. {dosync-0.4.2 → dosync-0.5.0}/tests/test_integration_suite.py +0 -0
  122. {dosync-0.4.2 → dosync-0.5.0}/tests/test_lightweight_heartbeat.py +0 -0
  123. {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_adapter.py +0 -0
  124. {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_channels.py +0 -0
  125. {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_listener.py +0 -0
  126. {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_return_home.py +0 -0
  127. {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_single_reader.py +0 -0
  128. {dosync-0.4.2 → dosync-0.5.0}/tests/test_mavlink_telemetry_closure.py +0 -0
  129. {dosync-0.4.2 → dosync-0.5.0}/tests/test_mcp_dynamic_intents.py +0 -0
  130. {dosync-0.4.2 → dosync-0.5.0}/tests/test_mcp_partial_progress.py +0 -0
  131. {dosync-0.4.2 → dosync-0.5.0}/tests/test_metrics.py +0 -0
  132. {dosync-0.4.2 → dosync-0.5.0}/tests/test_models.py +0 -0
  133. {dosync-0.4.2 → dosync-0.5.0}/tests/test_multihub_endpoints.py +0 -0
  134. {dosync-0.4.2 → dosync-0.5.0}/tests/test_operation_guards.py +0 -0
  135. {dosync-0.4.2 → dosync-0.5.0}/tests/test_operation_supervisor.py +0 -0
  136. {dosync-0.4.2 → dosync-0.5.0}/tests/test_operations.py +0 -0
  137. {dosync-0.4.2 → dosync-0.5.0}/tests/test_operations_endpoints.py +0 -0
  138. {dosync-0.4.2 → dosync-0.5.0}/tests/test_operations_persistence.py +0 -0
  139. {dosync-0.4.2 → dosync-0.5.0}/tests/test_operations_wiring.py +0 -0
  140. {dosync-0.4.2 → dosync-0.5.0}/tests/test_panel_polish_2026_07_21.py +0 -0
  141. {dosync-0.4.2 → dosync-0.5.0}/tests/test_policies.py +0 -0
  142. {dosync-0.4.2 → dosync-0.5.0}/tests/test_pushed_verification.py +0 -0
  143. {dosync-0.4.2 → dosync-0.5.0}/tests/test_reachability_cause.py +0 -0
  144. {dosync-0.4.2 → dosync-0.5.0}/tests/test_reconciler.py +0 -0
  145. {dosync-0.4.2 → dosync-0.5.0}/tests/test_resolution_wiring.py +0 -0
  146. {dosync-0.4.2 → dosync-0.5.0}/tests/test_resolver_semantics.py +0 -0
  147. {dosync-0.4.2 → dosync-0.5.0}/tests/test_route_composer.py +0 -0
  148. {dosync-0.4.2 → dosync-0.5.0}/tests/test_telemetry_bridge.py +0 -0
  149. {dosync-0.4.2 → dosync-0.5.0}/tests/test_third_party_adapters.py +0 -0
  150. {dosync-0.4.2 → dosync-0.5.0}/tests/test_validation.py +0 -0
  151. {dosync-0.4.2 → dosync-0.5.0}/tests/test_wiring_audit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dosync
3
- Version: 0.4.2
3
+ Version: 0.5.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
@@ -24,10 +24,11 @@ Requires-Python: >=3.10
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: fastapi<1.0,>=0.115.0
27
- Requires-Dist: uvicorn<1.0,>=0.23.0
27
+ Requires-Dist: uvicorn[standard]<1.0,>=0.23.0
28
28
  Requires-Dist: pydantic<3.0,>=2.7.0
29
29
  Requires-Dist: jsonschema<5.0,>=4.18
30
30
  Requires-Dist: bleak>=0.21
31
+ Requires-Dist: zeroconf>=0.130
31
32
  Requires-Dist: pyyaml>=6.0.1
32
33
  Requires-Dist: aiohttp>=3.9.0
33
34
  Requires-Dist: paho-mqtt>=2.0.0
@@ -210,7 +211,7 @@ DoSync earns its place in specific situations — and honestly gets in the way i
210
211
  ```
211
212
  User / AI says: "there is an emergency at home"
212
213
  │
213
- DoSync Hub v0.3.0
214
+ DoSync Hub
214
215
  │
215
216
  ┌───────────────┼───────────────┐
216
217
  ▼ ▼ ▼
@@ -257,6 +258,50 @@ pipx install dosync # recommended
257
258
  dosync-hub
258
259
  ```
259
260
 
261
+ <details>
262
+ <summary><b>On Windows</b> — pipx is not installed with Python there</summary>
263
+
264
+ Every step below was found by running it on a clean Windows machine. None of it
265
+ is exotic; it is simply what Windows needs and what this page used to omit.
266
+
267
+ ```powershell
268
+ python -m pip install --user pipx
269
+ python -m pipx ensurepath
270
+ ```
271
+
272
+ **Close PowerShell and open it again.** `ensurepath` edits your PATH and the
273
+ session you are in cannot see the change. Then:
274
+
275
+ ```powershell
276
+ pipx install dosync
277
+ dosync-hub
278
+ ```
279
+
280
+ Two things the rest of this page assumes that PowerShell does not provide:
281
+ `export VAR=value` is `$env:VAR = "value"`, and `curl` is an alias for
282
+ `Invoke-WebRequest`, which is a different program with a different syntax — use
283
+ `curl.exe`, or the PowerShell examples further down. `setup_pki.sh` is a shell
284
+ script and needs Git Bash or WSL.
285
+
286
+ </details>
287
+
288
+ ### Then open the dashboard
289
+
290
+ ```
291
+ http://localhost:47200
292
+ ```
293
+
294
+ Paste the API key the hub printed on startup, press **Scan**, and the hub
295
+ searches every transport it can reach — WiFi broadcast, mDNS, SSDP, Bluetooth —
296
+ and shows what answered. Name anything you want to keep and it is adopted.
297
+
298
+ **Start here.** Discovering and adopting a device needs no terminal and no
299
+ JSON: a 3D printer, a television and a Bluetooth sensor were adopted this way
300
+ on the reference deployment without a line being typed. The API calls below are
301
+ for building against DoSync, not for setting it up — they came first on this
302
+ page for a long time, which told people who do not write code that the project
303
+ was not for them, while a button that did the same job sat one section lower.
304
+
260
305
  <details>
261
306
  <summary><b>"error: externally-managed-environment"?</b> — Raspberry Pi OS, Debian 12+, Ubuntu 23.04+</summary>
262
307
 
@@ -294,11 +339,28 @@ That is a working hub on `http://127.0.0.1:47200`. It starts with a simulated
294
339
  executor, so you can drive the whole protocol — register devices, fire intents,
295
340
  read the audit chain — before you own a single smart device.
296
341
 
342
+ ### Or drive it from the API
343
+
344
+ Everything the dashboard does is an HTTP call, and this is the part to read if
345
+ you are building against DoSync rather than setting it up.
346
+
347
+ The hub prints an API key on that first start and stores only a hash of it, so
348
+ it is shown once. Save it — every command below needs it:
349
+
350
+ ```bash
351
+ export DOSYNC_TOKEN=<the key printed on first start>
352
+ ```
353
+
354
+ ```powershell
355
+ $env:DOSYNC_TOKEN = "<the key printed on first start>"
356
+ ```
357
+
297
358
  Register something and give it a goal:
298
359
 
299
360
  ```bash
300
361
  # 1. A device declares what it CAN DO (not what commands it takes)
301
362
  curl -X POST http://127.0.0.1:47200/v1/devices/register \
363
+ -H "Authorization: Bearer $DOSYNC_TOKEN" \
302
364
  -H 'Content-Type: application/json' -d '{
303
365
  "device_id": "siren-hall", "device_name": "Hall Siren",
304
366
  "manufacturer": "acme", "model": "S1", "firmware": "1.0",
@@ -309,14 +371,50 @@ curl -X POST http://127.0.0.1:47200/v1/devices/register \
309
371
 
310
372
  # 2. An AI expresses a GOAL — not a command, and it names no device
311
373
  curl -X POST http://127.0.0.1:47200/v1/intent/async \
374
+ -H "Authorization: Bearer $DOSYNC_TOKEN" \
312
375
  -H 'Content-Type: application/json' \
313
376
  -d '{"intent": "ensure_safety", "urgency": "emergency", "context": {}}'
314
377
 
315
378
  # 3. Ask WHY those devices were chosen
316
- curl http://127.0.0.1:47200/v1/intents/ensure_safety/explain
379
+ curl http://127.0.0.1:47200/v1/intents/ensure_safety/explain \
380
+ -H "Authorization: Bearer $DOSYNC_TOKEN"
317
381
 
318
382
  # 4. Read the tamper-evident record of what happened
319
- curl http://127.0.0.1:47200/v1/audit?limit=10
383
+ curl "http://127.0.0.1:47200/v1/audit?limit=10" \
384
+ -H "Authorization: Bearer $DOSYNC_TOKEN"
385
+ ```
386
+
387
+ <details>
388
+ <summary><b>The same calls in PowerShell</b></summary>
389
+
390
+ `curl` in PowerShell is an alias for `Invoke-WebRequest`, and quoting JSON for
391
+ `curl.exe` is impractical — a clean Windows machine following the commands above
392
+ verbatim gets `JSON decode error`, because PowerShell passes the escaped quotes
393
+ through literally. Build the body as an object instead:
394
+
395
+ ```powershell
396
+ $body = @{
397
+ device_id = "siren-hall"; device_name = "Hall Siren"
398
+ manufacturer = "acme"; model = "S1"; firmware = "1.0"
399
+ category = "actuator"; tags = @("alarm","emergency")
400
+ emergency_capable = $true; cert_tier = "basic"
401
+ capabilities = @{ sensors = @(); actuators = @(@{ id="alarm"; type="alarm"; description="sound" }) }
402
+ } | ConvertTo-Json -Depth 6
403
+
404
+ $headers = @{ Authorization = "Bearer $env:DOSYNC_TOKEN" }
405
+
406
+ Invoke-RestMethod -Method Post -Uri http://127.0.0.1:47200/v1/devices/register `
407
+ -Headers $headers -ContentType "application/json" -Body $body
408
+
409
+ Invoke-RestMethod -Method Post -Uri http://127.0.0.1:47200/v1/intent/async `
410
+ -Headers $headers -ContentType "application/json" `
411
+ -Body (@{ intent = "ensure_safety"; urgency = "emergency"; context = @{} } | ConvertTo-Json)
412
+
413
+ Invoke-RestMethod -Uri http://127.0.0.1:47200/v1/intents/ensure_safety/explain -Headers $headers
414
+ Invoke-RestMethod -Uri "http://127.0.0.1:47200/v1/audit?limit=10" -Headers $headers
415
+ ```
416
+
417
+ </details>
320
418
  ```
321
419
 
322
420
  Step 3 is the one worth pausing on: the hub tells you which devices it
@@ -338,7 +436,13 @@ Matter, BLE, MAVLink, and the Home Assistant bridge, which is the widest door of
338
436
  all: anything HA already integrates, DoSync can reach. **Reference** adapters
339
437
  (WiZ, Shelly) implement one vendor's product and ship as worked examples of how
340
438
  an adapter is written — not as endorsement, partnership, or a promise to track
341
- anyone's firmware. **Infrastructure** is notifications.
439
+ anyone's firmware. **They register only when their vendor library is installed,
440
+ and say nothing when it is not.** A hub whose operator owns nothing from that
441
+ vendor should never be told to install anything: the protocol does not presume
442
+ your hardware, and a reference adapter that nagged for `pip install pywizlight`
443
+ on every start was presuming it. If you do register a device that names an
444
+ adapter the hub cannot load, the startup check names that device and tells you
445
+ its actions will be simulated. **Infrastructure** is notifications.
342
446
 
343
447
  **If your device is not covered, describe it in a file.** A declarative adapter
344
448
  is YAML or JSON — no code, no release of DoSync to wait for:
@@ -419,35 +523,6 @@ pip install 'dosync[mqtt]' # MQTT devices
419
523
  pip install 'dosync[all]' # everything
420
524
  ```
421
525
 
422
- ### Access: token, your own password, or none
423
-
424
- The hub prints an API token the first time it starts and stores only a hash of
425
- it, so it cannot show you that one again. Three ways to deal with that,
426
- depending on who you are:
427
-
428
- ```bash
429
- # 1. Choose your own, like a password — for a person who has to type it
430
- dosync-manage keys create --token "my-house-2026-kitchen" --label dashboard
431
-
432
- # 2. Let it generate one — for a program that will store it
433
- dosync-manage keys create --label my-integration
434
-
435
- # 3. Turn authentication off entirely
436
- DOSYNC_AUTH=false dosync-hub
437
- ```
438
-
439
- **Option 3 is legitimate and not a trap door.** On a home network, behind a
440
- router, with no port forwarding, requiring a token protects against nobody who
441
- is not already inside your house. It is the wrong default for a clinic and a
442
- reasonable choice for a workshop, so DoSync provides it plainly instead of
443
- pretending everyone has the same threat model. What it is not suitable for is
444
- any hub reachable from outside its own network.
445
-
446
- Tokens are checked without rate limiting or lockout, so a chosen one must be at
447
- least 12 characters and a passphrase of several words is better than a short
448
- clever string. Existing keys: `dosync-manage keys list` (previews only — they
449
- are hashed), `dosync-manage keys revoke <preview>`, `dosync-manage keys reset`.
450
-
451
526
  ### Access: a password you choose, or none at all
452
527
 
453
528
  The hub prints a token on first start and stores only a hash of it, so it cannot
@@ -575,7 +650,7 @@ git clone https://github.com/giulianireg-spec/dosync-protocol
575
650
  cd dosync-protocol
576
651
  python3 -m venv venv && source venv/bin/activate
577
652
  pip install -e '.[dev]'
578
- pytest # 667 tests
653
+ pytest # runs the full suite
579
654
  dosync-hub --reload
580
655
  ```
581
656
 
@@ -585,7 +660,7 @@ dosync-hub --reload
585
660
 
586
661
  | Component | Status |
587
662
  |---|---|
588
- | REST API (12+ endpoints) | ✅ |
663
+ | REST API (40+ endpoints) | ✅ |
589
664
  | WebSocket real-time events | ✅ |
590
665
  | Web dashboard | ✅ |
591
666
  | API key authentication + SHA-256 audit log | ✅ |
@@ -644,7 +719,7 @@ python3 certify.py --host <hub-ip> --port 47200 --tier standard
644
719
  | Language | Location | Author | Certification |
645
720
  |---|---|---|---|
646
721
  | Python (reference) | `server.py` | this project | Standard 33/33 · Emergency 44/44 ✅ |
647
- | Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | 33/33 Standard ✅ |
722
+ | Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | Standard 33/33, against the v0.3 suite — re-validation against the current 56-test suite pending |
648
723
 
649
724
  The Node.js implementation is a **companion** port that validates the protocol
650
725
  is implementable in a second language against the same certification suite —
@@ -692,4 +767,4 @@ Apache 2.0 — free to implement, free to extend, no royalties.
692
767
 
693
768
  ---
694
769
 
695
- *DoSync Protocol v0.3.0 · © 2026 Rodrigo Giuliani · rgiuliani@dosync.dev*
770
+ *DoSync Protocol · © 2026 Rodrigo Giuliani · rgiuliani@dosync.dev*
@@ -149,7 +149,7 @@ DoSync earns its place in specific situations — and honestly gets in the way i
149
149
  ```
150
150
  User / AI says: "there is an emergency at home"
151
151
  │
152
- DoSync Hub v0.3.0
152
+ DoSync Hub
153
153
  │
154
154
  ┌───────────────┼───────────────┐
155
155
  ▼ ▼ ▼
@@ -196,6 +196,50 @@ pipx install dosync # recommended
196
196
  dosync-hub
197
197
  ```
198
198
 
199
+ <details>
200
+ <summary><b>On Windows</b> — pipx is not installed with Python there</summary>
201
+
202
+ Every step below was found by running it on a clean Windows machine. None of it
203
+ is exotic; it is simply what Windows needs and what this page used to omit.
204
+
205
+ ```powershell
206
+ python -m pip install --user pipx
207
+ python -m pipx ensurepath
208
+ ```
209
+
210
+ **Close PowerShell and open it again.** `ensurepath` edits your PATH and the
211
+ session you are in cannot see the change. Then:
212
+
213
+ ```powershell
214
+ pipx install dosync
215
+ dosync-hub
216
+ ```
217
+
218
+ Two things the rest of this page assumes that PowerShell does not provide:
219
+ `export VAR=value` is `$env:VAR = "value"`, and `curl` is an alias for
220
+ `Invoke-WebRequest`, which is a different program with a different syntax — use
221
+ `curl.exe`, or the PowerShell examples further down. `setup_pki.sh` is a shell
222
+ script and needs Git Bash or WSL.
223
+
224
+ </details>
225
+
226
+ ### Then open the dashboard
227
+
228
+ ```
229
+ http://localhost:47200
230
+ ```
231
+
232
+ Paste the API key the hub printed on startup, press **Scan**, and the hub
233
+ searches every transport it can reach — WiFi broadcast, mDNS, SSDP, Bluetooth —
234
+ and shows what answered. Name anything you want to keep and it is adopted.
235
+
236
+ **Start here.** Discovering and adopting a device needs no terminal and no
237
+ JSON: a 3D printer, a television and a Bluetooth sensor were adopted this way
238
+ on the reference deployment without a line being typed. The API calls below are
239
+ for building against DoSync, not for setting it up — they came first on this
240
+ page for a long time, which told people who do not write code that the project
241
+ was not for them, while a button that did the same job sat one section lower.
242
+
199
243
  <details>
200
244
  <summary><b>"error: externally-managed-environment"?</b> — Raspberry Pi OS, Debian 12+, Ubuntu 23.04+</summary>
201
245
 
@@ -233,11 +277,28 @@ That is a working hub on `http://127.0.0.1:47200`. It starts with a simulated
233
277
  executor, so you can drive the whole protocol — register devices, fire intents,
234
278
  read the audit chain — before you own a single smart device.
235
279
 
280
+ ### Or drive it from the API
281
+
282
+ Everything the dashboard does is an HTTP call, and this is the part to read if
283
+ you are building against DoSync rather than setting it up.
284
+
285
+ The hub prints an API key on that first start and stores only a hash of it, so
286
+ it is shown once. Save it — every command below needs it:
287
+
288
+ ```bash
289
+ export DOSYNC_TOKEN=<the key printed on first start>
290
+ ```
291
+
292
+ ```powershell
293
+ $env:DOSYNC_TOKEN = "<the key printed on first start>"
294
+ ```
295
+
236
296
  Register something and give it a goal:
237
297
 
238
298
  ```bash
239
299
  # 1. A device declares what it CAN DO (not what commands it takes)
240
300
  curl -X POST http://127.0.0.1:47200/v1/devices/register \
301
+ -H "Authorization: Bearer $DOSYNC_TOKEN" \
241
302
  -H 'Content-Type: application/json' -d '{
242
303
  "device_id": "siren-hall", "device_name": "Hall Siren",
243
304
  "manufacturer": "acme", "model": "S1", "firmware": "1.0",
@@ -248,14 +309,50 @@ curl -X POST http://127.0.0.1:47200/v1/devices/register \
248
309
 
249
310
  # 2. An AI expresses a GOAL — not a command, and it names no device
250
311
  curl -X POST http://127.0.0.1:47200/v1/intent/async \
312
+ -H "Authorization: Bearer $DOSYNC_TOKEN" \
251
313
  -H 'Content-Type: application/json' \
252
314
  -d '{"intent": "ensure_safety", "urgency": "emergency", "context": {}}'
253
315
 
254
316
  # 3. Ask WHY those devices were chosen
255
- curl http://127.0.0.1:47200/v1/intents/ensure_safety/explain
317
+ curl http://127.0.0.1:47200/v1/intents/ensure_safety/explain \
318
+ -H "Authorization: Bearer $DOSYNC_TOKEN"
256
319
 
257
320
  # 4. Read the tamper-evident record of what happened
258
- curl http://127.0.0.1:47200/v1/audit?limit=10
321
+ curl "http://127.0.0.1:47200/v1/audit?limit=10" \
322
+ -H "Authorization: Bearer $DOSYNC_TOKEN"
323
+ ```
324
+
325
+ <details>
326
+ <summary><b>The same calls in PowerShell</b></summary>
327
+
328
+ `curl` in PowerShell is an alias for `Invoke-WebRequest`, and quoting JSON for
329
+ `curl.exe` is impractical — a clean Windows machine following the commands above
330
+ verbatim gets `JSON decode error`, because PowerShell passes the escaped quotes
331
+ through literally. Build the body as an object instead:
332
+
333
+ ```powershell
334
+ $body = @{
335
+ device_id = "siren-hall"; device_name = "Hall Siren"
336
+ manufacturer = "acme"; model = "S1"; firmware = "1.0"
337
+ category = "actuator"; tags = @("alarm","emergency")
338
+ emergency_capable = $true; cert_tier = "basic"
339
+ capabilities = @{ sensors = @(); actuators = @(@{ id="alarm"; type="alarm"; description="sound" }) }
340
+ } | ConvertTo-Json -Depth 6
341
+
342
+ $headers = @{ Authorization = "Bearer $env:DOSYNC_TOKEN" }
343
+
344
+ Invoke-RestMethod -Method Post -Uri http://127.0.0.1:47200/v1/devices/register `
345
+ -Headers $headers -ContentType "application/json" -Body $body
346
+
347
+ Invoke-RestMethod -Method Post -Uri http://127.0.0.1:47200/v1/intent/async `
348
+ -Headers $headers -ContentType "application/json" `
349
+ -Body (@{ intent = "ensure_safety"; urgency = "emergency"; context = @{} } | ConvertTo-Json)
350
+
351
+ Invoke-RestMethod -Uri http://127.0.0.1:47200/v1/intents/ensure_safety/explain -Headers $headers
352
+ Invoke-RestMethod -Uri "http://127.0.0.1:47200/v1/audit?limit=10" -Headers $headers
353
+ ```
354
+
355
+ </details>
259
356
  ```
260
357
 
261
358
  Step 3 is the one worth pausing on: the hub tells you which devices it
@@ -277,7 +374,13 @@ Matter, BLE, MAVLink, and the Home Assistant bridge, which is the widest door of
277
374
  all: anything HA already integrates, DoSync can reach. **Reference** adapters
278
375
  (WiZ, Shelly) implement one vendor's product and ship as worked examples of how
279
376
  an adapter is written — not as endorsement, partnership, or a promise to track
280
- anyone's firmware. **Infrastructure** is notifications.
377
+ anyone's firmware. **They register only when their vendor library is installed,
378
+ and say nothing when it is not.** A hub whose operator owns nothing from that
379
+ vendor should never be told to install anything: the protocol does not presume
380
+ your hardware, and a reference adapter that nagged for `pip install pywizlight`
381
+ on every start was presuming it. If you do register a device that names an
382
+ adapter the hub cannot load, the startup check names that device and tells you
383
+ its actions will be simulated. **Infrastructure** is notifications.
281
384
 
282
385
  **If your device is not covered, describe it in a file.** A declarative adapter
283
386
  is YAML or JSON — no code, no release of DoSync to wait for:
@@ -358,35 +461,6 @@ pip install 'dosync[mqtt]' # MQTT devices
358
461
  pip install 'dosync[all]' # everything
359
462
  ```
360
463
 
361
- ### Access: token, your own password, or none
362
-
363
- The hub prints an API token the first time it starts and stores only a hash of
364
- it, so it cannot show you that one again. Three ways to deal with that,
365
- depending on who you are:
366
-
367
- ```bash
368
- # 1. Choose your own, like a password — for a person who has to type it
369
- dosync-manage keys create --token "my-house-2026-kitchen" --label dashboard
370
-
371
- # 2. Let it generate one — for a program that will store it
372
- dosync-manage keys create --label my-integration
373
-
374
- # 3. Turn authentication off entirely
375
- DOSYNC_AUTH=false dosync-hub
376
- ```
377
-
378
- **Option 3 is legitimate and not a trap door.** On a home network, behind a
379
- router, with no port forwarding, requiring a token protects against nobody who
380
- is not already inside your house. It is the wrong default for a clinic and a
381
- reasonable choice for a workshop, so DoSync provides it plainly instead of
382
- pretending everyone has the same threat model. What it is not suitable for is
383
- any hub reachable from outside its own network.
384
-
385
- Tokens are checked without rate limiting or lockout, so a chosen one must be at
386
- least 12 characters and a passphrase of several words is better than a short
387
- clever string. Existing keys: `dosync-manage keys list` (previews only — they
388
- are hashed), `dosync-manage keys revoke <preview>`, `dosync-manage keys reset`.
389
-
390
464
  ### Access: a password you choose, or none at all
391
465
 
392
466
  The hub prints a token on first start and stores only a hash of it, so it cannot
@@ -514,7 +588,7 @@ git clone https://github.com/giulianireg-spec/dosync-protocol
514
588
  cd dosync-protocol
515
589
  python3 -m venv venv && source venv/bin/activate
516
590
  pip install -e '.[dev]'
517
- pytest # 667 tests
591
+ pytest # runs the full suite
518
592
  dosync-hub --reload
519
593
  ```
520
594
 
@@ -524,7 +598,7 @@ dosync-hub --reload
524
598
 
525
599
  | Component | Status |
526
600
  |---|---|
527
- | REST API (12+ endpoints) | ✅ |
601
+ | REST API (40+ endpoints) | ✅ |
528
602
  | WebSocket real-time events | ✅ |
529
603
  | Web dashboard | ✅ |
530
604
  | API key authentication + SHA-256 audit log | ✅ |
@@ -583,7 +657,7 @@ python3 certify.py --host <hub-ip> --port 47200 --tier standard
583
657
  | Language | Location | Author | Certification |
584
658
  |---|---|---|---|
585
659
  | Python (reference) | `server.py` | this project | Standard 33/33 · Emergency 44/44 ✅ |
586
- | Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | 33/33 Standard ✅ |
660
+ | Node.js (companion) | [giulianireg-spec/dosync-node](https://github.com/giulianireg-spec/dosync-node) | this project | Standard 33/33, against the v0.3 suite — re-validation against the current 56-test suite pending |
587
661
 
588
662
  The Node.js implementation is a **companion** port that validates the protocol
589
663
  is implementable in a second language against the same certification suite —
@@ -631,4 +705,4 @@ Apache 2.0 — free to implement, free to extend, no royalties.
631
705
 
632
706
  ---
633
707
 
634
- *DoSync Protocol v0.3.0 · © 2026 Rodrigo Giuliani · rgiuliani@dosync.dev*
708
+ *DoSync Protocol · © 2026 Rodrigo Giuliani · rgiuliani@dosync.dev*
@@ -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.4.2"
16
+ __version__ = "0.5.0"
17
17
  __protocol_version__ = "0.4"
@@ -1,17 +1,17 @@
1
1
  """
2
2
  DoSync — Adapter Layer
3
3
  ======================
4
- Capa de traducción entre el protocolo DoSync y dispositivos físicos reales.
4
+ Translation layer between the DoSync protocol and real physical devices.
5
5
 
6
6
  Modelo:
7
7
  DoSync Hub → AdapterExecutor → [WiZAdapter | GPIOAdapter | ShellyAdapter | ...]
8
8
 
9
9
  Para agregar un nuevo dispositivo:
10
10
  1. Crear adapters/mi_marca.py implementando DoSyncAdapter
11
- 2. Registrar el dispositivo con adapter="mi_marca" en su CapabilityManifest
12
- 3. El hub lo maneja igual que cualquier otro dispositivo — sin cambios al núcleo
11
+ 2. Register the device with adapter="my_brand" in its CapabilityManifest
12
+ 3. The hub treats it like any other device — no core changes needed
13
13
 
14
- Publicación de adapters de terceros:
14
+ Publishing third-party adapters:
15
15
  pip install dosync-adapter-philipshue
16
16
  pip install dosync-adapter-shelly
17
17
  pip install dosync-adapter-matter
@@ -27,11 +27,11 @@ from ..models import ActionResult, DeviceAction, Urgency
27
27
  log = logging.getLogger("dosync.adapters")
28
28
 
29
29
 
30
- # ── Interfaz base que todo adapter debe implementar ───────────────────────────
30
+ # ── Base interface every adapter must implement ───────────────────────────────
31
31
 
32
32
  class DoSyncAdapter(ABC):
33
33
  """
34
- Interfaz base para adapters de dispositivos físicos.
34
+ Base interface for physical device adapters.
35
35
 
36
36
  Cada adapter traduce acciones DoSync al protocolo nativo
37
37
  del dispositivo (UDP, HTTP, GPIO, BLE, etc.).
@@ -160,16 +160,16 @@ class DoSyncAdapter(ABC):
160
160
  class AdapterExecutor:
161
161
  """
162
162
  Ejecutor central que delega acciones al adapter correcto
163
- según el campo 'adapter' del CapabilityManifest del dispositivo.
163
+ based on the 'adapter' field of the device's CapabilityManifest.
164
164
 
165
- Si un dispositivo no tiene adapter registrado, cae al SimulatedExecutor.
165
+ A device with no registered adapter falls back to the SimulatedExecutor.
166
166
 
167
167
  Uso:
168
168
  executor = AdapterExecutor(hub)
169
169
  executor.register(WiZAdapter())
170
170
  executor.register(GPIOAdapter())
171
171
 
172
- # El hub usa este executor en lugar del SimulatedExecutor
172
+ # The hub uses this executor instead of the SimulatedExecutor
173
173
  result = await hub.execute_intent(intent, executor)
174
174
  """
175
175
 
@@ -191,7 +191,7 @@ class AdapterExecutor:
191
191
  self._simulated = None
192
192
 
193
193
  def register(self, adapter: DoSyncAdapter) -> None:
194
- """Registra un adapter por su nombre."""
194
+ """Register an adapter under its name."""
195
195
  self._adapters[adapter.adapter_name] = adapter
196
196
  log.info("Adapter registered: %s", adapter.adapter_name)
197
197
 
@@ -206,8 +206,8 @@ class AdapterExecutor:
206
206
 
207
207
  async def execute(self, action: DeviceAction, urgency: Urgency) -> ActionResult:
208
208
  """
209
- Ejecuta una acción buscando el adapter correcto para el dispositivo.
210
- Fallback al SimulatedExecutor si el dispositivo no tiene adapter.
209
+ Execute an action, looking up the right adapter for the device.
210
+ Falls back to the SimulatedExecutor when the device has no adapter.
211
211
  """
212
212
  device = self._hub.registry.get(action.device_id)
213
213
 
@@ -250,7 +250,7 @@ class AdapterExecutor:
250
250
  result = await self._adapters[adapter_name].execute(action, urgency)
251
251
  if result.success:
252
252
  self._update_resolver_state(action)
253
- # Device Health Monitor — registrar resultado
253
+ # Device Health Monitor — record the outcome
254
254
  self._record_health(action, result)
255
255
  return result
256
256
  except Exception as e:
@@ -267,13 +267,19 @@ class AdapterExecutor:
267
267
  self._record_health(action, err_result)
268
268
  return err_result
269
269
 
270
- # Fallback
270
+ # Fallback. WARNING, not INFO, and the result says it was simulated:
271
+ # this branch is how a device that declares actuators nobody can execute
272
+ # still reports success. The reference deployment ran an SMS notifier
273
+ # here for an unknown length of time, and every log line read like an
274
+ # execution.
271
275
  if self._simulated:
272
- log.info(
273
- "No adapter for '%s' (device %s) — using SimulatedExecutor",
274
- adapter_name or "none", action.device_id,
276
+ reason = ("no_adapter_declared" if not adapter_name
277
+ else "adapter_unavailable")
278
+ log.warning(
279
+ "Simulating %s.%s — %s (adapter=%s). Nothing was sent to the device.",
280
+ action.device_id, action.action, reason, adapter_name or "none",
275
281
  )
276
- return await self._simulated.execute(action, urgency)
282
+ return await self._simulated.execute(action, urgency, reason=reason)
277
283
 
278
284
  return ActionResult(
279
285
  device_id=action.device_id,
@@ -283,7 +289,7 @@ class AdapterExecutor:
283
289
  )
284
290
 
285
291
  def _record_health(self, action: DeviceAction, result) -> None:
286
- """Registra el resultado en el Device Health Monitor."""
292
+ """Record the outcome in the Device Health Monitor."""
287
293
  try:
288
294
  db = getattr(self._hub, 'db', None)
289
295
  if db:
@@ -298,7 +304,7 @@ class AdapterExecutor:
298
304
  action.device_id, _e)
299
305
 
300
306
  def _update_resolver_state(self, action: DeviceAction) -> None:
301
- """Notifica al StateAwareResolver el nuevo estado tras una accion exitosa."""
307
+ """Tell the StateAwareResolver the new state after a successful action."""
302
308
  from ..hub import StateAwareResolver
303
309
  resolver = getattr(self._hub, 'resolver', None)
304
310
  if not isinstance(resolver, StateAwareResolver):