dosync 0.4.1__tar.gz → 0.4.2__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 (136) hide show
  1. {dosync-0.4.1 → dosync-0.4.2}/PKG-INFO +329 -6
  2. dosync-0.4.2/README.md +634 -0
  3. {dosync-0.4.1 → dosync-0.4.2}/dosync/__init__.py +1 -1
  4. {dosync-0.4.1 → dosync-0.4.2}/dosync/adapters/__init__.py +64 -0
  5. {dosync-0.4.1 → dosync-0.4.2}/dosync/adapters/ble.py +61 -0
  6. dosync-0.4.2/dosync/adapters/declarative.py +241 -0
  7. {dosync-0.4.1 → dosync-0.4.2}/dosync/adapters/notifications.py +14 -1
  8. {dosync-0.4.1 → dosync-0.4.2}/dosync/adapters/shelly.py +5 -0
  9. {dosync-0.4.1 → dosync-0.4.2}/dosync/adapters/wiz.py +5 -0
  10. {dosync-0.4.1 → dosync-0.4.2}/dosync/audit_backup.py +43 -0
  11. {dosync-0.4.1 → dosync-0.4.2}/dosync/auth.py +44 -5
  12. {dosync-0.4.1 → dosync-0.4.2}/dosync/certify.py +116 -4
  13. dosync-0.4.2/dosync/config_reference.py +149 -0
  14. dosync-0.4.2/dosync/dashboard.html +1141 -0
  15. {dosync-0.4.1 → dosync-0.4.2}/dosync/db.py +101 -0
  16. dosync-0.4.2/dosync/declarative.py +318 -0
  17. {dosync-0.4.1 → dosync-0.4.2}/dosync/device_arbiter.py +54 -5
  18. dosync-0.4.2/dosync/examples/__init__.py +7 -0
  19. dosync-0.4.2/dosync/examples/declarative/3d-printer.yaml +50 -0
  20. dosync-0.4.2/dosync/examples/declarative/air-conditioner.yaml +47 -0
  21. dosync-0.4.2/dosync/examples/declarative/building-lighting.json +52 -0
  22. dosync-0.4.2/dosync/examples/declarative/industrial-conveyor.yaml +52 -0
  23. dosync-0.4.2/dosync/examples/declarative/light-generic.yaml +50 -0
  24. dosync-0.4.2/dosync/examples/declarative/television.yaml +40 -0
  25. {dosync-0.4.1 → dosync-0.4.2}/dosync/hub.py +607 -12
  26. dosync-0.4.2/dosync/lightweight.py +156 -0
  27. {dosync-0.4.1 → dosync-0.4.2}/dosync/manage.py +246 -1
  28. {dosync-0.4.1 → dosync-0.4.2}/dosync/models.py +41 -0
  29. dosync-0.4.2/dosync/plugins.py +106 -0
  30. {dosync-0.4.1 → dosync-0.4.2}/dosync/server.py +732 -16
  31. dosync-0.4.2/dosync/spec_coverage.py +121 -0
  32. {dosync-0.4.1 → dosync-0.4.2}/dosync.egg-info/PKG-INFO +329 -6
  33. {dosync-0.4.1 → dosync-0.4.2}/dosync.egg-info/SOURCES.txt +23 -0
  34. {dosync-0.4.1 → dosync-0.4.2}/dosync.egg-info/requires.txt +6 -2
  35. {dosync-0.4.1 → dosync-0.4.2}/pyproject.toml +36 -3
  36. dosync-0.4.2/tests/test_audit_chain_integrity.py +981 -0
  37. {dosync-0.4.1 → dosync-0.4.2}/tests/test_auth.py +154 -0
  38. dosync-0.4.2/tests/test_certification_honesty.py +80 -0
  39. dosync-0.4.2/tests/test_declarative_adapters.py +305 -0
  40. dosync-0.4.2/tests/test_declarative_quarantine.py +196 -0
  41. dosync-0.4.2/tests/test_deployment_env_contract.py +570 -0
  42. dosync-0.4.2/tests/test_direct_action_governance.py +203 -0
  43. dosync-0.4.2/tests/test_discovery_adoption.py +380 -0
  44. {dosync-0.4.1 → dosync-0.4.2}/tests/test_emergency_preemption.py +123 -0
  45. dosync-0.4.2/tests/test_lightweight_heartbeat.py +238 -0
  46. dosync-0.4.2/tests/test_pushed_verification.py +182 -0
  47. dosync-0.4.2/tests/test_third_party_adapters.py +182 -0
  48. dosync-0.4.1/README.md +0 -315
  49. dosync-0.4.1/tests/test_deployment_env_contract.py +0 -115
  50. {dosync-0.4.1 → dosync-0.4.2}/LICENSE +0 -0
  51. {dosync-0.4.1 → dosync-0.4.2}/dosync/adapters/homeassistant.py +0 -0
  52. {dosync-0.4.1 → dosync-0.4.2}/dosync/adapters/matter.py +0 -0
  53. {dosync-0.4.1 → dosync-0.4.2}/dosync/adapters/mavlink.py +0 -0
  54. {dosync-0.4.1 → dosync-0.4.2}/dosync/adapters/mqtt.py +0 -0
  55. {dosync-0.4.1 → dosync-0.4.2}/dosync/auth_fastapi.py +0 -0
  56. {dosync-0.4.1 → dosync-0.4.2}/dosync/cert_signing.py +0 -0
  57. {dosync-0.4.1 → dosync-0.4.2}/dosync/cli.py +0 -0
  58. {dosync-0.4.1 → dosync-0.4.2}/dosync/composite_operations.py +0 -0
  59. {dosync-0.4.1 → dosync-0.4.2}/dosync/discovery.py +0 -0
  60. {dosync-0.4.1 → dosync-0.4.2}/dosync/ed25519_pure.py +0 -0
  61. {dosync-0.4.1 → dosync-0.4.2}/dosync/executor.py +0 -0
  62. {dosync-0.4.1 → dosync-0.4.2}/dosync/geo.py +0 -0
  63. {dosync-0.4.1 → dosync-0.4.2}/dosync/hub_monitor.py +0 -0
  64. {dosync-0.4.1 → dosync-0.4.2}/dosync/mcp_server.py +0 -0
  65. {dosync-0.4.1 → dosync-0.4.2}/dosync/metrics.py +0 -0
  66. {dosync-0.4.1 → dosync-0.4.2}/dosync/operation_guards.py +0 -0
  67. {dosync-0.4.1 → dosync-0.4.2}/dosync/operation_supervisor.py +0 -0
  68. {dosync-0.4.1 → dosync-0.4.2}/dosync/operations.py +0 -0
  69. {dosync-0.4.1 → dosync-0.4.2}/dosync/policies.py +0 -0
  70. {dosync-0.4.1 → dosync-0.4.2}/dosync/policy_config.py +0 -0
  71. {dosync-0.4.1 → dosync-0.4.2}/dosync/py.typed +0 -0
  72. {dosync-0.4.1 → dosync-0.4.2}/dosync/reconciler.py +0 -0
  73. {dosync-0.4.1 → dosync-0.4.2}/dosync/route_composer.py +0 -0
  74. {dosync-0.4.1 → dosync-0.4.2}/dosync/security.py +0 -0
  75. {dosync-0.4.1 → dosync-0.4.2}/dosync/validation.py +0 -0
  76. {dosync-0.4.1 → dosync-0.4.2}/dosync.egg-info/dependency_links.txt +0 -0
  77. {dosync-0.4.1 → dosync-0.4.2}/dosync.egg-info/entry_points.txt +0 -0
  78. {dosync-0.4.1 → dosync-0.4.2}/dosync.egg-info/top_level.txt +0 -0
  79. {dosync-0.4.1 → dosync-0.4.2}/setup.cfg +0 -0
  80. {dosync-0.4.1 → dosync-0.4.2}/tests/test_adapters.py +0 -0
  81. {dosync-0.4.1 → dosync-0.4.2}/tests/test_audit_archive.py +0 -0
  82. {dosync-0.4.1 → dosync-0.4.2}/tests/test_audit_backup.py +0 -0
  83. {dosync-0.4.1 → dosync-0.4.2}/tests/test_audit_provenance.py +0 -0
  84. {dosync-0.4.1 → dosync-0.4.2}/tests/test_ble_adapter.py +0 -0
  85. {dosync-0.4.1 → dosync-0.4.2}/tests/test_claim_state_machine.py +0 -0
  86. {dosync-0.4.1 → dosync-0.4.2}/tests/test_composite_operations.py +0 -0
  87. {dosync-0.4.1 → dosync-0.4.2}/tests/test_composite_orchestration.py +0 -0
  88. {dosync-0.4.1 → dosync-0.4.2}/tests/test_composition_kind_db.py +0 -0
  89. {dosync-0.4.1 → dosync-0.4.2}/tests/test_composition_kind_endpoint.py +0 -0
  90. {dosync-0.4.1 → dosync-0.4.2}/tests/test_composition_routing.py +0 -0
  91. {dosync-0.4.1 → dosync-0.4.2}/tests/test_db.py +0 -0
  92. {dosync-0.4.1 → dosync-0.4.2}/tests/test_device_health.py +0 -0
  93. {dosync-0.4.1 → dosync-0.4.2}/tests/test_device_heartbeat.py +0 -0
  94. {dosync-0.4.1 → dosync-0.4.2}/tests/test_drone_policies.py +0 -0
  95. {dosync-0.4.1 → dosync-0.4.2}/tests/test_ed25519_pure.py +0 -0
  96. {dosync-0.4.1 → dosync-0.4.2}/tests/test_event_loop_migration.py +0 -0
  97. {dosync-0.4.1 → dosync-0.4.2}/tests/test_explain_consistency.py +0 -0
  98. {dosync-0.4.1 → dosync-0.4.2}/tests/test_geo.py +0 -0
  99. {dosync-0.4.1 → dosync-0.4.2}/tests/test_ha_bridge_hygiene.py +0 -0
  100. {dosync-0.4.1 → dosync-0.4.2}/tests/test_hub_monitor.py +0 -0
  101. {dosync-0.4.1 → dosync-0.4.2}/tests/test_idempotency.py +0 -0
  102. {dosync-0.4.1 → dosync-0.4.2}/tests/test_independent_observation.py +0 -0
  103. {dosync-0.4.1 → dosync-0.4.2}/tests/test_integration_suite.py +0 -0
  104. {dosync-0.4.1 → dosync-0.4.2}/tests/test_mavlink_adapter.py +0 -0
  105. {dosync-0.4.1 → dosync-0.4.2}/tests/test_mavlink_channels.py +0 -0
  106. {dosync-0.4.1 → dosync-0.4.2}/tests/test_mavlink_listener.py +0 -0
  107. {dosync-0.4.1 → dosync-0.4.2}/tests/test_mavlink_return_home.py +0 -0
  108. {dosync-0.4.1 → dosync-0.4.2}/tests/test_mavlink_single_reader.py +0 -0
  109. {dosync-0.4.1 → dosync-0.4.2}/tests/test_mavlink_telemetry_closure.py +0 -0
  110. {dosync-0.4.1 → dosync-0.4.2}/tests/test_mcp_dynamic_intents.py +0 -0
  111. {dosync-0.4.1 → dosync-0.4.2}/tests/test_mcp_partial_progress.py +0 -0
  112. {dosync-0.4.1 → dosync-0.4.2}/tests/test_metrics.py +0 -0
  113. {dosync-0.4.1 → dosync-0.4.2}/tests/test_models.py +0 -0
  114. {dosync-0.4.1 → dosync-0.4.2}/tests/test_multihub_endpoints.py +0 -0
  115. {dosync-0.4.1 → dosync-0.4.2}/tests/test_operation_guards.py +0 -0
  116. {dosync-0.4.1 → dosync-0.4.2}/tests/test_operation_supervisor.py +0 -0
  117. {dosync-0.4.1 → dosync-0.4.2}/tests/test_operations.py +0 -0
  118. {dosync-0.4.1 → dosync-0.4.2}/tests/test_operations_endpoints.py +0 -0
  119. {dosync-0.4.1 → dosync-0.4.2}/tests/test_operations_persistence.py +0 -0
  120. {dosync-0.4.1 → dosync-0.4.2}/tests/test_operations_wiring.py +0 -0
  121. {dosync-0.4.1 → dosync-0.4.2}/tests/test_panel_polish_2026_07_21.py +0 -0
  122. {dosync-0.4.1 → dosync-0.4.2}/tests/test_policies.py +0 -0
  123. {dosync-0.4.1 → dosync-0.4.2}/tests/test_policy_config.py +0 -0
  124. {dosync-0.4.1 → dosync-0.4.2}/tests/test_reachability_cause.py +0 -0
  125. {dosync-0.4.1 → dosync-0.4.2}/tests/test_recall_benchmark_postpolicy.py +0 -0
  126. {dosync-0.4.1 → dosync-0.4.2}/tests/test_reconciler.py +0 -0
  127. {dosync-0.4.1 → dosync-0.4.2}/tests/test_resolution_wiring.py +0 -0
  128. {dosync-0.4.1 → dosync-0.4.2}/tests/test_resolver_scoring.py +0 -0
  129. {dosync-0.4.1 → dosync-0.4.2}/tests/test_resolver_semantics.py +0 -0
  130. {dosync-0.4.1 → dosync-0.4.2}/tests/test_route_composer.py +0 -0
  131. {dosync-0.4.1 → dosync-0.4.2}/tests/test_sensor_kind.py +0 -0
  132. {dosync-0.4.1 → dosync-0.4.2}/tests/test_server.py +0 -0
  133. {dosync-0.4.1 → dosync-0.4.2}/tests/test_telemetry_bridge.py +0 -0
  134. {dosync-0.4.1 → dosync-0.4.2}/tests/test_validation.py +0 -0
  135. {dosync-0.4.1 → dosync-0.4.2}/tests/test_validation_integration.py +0 -0
  136. {dosync-0.4.1 → dosync-0.4.2}/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.1
