pyshal 0.1.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 (60) hide show
  1. pyshal-0.1.0/CHANGELOG.md +119 -0
  2. pyshal-0.1.0/CONTRIBUTING.md +58 -0
  3. pyshal-0.1.0/LICENSE +21 -0
  4. pyshal-0.1.0/MANIFEST.in +14 -0
  5. pyshal-0.1.0/PKG-INFO +385 -0
  6. pyshal-0.1.0/README.md +352 -0
  7. pyshal-0.1.0/docs/CATALOG.md +643 -0
  8. pyshal-0.1.0/docs/DECISIONS - V2.1.md +63 -0
  9. pyshal-0.1.0/docs/DESIGN - PHASE 2 ASYNC.md +804 -0
  10. pyshal-0.1.0/docs/DESIGN - V1.md +413 -0
  11. pyshal-0.1.0/docs/DESIGN V2.md +719 -0
  12. pyshal-0.1.0/docs/PROPOSAL - SW TOPOLOGIES.md +191 -0
  13. pyshal-0.1.0/docs/SDK.md +385 -0
  14. pyshal-0.1.0/docs/agents/domain.md +53 -0
  15. pyshal-0.1.0/docs/agents/issue-tracker.md +22 -0
  16. pyshal-0.1.0/docs/agents/triage-labels.md +15 -0
  17. pyshal-0.1.0/pyproject.toml +91 -0
  18. pyshal-0.1.0/setup.cfg +4 -0
  19. pyshal-0.1.0/src/pyshal.egg-info/PKG-INFO +385 -0
  20. pyshal-0.1.0/src/pyshal.egg-info/SOURCES.txt +58 -0
  21. pyshal-0.1.0/src/pyshal.egg-info/dependency_links.txt +1 -0
  22. pyshal-0.1.0/src/pyshal.egg-info/entry_points.txt +19 -0
  23. pyshal-0.1.0/src/pyshal.egg-info/requires.txt +6 -0
  24. pyshal-0.1.0/src/pyshal.egg-info/top_level.txt +1 -0
  25. pyshal-0.1.0/src/shal/__init__.py +65 -0
  26. pyshal-0.1.0/src/shal/approval.py +143 -0
  27. pyshal-0.1.0/src/shal/buses/__init__.py +0 -0
  28. pyshal-0.1.0/src/shal/buses/http_bus.py +74 -0
  29. pyshal-0.1.0/src/shal/buses/i2c_cli.py +90 -0
  30. pyshal-0.1.0/src/shal/buses/local.py +51 -0
  31. pyshal-0.1.0/src/shal/buses/mux.py +83 -0
  32. pyshal-0.1.0/src/shal/buses/scpi_raw.py +110 -0
  33. pyshal-0.1.0/src/shal/buses/sim.py +232 -0
  34. pyshal-0.1.0/src/shal/buses/sim_msg.py +101 -0
  35. pyshal-0.1.0/src/shal/buses/sim_scpi.py +138 -0
  36. pyshal-0.1.0/src/shal/buses/spi_cli.py +63 -0
  37. pyshal-0.1.0/src/shal/buses/ssh.py +74 -0
  38. pyshal-0.1.0/src/shal/buses/tcp.py +100 -0
  39. pyshal-0.1.0/src/shal/capabilities.py +67 -0
  40. pyshal-0.1.0/src/shal/conformance.py +246 -0
  41. pyshal-0.1.0/src/shal/driver.py +299 -0
  42. pyshal-0.1.0/src/shal/drivers/__init__.py +0 -0
  43. pyshal-0.1.0/src/shal/drivers/ads1115.py +48 -0
  44. pyshal-0.1.0/src/shal/drivers/ina219.py +57 -0
  45. pyshal-0.1.0/src/shal/drivers/keysight_34461a.py +44 -0
  46. pyshal-0.1.0/src/shal/drivers/mcp23017.py +57 -0
  47. pyshal-0.1.0/src/shal/drivers/mcp9808.py +39 -0
  48. pyshal-0.1.0/src/shal/drivers/rigol_dp832.py +62 -0
  49. pyshal-0.1.0/src/shal/drivers/tmp102.py +30 -0
  50. pyshal-0.1.0/src/shal/errors.py +89 -0
  51. pyshal-0.1.0/src/shal/hal.py +221 -0
  52. pyshal-0.1.0/src/shal/limits.py +192 -0
  53. pyshal-0.1.0/src/shal/loader.py +224 -0
  54. pyshal-0.1.0/src/shal/log.py +76 -0
  55. pyshal-0.1.0/src/shal/logging.py +113 -0
  56. pyshal-0.1.0/src/shal/node.py +71 -0
  57. pyshal-0.1.0/src/shal/py.typed +0 -0
  58. pyshal-0.1.0/src/shal/registry.py +229 -0
  59. pyshal-0.1.0/src/shal/schema/shal-v1.schema.json +142 -0
  60. pyshal-0.1.0/src/shal/transport.py +118 -0
