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.
- pyshal-0.1.0/CHANGELOG.md +119 -0
- pyshal-0.1.0/CONTRIBUTING.md +58 -0
- pyshal-0.1.0/LICENSE +21 -0
- pyshal-0.1.0/MANIFEST.in +14 -0
- pyshal-0.1.0/PKG-INFO +385 -0
- pyshal-0.1.0/README.md +352 -0
- pyshal-0.1.0/docs/CATALOG.md +643 -0
- pyshal-0.1.0/docs/DECISIONS - V2.1.md +63 -0
- pyshal-0.1.0/docs/DESIGN - PHASE 2 ASYNC.md +804 -0
- pyshal-0.1.0/docs/DESIGN - V1.md +413 -0
- pyshal-0.1.0/docs/DESIGN V2.md +719 -0
- pyshal-0.1.0/docs/PROPOSAL - SW TOPOLOGIES.md +191 -0
- pyshal-0.1.0/docs/SDK.md +385 -0
- pyshal-0.1.0/docs/agents/domain.md +53 -0
- pyshal-0.1.0/docs/agents/issue-tracker.md +22 -0
- pyshal-0.1.0/docs/agents/triage-labels.md +15 -0
- pyshal-0.1.0/pyproject.toml +91 -0
- pyshal-0.1.0/setup.cfg +4 -0
- pyshal-0.1.0/src/pyshal.egg-info/PKG-INFO +385 -0
- pyshal-0.1.0/src/pyshal.egg-info/SOURCES.txt +58 -0
- pyshal-0.1.0/src/pyshal.egg-info/dependency_links.txt +1 -0
- pyshal-0.1.0/src/pyshal.egg-info/entry_points.txt +19 -0
- pyshal-0.1.0/src/pyshal.egg-info/requires.txt +6 -0
- pyshal-0.1.0/src/pyshal.egg-info/top_level.txt +1 -0
- pyshal-0.1.0/src/shal/__init__.py +65 -0
- pyshal-0.1.0/src/shal/approval.py +143 -0
- pyshal-0.1.0/src/shal/buses/__init__.py +0 -0
- pyshal-0.1.0/src/shal/buses/http_bus.py +74 -0
- pyshal-0.1.0/src/shal/buses/i2c_cli.py +90 -0
- pyshal-0.1.0/src/shal/buses/local.py +51 -0
- pyshal-0.1.0/src/shal/buses/mux.py +83 -0
- pyshal-0.1.0/src/shal/buses/scpi_raw.py +110 -0
- pyshal-0.1.0/src/shal/buses/sim.py +232 -0
- pyshal-0.1.0/src/shal/buses/sim_msg.py +101 -0
- pyshal-0.1.0/src/shal/buses/sim_scpi.py +138 -0
- pyshal-0.1.0/src/shal/buses/spi_cli.py +63 -0
- pyshal-0.1.0/src/shal/buses/ssh.py +74 -0
- pyshal-0.1.0/src/shal/buses/tcp.py +100 -0
- pyshal-0.1.0/src/shal/capabilities.py +67 -0
- pyshal-0.1.0/src/shal/conformance.py +246 -0
- pyshal-0.1.0/src/shal/driver.py +299 -0
- pyshal-0.1.0/src/shal/drivers/__init__.py +0 -0
- pyshal-0.1.0/src/shal/drivers/ads1115.py +48 -0
- pyshal-0.1.0/src/shal/drivers/ina219.py +57 -0
- pyshal-0.1.0/src/shal/drivers/keysight_34461a.py +44 -0
- pyshal-0.1.0/src/shal/drivers/mcp23017.py +57 -0
- pyshal-0.1.0/src/shal/drivers/mcp9808.py +39 -0
- pyshal-0.1.0/src/shal/drivers/rigol_dp832.py +62 -0
- pyshal-0.1.0/src/shal/drivers/tmp102.py +30 -0
- pyshal-0.1.0/src/shal/errors.py +89 -0
- pyshal-0.1.0/src/shal/hal.py +221 -0
- pyshal-0.1.0/src/shal/limits.py +192 -0
- pyshal-0.1.0/src/shal/loader.py +224 -0
- pyshal-0.1.0/src/shal/log.py +76 -0
- pyshal-0.1.0/src/shal/logging.py +113 -0
- pyshal-0.1.0/src/shal/node.py +71 -0
- pyshal-0.1.0/src/shal/py.typed +0 -0
- pyshal-0.1.0/src/shal/registry.py +229 -0
- pyshal-0.1.0/src/shal/schema/shal-v1.schema.json +142 -0
- 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.
|
pyshal-0.1.0/MANIFEST.in
ADDED
|
@@ -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
|
+
[](https://github.com/determlab/shal)
|
|
49
|
+
[](#install)
|
|
50
|
+
[](#install)
|
|
51
|
+
[](LICENSE)
|
|
52
|
+
[](#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 · ✓ Validation & test engineers ·
|
|
67
|
+
✓ Hardware-in-the-loop automation · ✓ 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).
|