3
+ Version: 0.4.2
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
@@ -27,12 +27,16 @@ Requires-Dist: fastapi<1.0,>=0.115.0
27
27
  Requires-Dist: uvicorn<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
+ Requires-Dist: bleak>=0.21
31
+ Requires-Dist: pyyaml>=6.0.1
32
+ Requires-Dist: aiohttp>=3.9.0
33
+ Requires-Dist: paho-mqtt>=2.0.0
30
34
  Provides-Extra: wiz
31
35
  Requires-Dist: pywizlight>=0.5.0; extra == "wiz"
32
36
  Provides-Extra: mqtt
33
37
  Requires-Dist: paho-mqtt>=2.0.0; extra == "mqtt"
34
38
  Provides-Extra: ha
35
- Requires-Dist: aiohttp>=3.8.0; extra == "ha"
39
+ Requires-Dist: aiohttp>=3.9.0; extra == "ha"
36
40
  Provides-Extra: ble
37
41
  Requires-Dist: bleak>=0.21; extra == "ble"
38
42
  Provides-Extra: sms
@@ -44,7 +48,7 @@ Requires-Dist: mcp>=1.0.0; extra == "mcp"
44
48
  Provides-Extra: all
45
49
  Requires-Dist: pywizlight>=0.5.0; extra == "all"