@@ -0,0 +1,119 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Fixed
10
+ - **Authoring-contract drift** (#15) — aligned the `shal-build-*` skills with
11
+ `docs/SDK.md` and the framework so a driver copied verbatim from the
12
+ `shal-build-driver` skeleton passes `conformance.check_driver` (now regression-
13
+ tested): the skeleton uses the blessed `shal.TemperatureSensor` and includes the
14
+ required `llm_ready` + `@op` (no longer framed as "optional"). Also fixed the
15
+ `src/shal/schema/` path in `shal-build-yaml`, completed its bundled-id list
16
+ (added `shal,scpi-raw`/`shal,sim-scpi`/`shal,sim-msg`, pointing at
17
+ `shal.catalog()` as authoritative), added `txn=` to the documented `HopError`
18
+ signature, and documented the actuator `safe_state()` hook in the SDK.
19
+
20
+ ### Added
21
+ - **Human-in-the-loop actuation gate** (#14) — actuator and destructive/config
22
+ ops (`@shal.op(side_effect="actuator"|"config")`) now stop for an injectable
23
+ `Approver` *after* the limit check and *before* any bus I/O. The gate lives in the capability-wrapper, so neither the
24
+ tool surface (`call_tool`) nor the raw path (`get_device().method()`) can bypass
25
+ it. SHAL ships the mechanism + a safe default (`ConsoleApprover`: prompt when
26
+ interactive, deny when headless) plus `AutoApprove`/`DenyAll`/`CallableApprover`;
27
+ install one with `shal.set_approver(...)` or the `shal.approver(...)` context
28
+ manager. Refusal raises `shal.ApprovalDenied` (nothing sent) and `call_tool`
29
+ returns `{"ok": False, "rejected": "approval"}`. Every decision (approved/denied)
30
+ is written to `shal.audit`. Order is always limits → approval → I/O.
31
+ - **Declared operating limits** (#10) — `@shal.op(params=...)` takes JSON-Schema
32
+ fragments per parameter; the merged schema is advertised verbatim in
33
+ `tool_schemas()`/`catalog()` AND enforced by the framework before any bus I/O
34
+ (`shal.LimitError`; rejected writes are audited `outcome=rejected`). Two
35
+ narrow-only layers stack on top: `driver.op_limits()` for address-dependent
36
+ ratings and YAML `config.limits` for installation policy — widening fails the
37
+ load naming both numbers.
38
+ - **Conformance kit** (#10) — `shal.conformance.check_driver()` self-certifies a
39
+ driver: static checks (llm_ready, @op metadata, schema well-formedness) plus
40
+ live probes on a sim topology (limits actually reject pre-I/O, writes actually
41
+ hit the audit channel, capabilities actually isinstance).
42
+ - **Generic sim buses** (#10) — `shal,sim-scpi` (`@scpi_sim_model`) and
43
+ `shal,sim-msg` (`@msg_sim_model`) mirror sim-i2c's model registry for the
44
+ MessageTransport families; ships a DP832 model for hermetic SCPI coverage.
45
+ - **Driver SDK guide** (#10) — `docs/SDK.md`: the complete authoring contract
46
+ (driver anatomy, capabilities, transport dialects, limits, sims, conformance);
47
+ with the skills, writing a driver requires reading zero SHAL internals. New
48
+ `shal-generate-driver` skill: the documentation→driver generation recipe.
49
+ - **Core I²C drivers** (#2/#3) — `microchip,mcp9808` (`TemperatureSensor`),
50
+ `ti,ads1115` (new `ADC` capability), `microchip,mcp23017` (new `GPIOExpander`
51
+ capability). All dependency-free, sim-backed (`shal,sim-i2c` models), hermetic tests.
52
+ - **SCPI instrument stack** (#2, Wave 1) — `shal,scpi-raw` bus (SCPI over a raw TCP
53
+ socket, the lab :5025 convention; stdlib sockets only, no VISA; plaintext with a
54
+ required `insecure: true`), plus the first instrument drivers `rigol,dp832`
55
+ (`PowerSupply`) and `keysight,34461a` (`DigitalMultimeter`) and their capability
56
+ Protocols. End-to-end tested against a fake SCPI socket server (no hardware).
57
+ - **Driver `ti,ina219`** (#2, Wave 1) — I²C bus-voltage / current / power monitor,
58
+ the first `PowerMonitor` capability. Dependency-free, sim-backed (`shal,sim-i2c`
59
+ gains an `ina219` model), fully hermetic tests.
60
+ - **Node-level agent metadata** (#1) — optional `description:` (instance context
61
+ blended into each tool's description, so an agent distinguishes like devices) and
62
+ `expose: false` (omit a node from `tool_schemas()`/`tool_catalog()`/`call_tool()`
63
+ while keeping it usable from Python) on any topology node. Additive; existing
64
+ topologies are unaffected.
65
+ - **`shal.catalog()` authoring surface** (#1) — an introspection view of every
66
+ registered driver/bus so an LLM (or a human) can construct a valid topology:
67
+ `catalog()` returns compact summaries, `catalog(compatible)` the full detail.
68
+ Most fields are derived (compatible, required parent kind, capability Protocol,
69
+ ops); a class declares only the irreducible bits via an optional `authoring_meta()`
70
+ classmethod (`address_schema` / `config_schema` / `child_address_schema` as
71
+ JSON-Schema fragments). Op annotations map side-effects to MCP-style hint names
72
+ (`readOnlyHint`/`idempotentHint`/`destructiveHint`), also added to `tool_catalog()`.
73
+ - **Topology includes** — a node may `use:` an external `template:` file to graft
74
+ a reusable subtree (a board, a rack) without copy-paste, with `with:` parameter
75
+ substitution (`${param}`), use-site key overrides, include chains, a cycle
76
+ guard, and path confinement to the project tree. Still `yaml.safe_load` only —
77
+ the splice happens in the loader, never via a YAML tag.
78
+ - **Registry collision policy** — two different classes claiming one `compatible`
79
+ no longer silently overwrite (last-write-wins). The clash fails the load,
80
+ naming each providing distribution; disambiguate with a node `from:` key,
81
+ `register(..., override=True)`, or by uninstalling one. Re-registering the same
82
+ class stays an idempotent no-op.
83
+ - **LLM tool surface** — `@shal.op(description, unit, side_effect)` metadata on
84
+ capability ops; `Driver.llm_ready = True` enforces it at bind time.
85
+ `hal.tool_schemas()` emits Anthropic tool-use definitions for every device op,
86
+ `hal.tool_catalog()` reports per-op `side_effect`/idempotency for gating, and
87
+ `hal.call_tool(name, args)` dispatches — a delivery-unknown write is reported,
88
+ never silently retried. Buses are excluded (they provide transport, not
89
+ capabilities).
90
+
91
+ ## [0.1.0] - 2026-06-10
92
+
93
+ Phase 1: the synchronous core.
94
+
95
+ ### Added
96
+ - Topology loader: versioned YAML (`shal_version: 1`) with JSON Schema
97
+ validation, global id uniqueness, address-grammar validation at load,
98
+ `$ref` back-links, `${ENV_VAR}` resolution for addresses and `config:` values.
99
+ - Typed transport kinds: `ByteTransport`, `CommandTransport` (argv only),
100
+ `MessageTransport`, `Stream` (Phase 2 placeholder); `kinds()` introspection.
101
+ - Driver model: registry keyed by `compatible`, entry-point group
102
+ `shal.drivers`, `@shal.idempotent`, framework-owned retry
103
+ (reconnect once / retry once for idempotent ops; delivery-unknown writes are
104
+ never re-fired).
105
+ - Bundled buses: `shal,sim-i2c`, `shal,local`, `shal,ssh-host`,
106
+ `shal,i2c-cli`, `shal,spi-cli`, `shal,tcp` (TLS default), `shal,http`,
107
+ `nxp,pca9548` mux with per-mux selection cache.
108
+ - Bundled driver: `ti,tmp102` (`TemperatureSensor` capability).
109
+ - Lookup API: `shal.load()` context manager, `get_device()` by id/path with
110
+ positional shorthand, deterministic leaf→root teardown.
111
+ - Error taxonomy: `LoadError`, `HopError` (`delivered: no|unknown`),
112
+ `HopTimeout`, `Busy`, `Gap`.
113
+ - Observability: structured records with stable `event` keys and
114
+ `path/hop/addr/txn/duration_ms` fields on every hop; WARNING on handled
115
+ retries; DEBUG breadcrumbs before raising; `shal.audit` channel for
116
+ actuator-style write ops (silent by default); `shal.logging` with
117
+ `ConsoleFormatter`, `JSONFormatter`, and the `capture()` JSON-lines flight
118
+ recorder.
119
+ - Packaging: PEP 621 metadata, `py.typed`, MIT license, CI workflow.
@@ -0,0 +1,58 @@
1
+ # Contributing to SHAL
2
+
3
+ Thanks for your interest. SHAL is a standard-in-the-making, so the bar for the
4
+ core is deliberately high; extending it via drivers and buses is meant to be easy.
5
+
6
+ ## Where complexity goes
7
+
8
+ > Complexity flows toward the rarest audience; simplicity flows toward users.
9
+
10
+ Three audiences, in order of how rare they are: **bus authors** (core/experts) →
11
+ **driver authors** (the community) → **end users** (write YAML, call
12
+ capabilities). A change that pushes complexity toward end users is rejected.
13
+ The locked design lives in [docs/DESIGN V2.md](docs/DESIGN%20V2.md) and
14
+ [docs/DECISIONS - V2.1.md](docs/DECISIONS%20-%20V2.1.md) — read them before
15
+ proposing core changes; the invariants there are not up for re-litigation in a PR.
16
+
17
+ ## Project layout
18
+
19
+ ```
20
+ src/shal/ the package (importable as `shal`)
21
+ tests/ pytest suite
22
+ docs/ design + decision records
23
+ examples/demos/ runnable showcases (Deebot cloud, microservice mesh) — not shipped
24
+ examples/driver-creator/ the doc→driver generation benchmark — not shipped
25
+ ```
26
+
27
+ ## Dev setup
28
+
29
+ ```sh
30
+ pip install -e ".[dev]"
31
+ python -m pytest # tests
32
+ ruff check src tests # lint
33
+ python -m build # sdist + wheel
34
+ ```
35
+
36
+ Requires Python ≥ 3.10. CI runs the suite on Linux and Windows across 3.10–3.13.
37
+
38
+ ## Adding a driver or bus
39
+
40
+ Don't edit the core to add hardware support — publish a package that exposes your
41
+ driver via the `shal.drivers` entry point (the bundled drivers are wired the same
42
+ way in `pyproject.toml`). The `.claude/skills/` folder has step-by-step guides:
43
+ `shal-build-driver`, `shal-build-bus`, `shal-build-yaml`.
44
+
45
+ ## Pull requests
46
+
47
+ - Keep changes minimal and consistent with the documented invariants; the code
48
+ comments state them explicitly.
49
+ - Every change ships with tests; the suite must stay green and `ruff` clean.
50
+ - A failure that needs a *design decision* rather than a bug fix → open an issue
51
+ first, don't guess.
52
+ - Update `CHANGELOG.md` under `## [Unreleased]`.
53
+
54
+ ## The non-negotiables (security & safety)
55
+
56
+ `yaml.safe_load` only · `CommandTransport` carries argv vectors, never shell
57
+ strings · a delivery-unknown write is never auto-retried · secrets via `${ENV}`,
58
+ never in topology files, never logged · the library never configures logging.
pyshal-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SHAL contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ # Source distribution contents. Wheel data files are handled by package-data in
2
+ # pyproject.toml; this governs the sdist tarball.
3
+ include LICENSE
4
+ include README.md
5
+ include CHANGELOG.md
6
+ include CONTRIBUTING.md
7
+ include pyproject.toml
8
+ recursive-include src/shal *.json py.typed
9
+ recursive-include docs *.md
10
+
11
+ # Keep examples out of the tarball.
12
+ prune examples
13
+ prune tests
14
+ global-exclude __pycache__ *.py[cod]
pyshal-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,385 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyshal
3
+ Version: 0.1.0
4
+ Summary: System/Software Hardware Abstraction Layer — device-tree-inspired, dynamic, user-space, network-capable
5
+ Author: SHAL contributors
6
+ License: MIT
7
+ Project-URL: Documentation, https://github.com/determlab/shal#readme
8
+ Project-URL: Changelog, https://github.com/determlab/shal/blob/main/CHANGELOG.md
9
+ Project-URL: Source, https://github.com/determlab/shal
10
+ Keywords: hardware,hal,device-tree,i2c,spi,ssh,embedded,lab-automation,robotics,instrumentation
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: System :: Hardware
22
+ Classifier: Topic :: Software Development :: Embedded Systems
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: pyyaml>=6.0
28
+ Requires-Dist: jsonschema>=4.18
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7; extra == "dev"
31
+ Requires-Dist: ruff>=0.4; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ <div align="center">
35
+
36
+ # SHAL
37
+
38
+ ### Give an AI agent real hardware — gated, so it asks before it moves.
39
+
40
+ Describe your whole lab — sensors, instruments, robots, services — in **one YAML
41
+ file**, and hand it to an LLM as **typed, permission-gated tools**: writes stop
42
+ for approval before they reach the device; reads don't. No transport code, no glue.
43
+
44
+ > In a blind test, an agent wrote working, safety-checked drivers for **4 of 4**
45
+ > devices from documentation alone — then drove a **real robot, gated**.
46
+
47
+ <!-- BADGES -->
48
+ [![GitHub stars](https://img.shields.io/github/stars/determlab/shal?style=social)](https://github.com/determlab/shal)
49
+ [![PyPI](https://img.shields.io/badge/PyPI-coming_soon-blue)](#install)
50
+ [![Python](https://img.shields.io/badge/python-3.10+-blue)](#install)
51
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
52
+ [![Status](https://img.shields.io/badge/status-alpha-orange)](#roadmap)
53
+
54
+ <picture>
55
+ <source srcset="docs/assets/SHAL_banner.webp" type="image/webp">
56
+ <img alt="SHAL turns your lab and services into one YAML topology — controlled from Python or exposed to an AI agent as typed, gated tools" src="docs/assets/SHAL_banner.png" width="100%">
57
+ </picture>
58
+
59
+
60
+ ### [→ Try it in 60 seconds — no hardware required](#quick-start)
61
+
62
+ </div>
63
+
64
+ **Built for:**
65
+
66
+ ✓ AI agent builders &nbsp;·&nbsp; ✓ Validation & test engineers &nbsp;·&nbsp;
67
+ ✓ Hardware-in-the-loop automation &nbsp;·&nbsp; ✓ Labs with mixed hardware + software
68
+
69
+ ---
70
+
71
+ ## From glue scripts to agent tools — in three steps
72
+
73
+ **Step 1 · Today, without SHAL** — a separate library, address, and retry per device:
74
+
75
+ ```python
76
+ sensor = TMP102(i2c_bus, 0x48)
77
+ supply = SCPIPowerSupply("10.0.0.50:5025")
78
+ results = RESTClient("https://mes.lab.internal")
79
+ # ...and you wire each one's retries, logging, and tool-wrapper by hand
80
+ ```
81
+
82
+ **Step 2 · With SHAL** — describe the rack once, then call devices by name:
83
+
84
+ ```python
85
+ hal = shal.load("lab.yaml")
86
+
87
+ hal.get_device("ambient_temp").read_celsius() # I²C sensor
88
+ hal.get_device("dut_power").set_voltage(3.3) # SCPI supply
89
+ hal.get_device("results_db").record(status="pass") # HTTP service
90
+ ```
91
+
92
+ > **Validation & test engineers can stop here.** One model for the whole rack —
93
+ > no agent needed, no transport code, no glue.
94
+
95
+ **Step 3 · Hand the same rack to an agent** — the tool catalog is generated for you:
96
+
97
+ ```python
98
+ tools = hal.tool_schemas() # one typed tool per device op
99
+ hal.call_tool("dut_power__set_voltage", {"volts": 3.3})
100
+ ```
101
+
102
+ > Writes are gated, reads aren't. The agent never sees SCPI, I²C, or an address.
103
+
104
+ ---
105
+
106
+ ## Why existing agent frameworks fall short
107
+
108
+ Most agent tooling assumes **software-only** tools: APIs, databases, functions.
109
+ The moment a tool is a *physical* device — a sensor on I²C, an instrument over a
110
+ raw socket, a robot behind a network hop — you're on your own.
111
+
112
+ SHAL exposes physical devices, remote labs, instruments, **and** software
113
+ services as the *same* kind of tool — with the safety rails physical actions
114
+ need: gated writes, honest failure, a full audit trail.
115
+
116
+ ---
117
+
118
+ ## One model for hardware *and* software
119
+
120
+ The core idea is small:
121
+
122
+ > **A bus is just a node that provides a transport to its children.**
123
+
124
+ A sensor on I²C and an HTTP service are the same kind of node. Your code — and
125
+ your agent — calls **capabilities** (`read_celsius()`, `set_voltage()`), never
126
+ transports. `[core]` ships with SHAL; `[pkg]` is a driver you install or write.
127
+
128
+ ```yaml
129
+ # lab.yaml — hardware and software in ONE graph
130
+ shal_version: 1
131
+ root:
132
+ bench: # one SSH hop to the bench controller [core]
133
+ driver: shal,ssh-host
134
+ address: ${BENCH_SSH} # secrets resolve from the environment, never logged
135
+ children:
136
+ i2c0: # I²C rendered as argv over the SSH hop [core]
137
+ driver: shal,i2c-cli
138
+ address: /dev/i2c-1
139
+ children:
140
+ ambient: { id: ambient_temp, driver: ti,tmp102, address: 0x48 } # [core]
141
+
142
+ instruments: # raw-socket SCPI bus [pkg]
143
+ driver: acme,scpi
144
+ address: 10.0.0.50:5025
145
+ children:
146
+ supply: { id: dut_power, driver: keysight,e36312, address: ch1 } # [pkg]
147
+
148
+ services: # HTTPS to internal services [core]
149
+ driver: shal,http
150
+ address: https://mes.lab.internal
151
+ children:
152
+ results: { id: results_db, driver: acme,mes-results, address: api/v2 } # [pkg]
153
+ ```
154
+
155
+ Every node is reached the same way — `hal.get_device("dut_power").set_voltage(3.3)`
156
+ — sensor or database, local or across the network. Same retries, same logs. Swap
157
+ any node for its sim and **nothing in your code changes**.
158
+
159
+ ---
160
+
161
+ ## Features
162
+
163
+ - **Agent-native** — every device op becomes a gated LLM tool.
164
+ - **Asks before it moves** — actuator & destructive/config ops stop for a
165
+ host-supplied approver (CLI prompt, an agent, or auto in sim/CI); the gate is
166
+ pre-I/O and unbypassable.
167
+ - **Hardware + software, one graph** — a sensor and an HTTP service are the same node.
168
+ - **Capabilities, not wires** — call `read_celsius()`, never I²C.
169
+ - **Retry you can trust** — reads auto-retry; risky writes never silently repeat.
170
+ - **Sim-first** — test the whole rack with zero hardware.
171
+ - **Recursive** — muxes, jumpboxes, nested buses: one primitive, no special cases.
172
+ - **Drivers as plugins** — add a device in one small class.
173
+ - **Secure by default** — no shell strings, TLS on, secrets via `${ENV}`.
174
+ - **Observable** — structured logs, one `txn` id per call.
175
+
176
+ ---
177
+
178
+ ## Install
179
+
180
+ > SHAL is in **alpha** (Phase 1). A PyPI release is coming; for now install from
181
+ > source:
182
+
183
+ ```bash
184
+ pip install git+https://github.com/determlab/shal
185
+
186
+ # or, for development
187
+ git clone https://github.com/determlab/shal && cd shal
188
+ pip install -e ".[dev]" # pytest, ruff
189
+ ```
190
+
191
+ Requires **Python ≥ 3.10**. Dependencies: `pyyaml`, `jsonschema`.
192
+
193
+ ---
194
+
195
+ ## Quick Start
196
+
197
+ Runs with **zero hardware** — the simulated bus ships with SHAL. New to hardware?
198
+ This is the whole setup, and it's just Python and a YAML file.
199
+
200
+ ```yaml
201
+ # sim.yaml
202
+ shal_version: 1
203
+ root:
204
+ bus:
205
+ driver: shal,sim-i2c
206
+ address: sim0
207
+ children:
208
+ temp0:
209
+ id: ambient_temp
210
+ driver: ti,tmp102
211
+ address: 0x48
212
+ ```
213
+
214
+ ```python
215
+ import shal
216
+
217
+ with shal.load("sim.yaml") as hal:
218
+ print(hal.get_device("ambient_temp").read_celsius()) # 25.0
219
+ ```
220
+
221
+ ```bash
222
+ $ python quickstart.py
223
+ 25.0
224
+ ```
225
+
226
+ When the real board arrives, change `shal,sim-i2c` → `shal,i2c-cli`. **Your
227
+ Python doesn't change.**
228
+
229
+ ---
230
+
231
+ ## Write a driver in 30 seconds
232
+
233
+ Need a device SHAL doesn't have yet? A driver is one small class. This is the
234
+ *entire* bundled temperature-sensor driver:
235
+
236
+ ```python
237
+ from shal import Driver, TemperatureSensor, registry, idempotent, op, ByteTransport, Read, Write
238
+
239
+ @registry.register
240
+ class Tmp102(Driver, TemperatureSensor):
241
+ compatible = "ti,tmp102" # matched against the YAML `driver:` field
242
+ kind = ByteTransport
243
+ llm_ready = True
244
+
245
+ @idempotent # a read: safe to auto-retry across drops
246
+ @op("Read the ambient temperature now.", unit="celsius", side_effect="none")
247
+ def read_celsius(self) -> float:
248
+ raw = self.bus.txn(self.addr, [Write(b"\x00"), Read(2)])
249
+ return ((raw[0] << 4) | (raw[1] >> 4)) * 0.0625
250
+ ```
251
+
252
+ That's it — register the `compatible`, implement the capability. The `@op`
253
+ metadata is what makes it show up as a gated agent tool. SHAL discovers your
254
+ driver via the `shal.drivers` entry point.
255
+
256
+ ---
257
+
258
+ ## How It Works
259
+
260
+ A topology is a tree, and **every edge is a bus** — itself a node that carries
261
+ traffic to its children. You call a capability; SHAL translates it down the stack
262
+ to the wire and hands the result back up. No layer leaks into the one above.
263
+
264
+ ```mermaid
265
+ sequenceDiagram
266
+ participant U as Your code
267
+ participant T as tmp102 driver
268
+ participant I as i2c-cli bus
269
+ participant S as ssh-host bus
270
+ U->>T: read_celsius()
271
+ T->>I: read register 0x00
272
+ I->>S: i2ctransfer 0x48 … (argv)
273
+ Note over S: runs on lab_server
274
+ S-->>I: raw bytes
275
+ I-->>T: raw bytes
276
+ T-->>U: 22.5 °C
277
+ ```
278
+
279
+ Because every hop is the same primitive, an SSH jumpbox, an I²C mux, and an
280
+ in-process sim all compose — no special cases.
281
+
282
+ ---
283
+
284
+ ## Core Concepts
285
+
286
+ | Concept | What it means |
287
+ |---|---|
288
+ | **Node** | Anything in the tree: a device, a bus, a board. |
289
+ | **Bus** | A node that provides a transport to its children (I²C, SSH, HTTP…). |
290
+ | **Driver** | Bound to a node by its `compatible` string. Implements a capability. |
291
+ | **Capability** | The typed API your code calls (`read_celsius()`), independent of transport. |
292
+ | **id vs path** | `id` is a stable name for lookup; `path` is where it sits. Move a device, keep its `id`. |
293
+
294
+ ```python
295
+ hal.get_device("ambient_temp").read_celsius() # by semantic id — no wires leak in
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Real-World Use Cases
301
+
302
+ - **AI agents with real-world access** — expose a lab or robot to an LLM as
303
+ gated tools; every actuator call stops for a human (or policy) to approve
304
+ before it fires, and a delivery-unknown write is never silently retried.
305
+ - **Validation & test racks** — one model for eval boards, instruments, and the
306
+ results database; test against sims in CI before hardware.
307
+ - **Manufacturing lines** — same capability calls across stations; one audit
308
+ trail (`shal.audit`) for every actuator command.
309
+ - **Remote & distributed setups** — drive hardware behind an SSH jumpbox with
310
+ nothing on the far side but standard CLI tools.
311
+ - **Robotics bringup** — start against a sim, swap in transports as boards land,
312
+ without rewriting control code.
313
+
314
+ ---
315
+
316
+ ## FAQ
317
+
318
+ **Why not just wrap Python libraries as agent tools myself?**
319
+ You can — until there are ten devices on four transports, some behind an SSH hop,
320
+ some not. Then you're hand-maintaining a tool wrapper, address, retry policy, and
321
+ audit log *per device*. SHAL generates all of it from one topology.
322
+
323
+ **Is it production-ready?**
324
+ It's **alpha** (Phase 1). The synchronous core — topology, drivers, buses, retry
325
+ policy, and the agent tool surface — is real and tested. Async/streaming, the
326
+ actuator watchdog, and route failover are Phase 2 ([roadmap](#roadmap)).
327
+
328
+ **Do I need real hardware to try it?**
329
+ No. The bundled simulated bus runs the [Quick Start](#quick-start) with zero
330
+ hardware — swap in a real transport later, and your code doesn't change.
331
+
332
+ ---
333
+
334
+ ## Roadmap
335
+
336
+ **Shipped — Phase 1 (synchronous core, v0.1.0):**
337
+
338
+ - ✅ Declarative YAML topology: JSON-Schema validation, `id`/`path`/`$ref`,
339
+ `${ENV}` secrets, reusable `template:` includes
340
+ - ✅ Bundled buses: `sim-i2c`, `local`, `ssh-host`, `i2c-cli`, `spi-cli`,
341
+ `tcp` (TLS), `http`, `nxp,pca9548` mux
342
+ - ✅ Capability model, driver plugin registry, trustworthy retry policy
343
+ - ✅ Agent tool surface: `tool_schemas()` / `tool_catalog()` / `call_tool()`
344
+ - ✅ Human-in-the-loop actuation gate: actuator ops stop for an injectable
345
+ `Approver` (pre-I/O, unbypassable, every decision audited)
346
+ - ✅ Structured observability + `capture()` flight recorder
347
+
348
+ **Designed, in progress — Phase 2:**
349
+
350
+ - 🚧 Async / streaming (`subscribe`, held channels) — [spec](docs/DESIGN%20-%20PHASE%202%20ASYNC.md)
351
+ - 🚧 Actuator watchdog & safe-state (timeouts, auto safe-state on disconnect)
352
+ - 🚧 Route failover for multi-path devices
353
+
354
+ ---
355
+
356
+ ## Documentation
357
+
358
+ - [Architecture & locked decisions](docs/DESIGN%20V2.md)
359
+ - [Phase 1 implementation decisions](docs/DECISIONS%20-%20V2.1.md)
360
+ - [Phase 2 async + watchdog spec](docs/DESIGN%20-%20PHASE%202%20ASYNC.md)
361
+ - Build guides: write a [driver](.claude/skills/shal-build-driver/SKILL.md),
362
+ a [bus](.claude/skills/shal-build-bus/SKILL.md), or a
363
+ [topology](.claude/skills/shal-build-yaml/SKILL.md)
364
+
365
+ ---
366
+
367
+ ## Contributing
368
+
369
+ Contributions welcome — **new drivers and buses especially**. A driver is one
370
+ small class (see [above](#write-a-driver-in-30-seconds)); SHAL discovers it via
371
+ the `shal.drivers` entry point.
372
+
373
+ ```bash
374
+ pip install -e ".[dev]"
375
+ python -m pytest # test suite
376
+ ruff check src tests # lint
377
+ ```
378
+
379
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide.
380
+
381
+ ---
382
+
383
+ ## License
384
+
385
+ [MIT](LICENSE).