46
50
  Requires-Dist: paho-mqtt>=2.0.0; extra == "all"
47
- Requires-Dist: aiohttp>=3.8.0; extra == "all"
51
+ Requires-Dist: aiohttp>=3.9.0; extra == "all"
48
52
  Requires-Dist: bleak>=0.21; extra == "all"
49
53
  Requires-Dist: twilio>=8.0.0; extra == "all"
50
54
  Requires-Dist: pymavlink>=2.4.0; extra == "all"
@@ -57,7 +61,7 @@ Dynamic: license-file
57
61
 
58
62
  # DoSync Protocol
59
63
 
60
- > The semantic layer between AI agents and physical devices.
64
+ > Governance and accountability for AI that acts on physical devices.
61
65
 
62
66
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
63
67
  [![Protocol](https://img.shields.io/badge/protocol-v0.4-green.svg)](spec/DoSync-SPEC-v0.1.md)
@@ -99,6 +103,65 @@ When the hub receives `"ensure_safety / emergency"`, every registered device fig
99
103
 
100
104
  ---
101
105
 
106
+ ## How is this different from what already exists?
107
+
108
+ A fair question, and the honest answer is that DoSync sits **above** most of what
109
+ it gets compared to, not against it.
110
+
111
+ | | What it does | What it does not decide |
112
+ |---|---|---|
113
+ | **Matter, Zigbee, MQTT** | Move commands to devices | Which device should act, or whether it should |
114
+ | **W3C Web of Things** (Thing Description) | Describe a device's properties, actions and events, with semantic annotations | Which devices serve a goal, what an operator forbids, or what happened afterwards |
115
+ | **MCP, A2A** | Connect an agent to tools | Anything about a tool being a door lock and the action being irreversible |
116
+ | **DoSync** | Resolve a goal to a plan, constrain it, execute it, and prove what happened | Transport, device description, or agent connectivity — it uses all three |
117
+
118
+ **Specifically on W3C Web of Things**, since it is the closest and the most
119
+ established: a Thing Description tells you a lock exposes a `lock` action and
120
+ how to invoke it. That is genuinely the right way to describe a device, and
121
+ DoSync does not compete with it. What a description cannot do is decide that a
122
+ lock is one of the things that should respond to *"there is an emergency"*,
123
+ refuse to touch it because this deployment forbids it, arbitrate when two
124
+ intents want it at once, or leave evidence afterwards that survives someone with
125
+ root access. Those are the questions DoSync answers.
126
+
127
+ **On MCP**: DoSync ships an MCP server. It is a distribution channel, not a
128
+ rival. MCP is how an agent reaches DoSync; DoSync is what happens between the
129
+ agent's goal and a device moving.
130
+
131
+ ### The five things
132
+
133
+ Everything above reduces to five properties. Each is verifiable in a running
134
+ hub — the numbers below come from the reference deployment, not from a
135
+ brochure:
136
+
137
+ 1. **Explainable resolution.** `GET /v1/intents/{class}/explain` returns which
138
+ devices were evaluated, which were included, and the score breakdown behind
139
+ each. The score it reports is the same value the resolver decided with — one
140
+ computation, not a narration of one.
141
+ 2. **Policies the AI cannot route around.** A deployment declares what must not
142
+ happen; every path to a device is evaluated against it, including direct
143
+ device actions and the MCP tool. This was not true until we audited our own
144
+ claim and found the hole.
145
+ 3. **A record that resists tampering — and says where it stops.** SHA-256 chain
146
+ with policy provenance, plus sequence numbers, a head mark and signed
147
+ exportable checkpoints. What it detects and what it cannot is written down in
148
+ [the threat model](docs/AUDIT-THREAT-MODEL.md), including the rows that read
149
+ "not detected".
150
+ 4. **Formal arbitration of physical conflict.** A per-device claim state machine
151
+ with stated invariants ([consistency model §3.1](spec/CONSISTENCY-MODEL.md)), so an
152
+ emergency and a routine wanting the same device is resolved by rule rather
153
+ than by timing.
154
+ 5. **Failure semantics that do not lie.** `contradicted` (the device said yes,
155
+ the sensor disagrees) is distinct from `unverifiable` (we could not look), and
156
+ `likely_powered_off` from `indeterminate`. The system says what it does not
157
+ know.
158
+
159
+ None of these is claimed to be unbreakable. Claim 3 in particular has documented
160
+ limits, on purpose: a protocol whose value is honesty cannot make absolute
161
+ security claims and stay coherent.
162
+
163
+ ---
164
+
102
165
  ## Scope and safety boundaries
103
166
 
104
167
  DoSync coordinates **non-safety-critical systems** — lighting, access, climate, notifications, logging — and produces a tamper-evident record of every action. It is infrastructure for coordination and auditability, not a certified safety system.
@@ -190,10 +253,43 @@ Benchmark (Raspberry Pi 5, Python 3.11.2):
190
253
  ### Install and run — five minutes, no hardware
191
254
 
192
255
  ```bash
193
- pip install dosync
256
+ pipx install dosync # recommended
194
257
  dosync-hub
195
258
  ```
196
259
 
260
+ <details>
261
+ <summary><b>"error: externally-managed-environment"?</b> — Raspberry Pi OS, Debian 12+, Ubuntu 23.04+</summary>
262
+
263
+ Those systems refuse system-wide `pip install` (PEP 668) to stop Python packages
264
+ from breaking the OS. This hits the Raspberry Pi first, which is the most likely
265
+ machine to be running a hub, so it is worth getting right rather than working
266
+ around.
267
+
268
+ **`pipx` is the correct tool here** and not a workaround: DoSync is an
269
+ application with commands you run, not a library you import into your own code.
270
+ pipx gives it a private environment and still puts `dosync-hub`,
271
+ `dosync-manage` and `dosync-certify` on your PATH.
272
+
273
+ ```bash
274
+ sudo apt install pipx # once
275
+ pipx ensurepath # once; open a new shell afterwards
276
+ pipx install dosync
277
+ ```
278
+
279
+ If you are writing Python against DoSync rather than running the hub, a virtual
280
+ environment is the right choice instead:
281
+
282
+ ```bash
283
+ python3 -m venv ~/dosync-env
284
+ ~/dosync-env/bin/pip install dosync
285
+ ~/dosync-env/bin/dosync-hub
286
+ ```
287
+
288
+ `pip install --break-system-packages dosync` also works and is the one option we
289
+ would not recommend: it installs into the system Python that your OS depends on,
290
+ which is the situation PEP 668 exists to prevent.
291
+ </details>
292
+
197
293
  That is a working hub on `http://127.0.0.1:47200`. It starts with a simulated
198
294
  executor, so you can drive the whole protocol — register devices, fire intents,
199
295
  read the audit chain — before you own a single smart device.
@@ -228,7 +324,93 @@ evaluated, which it included, and the score breakdown behind each decision.
228
324
  Step 4 is the other: every action leaves a SHA-256-chained entry, so what the
229
325
  system did is provable after the fact rather than merely logged.
230
326
 
231
- Install only the adapters you need:
327
+ **Discovery works out of the box.** The library that finds Bluetooth devices
328
+ ships in the core install, and the BLE adapter registers itself when it is
329
+ available — because discovery is how you learn what you have. Requiring an extra
330
+ first would be a circle: nobody installs a Bluetooth library before knowing they
331
+ own Bluetooth devices, and nobody can find out without it. A hub with no radio
332
+ loses nothing — the scan reports the transport as unsearchable rather than
333
+ failing — and `pip uninstall bleak` or `DOSYNC_BLE_ENABLED=false` removes it.
334
+
335
+ **On the adapters that ship with DoSync.** They come in three kinds, visible at
336
+ `GET /v1/adapters`. **Ecosystem** adapters implement open standards — MQTT,
337
+ Matter, BLE, MAVLink, and the Home Assistant bridge, which is the widest door of
338
+ all: anything HA already integrates, DoSync can reach. **Reference** adapters
339
+ (WiZ, Shelly) implement one vendor's product and ship as worked examples of how
340
+ an adapter is written — not as endorsement, partnership, or a promise to track
341
+ anyone's firmware. **Infrastructure** is notifications.
342
+
343
+ **If your device is not covered, describe it in a file.** A declarative adapter
344
+ is YAML or JSON — no code, no release of DoSync to wait for:
345
+
346
+ ```yaml
347
+ device:
348
+ id: light-hallway
349
+ name: Hallway light
350
+ tags: [light, energy] # how intents find it
351
+ emergency_capable: true # whether an emergency may use it
352
+
353
+ transport:
354
+ kind: http
355
+ base_url: http://192.168.1.40
356
+
357
+ actions:
358
+ turn_on:
359
+ type: turn_on # what it MEANS to DoSync, not just its name
360
+ request: { method: POST, path: /light/on }
361
+ ```
362
+
363
+ Drop it in `declarative/` (or set `DOSYNC_DECLARATIVE_DIR`) and restart.
364
+
365
+ **You do not have to write the first one.** Six worked examples ship with the
366
+ package — a light, an air conditioner, a 3D printer, a television, a floor's
367
+ lighting controller and an industrial conveyor over MQTT — chosen so one of them
368
+ probably resembles what you have:
369
+
370
+ ```bash
371
+ dosync-manage examples # copies them into declarative/, ready to edit
372
+ ```
373
+
374
+ They are also readable in the repository at
375
+ [`examples/declarative/`](examples/declarative/).
376
+
377
+ The `type` on each action is the part that matters. A file that only said "POST
378
+ /on turns it on" would let DoSync switch the device and leave it invisible to
379
+ everything else: no intent could select it, no policy could name it, an
380
+ emergency would pass it by.
381
+
382
+ **What a declarative adapter cannot do**, stated plainly: it speaks HTTP. It
383
+ cannot speak Zigbee, Z-Wave, BLE pairing, an OPC-UA session, or anything needing
384
+ a handshake, session state or a vendor SDK. Those need a code adapter — an
385
+ ecosystem one here, or a third-party package. This format covers most simple
386
+ devices and almost no complex ones.
387
+
388
+ **If it needs real code — pairing, a session, a vendor SDK — publish a package.**
389
+ DoSync discovers adapters advertised by anything installed alongside it:
390
+
391
+ ```toml
392
+ # in the vendor's pyproject.toml
393
+ [project.entry-points."dosync.adapters"]
394
+ daikin = "dosync_adapter_daikin:DaikinAdapter"
395
+ ```
396
+
397
+ The operator runs `pip install dosync-adapter-daikin` and the hub finds it. No
398
+ pull request here, and no promise from this project to maintain code for
399
+ hardware it has never seen — the publisher answers for their own adapter.
400
+
401
+ A third-party adapter runs inside the hub with the hub's permissions, so the hub
402
+ says so: it is logged at WARNING when loaded, recorded in the audit chain, and
403
+ reported as `kind: third_party` at `/v1/adapters` regardless of what the plugin
404
+ declares about itself. Where code came from is not the code's to assert.
405
+
406
+ DoSync does not download an adapter for you: the
407
+ protocol's whole argument is that nothing actuates hardware without a policy and
408
+ a record, and fetching executable code from the internet would put the largest
409
+ possible hole exactly there. Instead, an operator writes a declarative adapter
410
+ for HTTP/MQTT/Modbus devices, or installs a third-party package deliberately.
411
+
412
+ Install only the CONTROL adapters you need — those follow the opposite rule,
413
+ since you already know which hardware you own:
232
414
 
233
415
  ```bash
234
416
  pip install 'dosync[wiz]' # Philips WiZ bulbs
@@ -237,6 +419,147 @@ pip install 'dosync[mqtt]' # MQTT devices
237
419
  pip install 'dosync[all]' # everything
238
420
  ```
239
421
 
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
+ ### Access: a password you choose, or none at all
452
+
453
+ The hub prints a token on first start and stores only a hash of it, so it cannot
454
+ show you that one again. You are not stuck with it.
455
+
456
+ **From the dashboard** (the ⚙ button, once connected): set a password of your
457
+ choosing, or turn the token requirement off entirely. No shell, no unit file.
458
+
459
+ **From a terminal**, if you prefer:
460
+
461
+ ```bash
462
+ # Choose your own — for a person who has to type it
463
+ dosync-manage keys create --token "my-house-2026-kitchen" --label dashboard
464
+
465
+ # Let it generate one — for a program that will store it
466
+ dosync-manage keys create --label my-integration
467
+
468
+ # Start with no authentication at all
469
+ DOSYNC_AUTH=false dosync-hub
470
+ ```
471
+
472
+ **Running without a token is a legitimate choice, not a trap door.** On a home
473
+ network, behind a router, with no port forwarding, a token protects against
474
+ nobody who is not already inside your house. It is the wrong default for a
475
+ clinic and an unnecessary obstacle for a workshop, so DoSync offers it plainly
476
+ rather than assuming everyone shares one threat model. It is not suitable for
477
+ any hub reachable from outside its own network.
478
+
479
+ Two things worth knowing:
480
+
481
+ - **`DOSYNC_AUTH` in the environment wins.** If it is set in your service
482
+ configuration, the dashboard will tell you so and refuse to override it — a
483
+ click in a browser should not quietly undo what the machine was told to do.
484
+ - **Changing access is recorded.** Setting a password or turning authentication
485
+ off appends to the audit chain, so "when did this hub become open, and who did
486
+ it" has an answer. The token value itself is never written there.
487
+
488
+ A chosen password must be at least 12 characters, and a passphrase of several
489
+ words is better than a short clever string: a bearer token is checked with no
490
+ rate limiting and no lockout, so it is guessed offline at full speed.
491
+
492
+ Existing keys: `dosync-manage keys list` (previews only — they are hashed),
493
+ `keys revoke <preview>`, `keys reset`.
494
+
495
+ ### Hardware that cannot do TLS
496
+
497
+ A sensor running a year on a coin cell cannot perform a TLS handshake — it costs
498
+ more battery than a month of operation. Such a device can still report liveness,
499
+ signed rather than encrypted:
500
+
501
+ ```bash
502
+ DOSYNC_LIGHTWEIGHT_HEARTBEAT=true dosync-hub
503
+ ```
504
+
505
+ `POST /v1/heartbeat/signed` accepts a heartbeat authenticated by HMAC over the
506
+ device's provisioning token. It is **off by default**, and it is worth knowing
507
+ exactly what it trades before turning it on: the channel provides message
508
+ authenticity and replay resistance, and **no confidentiality** — the device id,
509
+ timestamp and report travel readable. Devices using it are marked
510
+ `report_channel: signed_plaintext` so they are distinguishable from ones on mTLS.
511
+
512
+ That trade is defensible for a heartbeat and would not be for an action: a
513
+ heartbeat is positive signal only, so a forged one cannot switch anything on.
514
+ The attack it invites is replay — repeating a captured message to keep a failed
515
+ device reporting healthy — and that is closed. See spec §7.10 and
516
+ [the threat model](docs/AUDIT-THREAT-MODEL.md).
517
+
518
+ ### TLS, and why your browser says "Not secure"
519
+
520
+ `bash setup_pki.sh` creates a private certificate authority in `certs/` and
521
+ issues the hub a certificate from it. Start the hub with those files and traffic
522
+ is encrypted:
523
+
524
+ ```bash
525
+ dosync-hub --host 0.0.0.0 & # or with uvicorn's --ssl-keyfile / --ssl-certfile
526
+ ```
527
+
528
+ Your browser will then show **"Not secure"** with `https` struck through. This
529
+ is expected and it does **not** mean the connection is unencrypted. It means the
530
+ browser does not recognise the authority that signed the certificate — which is
531
+ you. A public CA cannot issue a certificate for `192.168.x.x`, so a hub on a
532
+ private network is always in this position.
533
+
534
+ Two honest options:
535
+
536
+ **Accept the warning.** Click through it. The connection is encrypted; what is
537
+ missing is a third party vouching that the server is who it claims. On your own
538
+ LAN, where you set up the hub yourself, that is a much smaller gap than it looks.
539
+
540
+ **Trust your own CA, and the warning goes away** — on the machines you choose:
541
+
542
+ ```bash
543
+ # macOS
544
+ sudo security add-trusted-cert -d -r trustRoot \
545
+ -k /Library/Keychains/System.keychain certs/ca.crt
546
+
547
+ # Linux (Debian/Ubuntu)
548
+ sudo cp certs/ca.crt /usr/local/share/ca-certificates/dosync-ca.crt
549
+ sudo update-ca-certificates
550
+
551
+ # Windows (PowerShell, as Administrator)
552
+ Import-Certificate -FilePath ca.crt -CertStoreLocation Cert:\LocalMachine\Root
553
+ ```
554
+
555
+ Copy `certs/ca.crt` from the hub first — it is the only file you need, and it
556
+ contains no secret. The hub's private key (`certs/hub.key`) never leaves the
557
+ hub.
558
+
559
+ **What the warning does mean.** If you see it on a hub you did not set up, or on
560
+ a network you do not control, do not click through — that is exactly the case
561
+ the warning exists for.
562
+
240
563
  ### Docker
241
564
 
242
565
  ```bash