Skip to main content

System/Software Hardware Abstraction Layer — device-tree-inspired, dynamic, user-space, network-capable

Project description

SHAL

Give an AI agent real hardware — gated, so it asks before it moves.

Describe your whole lab — sensors, instruments, robots, services — in one YAML file, and hand it to an LLM as typed, permission-gated tools: writes stop for approval before they reach the device; reads don't. No transport code, no glue.

In a blind test, an agent wrote working, safety-checked drivers for 4 of 4 devices from documentation alone — then drove a real robot, gated.

GitHub stars PyPI Python License Status

SHAL turns your lab and services into one YAML topology — controlled from Python or exposed to an AI agent as typed, gated tools

→ Try it in 60 seconds — no hardware required

Built for:

✓ AI agent builders  ·  ✓ Validation & test engineers  ·  ✓ Hardware-in-the-loop automation  ·  ✓ Labs with mixed hardware + software


From glue scripts to agent tools — in three steps

Step 1 · Today, without SHAL — a separate library, address, and retry per device:

sensor  = TMP102(i2c_bus, 0x48)
supply  = SCPIPowerSupply("10.0.0.50:5025")
results = RESTClient("https://mes.lab.internal")
# ...and you wire each one's retries, logging, and tool-wrapper by hand

Step 2 · With SHAL — describe the rack once, then call devices by name:

hal = shal.load("lab.yaml")

hal.get_device("ambient_temp").read_celsius()       # I²C sensor
hal.get_device("dut_power").set_voltage(3.3)        # SCPI supply
hal.get_device("results_db").record(status="pass")  # HTTP service

Validation & test engineers can stop here. One model for the whole rack — no agent needed, no transport code, no glue.

Step 3 · Hand the same rack to an agent — the tool catalog is generated for you:

tools = hal.tool_schemas()                           # one typed tool per device op
hal.call_tool("dut_power__set_voltage", {"volts": 3.3})

Writes are gated, reads aren't. The agent never sees SCPI, I²C, or an address.


Why existing agent frameworks fall short

Most agent tooling assumes software-only tools: APIs, databases, functions. The moment a tool is a physical device — a sensor on I²C, an instrument over a raw socket, a robot behind a network hop — you're on your own.

SHAL exposes physical devices, remote labs, instruments, and software services as the same kind of tool — with the safety rails physical actions need: gated writes, honest failure, a full audit trail.


One model for hardware and software

The core idea is small:

A bus is just a node that provides a transport to its children.

A sensor on I²C and an HTTP service are the same kind of node. Your code — and your agent — calls capabilities (read_celsius(), set_voltage()), never transports. [core] ships with SHAL; [pkg] is a driver you install or write.

# lab.yaml — hardware and software in ONE graph
shal_version: 1
root:
  bench:                         # one SSH hop to the bench controller       [core]
    driver: shal,ssh-host
    address: ${BENCH_SSH}        # secrets resolve from the environment, never logged
    children:
      i2c0:                      # I²C rendered as argv over the SSH hop      [core]
        driver: shal,i2c-cli
        address: /dev/i2c-1
        children:
          ambient: { id: ambient_temp, driver: ti,tmp102, address: 0x48 }   # [core]

  instruments:                   # raw-socket SCPI bus                        [pkg]
    driver: acme,scpi
    address: 10.0.0.50:5025
    children:
      supply: { id: dut_power, driver: keysight,e36312, address: ch1 }       # [pkg]

  services:                      # HTTPS to internal services                [core]
    driver: shal,http
    address: https://mes.lab.internal
    children:
      results: { id: results_db, driver: acme,mes-results, address: api/v2 } # [pkg]

Every node is reached the same way — hal.get_device("dut_power").set_voltage(3.3) — sensor or database, local or across the network. Same retries, same logs. Swap any node for its sim and nothing in your code changes.


Features

  • Agent-native — every device op becomes a gated LLM tool.
  • Asks before it moves — actuator & destructive/config ops stop for a host-supplied approver (CLI prompt, an agent, or auto in sim/CI); the gate is pre-I/O and unbypassable.
  • Hardware + software, one graph — a sensor and an HTTP service are the same node.
  • Capabilities, not wires — call read_celsius(), never I²C.
  • Retry you can trust — reads auto-retry; risky writes never silently repeat.
  • Sim-first — test the whole rack with zero hardware.
  • Recursive — muxes, jumpboxes, nested buses: one primitive, no special cases.
  • Drivers as plugins — add a device in one small class.
  • Secure by default — no shell strings, TLS on, secrets via ${ENV}.
  • Observable — structured logs, one txn id per call.

Install

SHAL is in alpha (Phase 1).

pip install pyshal      # package is `pyshal`; you import it as `shal`
import shal

For development:

git clone https://github.com/determlab/shal && cd shal
pip install -e ".[dev]"   # pytest, ruff

Requires Python ≥ 3.10 for SHAL core. A device's own library may need newer — the featured Deebot path (deebot-client) needs 3.11+ (asyncio.TaskGroup); build that venv on 3.11+. Dependencies: pyyaml, jsonschema.


Quick Start

Runs with zero hardware — the simulated bus ships with SHAL. New to hardware? This is the whole setup, and it's just Python and a YAML file.

# sim.yaml
shal_version: 1
root:
  bus:
    driver: shal,sim-i2c
    address: sim0
    children:
      temp0:
        id: ambient_temp
        driver: ti,tmp102
        address: 0x48
import shal

with shal.load("sim.yaml") as hal:
    print(hal.get_device("ambient_temp").read_celsius())   # 25.0
$ python quickstart.py
25.0

When the real board arrives, change shal,sim-i2cshal,i2c-cli. Your Python doesn't change.


Drive it from an agent (MCP)

Expose a whole topology to an MCP host (Claude Code/Desktop, …) as gated tools — no glue:

pip install "pyshal[mcp]"
shal mcp lab.yaml            # reads run free; writes ask a human first

Register it with your host (example mcpServers block):

{"mcpServers": {"shal": {"command": "shal", "args": ["mcp", "lab.yaml"]}}}

Now tell the agent "read the DUT temperature" (runs immediately) or "set 3.3 V" — a write pauses: the agent gets an approval_required ticket and a human approves the shal_approve tool before anything reaches hardware. Opt into free writes with --approve auto (the choice is recorded in the audit log).

Already own a device with a Python library? Wrapping it as a SHAL driver is a few lines — see the ready-to-edit examples in examples/demos/ (a Sonos speaker, a Deebot vacuum), then point SHAL at your topology:

shal probe my-setup.yaml   # one-shot: print your devices' state, then exit
shal mcp   my-setup.yaml   # or serve it to an AI host (writes gated)

Write a driver in 30 seconds

Need a device SHAL doesn't have yet? A driver is one small class. This is the entire bundled temperature-sensor driver:

from shal import Driver, TemperatureSensor, registry, idempotent, op, ByteTransport, Read, Write

@registry.register
class Tmp102(Driver, TemperatureSensor):
    compatible = "ti,tmp102"          # matched against the YAML `driver:` field
    kind = ByteTransport
    llm_ready = True

    @idempotent                        # a read: safe to auto-retry across drops
    @op("Read the ambient temperature now.", unit="celsius", side_effect="none")
    def read_celsius(self) -> float:
        raw = self.bus.txn(self.addr, [Write(b"\x00"), Read(2)])
        return ((raw[0] << 4) | (raw[1] >> 4)) * 0.0625

That's it — register the compatible, implement the capability. The @op metadata is what makes it show up as a gated agent tool. SHAL discovers your driver via the shal.drivers entry point.


How It Works

A topology is a tree, and every edge is a bus — itself a node that carries traffic to its children. You call a capability; SHAL translates it down the stack to the wire and hands the result back up. No layer leaks into the one above.

sequenceDiagram
    participant U as Your code
    participant T as tmp102 driver
    participant I as i2c-cli bus
    participant S as ssh-host bus
    U->>T: read_celsius()
    T->>I: read register 0x00
    I->>S: i2ctransfer 0x48 … (argv)
    Note over S: runs on lab_server
    S-->>I: raw bytes
    I-->>T: raw bytes
    T-->>U: 22.5 °C

Because every hop is the same primitive, an SSH jumpbox, an I²C mux, and an in-process sim all compose — no special cases.


Core Concepts

Concept What it means
Node Anything in the tree: a device, a bus, a board.
Bus A node that provides a transport to its children (I²C, SSH, HTTP…).
Driver Bound to a node by its compatible string. Implements a capability.
Capability The typed API your code calls (read_celsius()), independent of transport.
id vs path id is a stable name for lookup; path is where it sits. Move a device, keep its id.
hal.get_device("ambient_temp").read_celsius()   # by semantic id — no wires leak in

Real-World Use Cases

  • AI agents with real-world access — expose a lab or robot to an LLM as gated tools; every actuator call stops for a human (or policy) to approve before it fires, and a delivery-unknown write is never silently retried.
  • Validation & test racks — one model for eval boards, instruments, and the results database; test against sims in CI before hardware.
  • Manufacturing lines — same capability calls across stations; one audit trail (shal.audit) for every actuator command.
  • Remote & distributed setups — drive hardware behind an SSH jumpbox with nothing on the far side but standard CLI tools.
  • Robotics bringup — start against a sim, swap in transports as boards land, without rewriting control code.

FAQ

Why not just wrap Python libraries as agent tools myself? You can — until there are ten devices on four transports, some behind an SSH hop, some not. Then you're hand-maintaining a tool wrapper, address, retry policy, and audit log per device. SHAL generates all of it from one topology.

Is it production-ready? It's alpha (Phase 1). The synchronous core — topology, drivers, buses, retry policy, and the agent tool surface — is real and tested. Async/streaming, the actuator watchdog, and route failover are Phase 2 (roadmap).

Do I need real hardware to try it? No. The bundled simulated bus runs the Quick Start with zero hardware — swap in a real transport later, and your code doesn't change.


Roadmap

Shipped — Phase 1 (synchronous core, v0.1.0):

  • ✅ Declarative YAML topology: JSON-Schema validation, id/path/$ref, ${ENV} secrets, reusable template: includes
  • ✅ Bundled buses: sim-i2c, local, ssh-host, i2c-cli, spi-cli, tcp (TLS), http, nxp,pca9548 mux
  • ✅ Capability model, driver plugin registry, trustworthy retry policy
  • ✅ Agent tool surface: tool_schemas() / tool_catalog() / call_tool()
  • ✅ Human-in-the-loop actuation gate: actuator ops stop for an injectable Approver (pre-I/O, unbypassable, every decision audited)
  • ✅ Structured observability + capture() flight recorder

Designed, in progress — Phase 2:

  • 🚧 Async / streaming (subscribe, held channels) — spec
  • 🚧 Actuator watchdog & safe-state (timeouts, auto safe-state on disconnect)
  • 🚧 Route failover for multi-path devices

Documentation


Contributing

Contributions welcome — new drivers and buses especially. A driver is one small class (see above); SHAL discovers it via the shal.drivers entry point.

pip install -e ".[dev]"
python -m pytest          # test suite
ruff check src tests      # lint

See CONTRIBUTING.md for the full guide.


License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyshal-0.2.1.tar.gz (109.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyshal-0.2.1-py3-none-any.whl (96.8 kB view details)

Uploaded Python 3

File details

Details for the file pyshal-0.2.1.tar.gz.

File metadata

  • Download URL: pyshal-0.2.1.tar.gz
  • Upload date:
  • Size: 109.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyshal-0.2.1.tar.gz
Algorithm Hash digest
SHA256 a7f9f7222301ae50f1c652fa8e0cbfa43e25d7de6360d5b45c18102560d06f5a
MD5 e27ff89370d19e8087f5f9fed09636c9
BLAKE2b-256 de54f7f1280ceb131a93a54da618e1ec79d6f85db09f8a63eb0498c1abac8278

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyshal-0.2.1.tar.gz:

Publisher: release.yml on determlab/shal

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyshal-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: pyshal-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 96.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyshal-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 61e121a83ca9a1541340199183e05251f414347141b6e460927b7a65f27d490e
MD5 f4ab318319b1499ad8b8284e6806ce5b
BLAKE2b-256 dadc42728c0cda1bd361989454801c2afbb0dab025c98905ab2c9d643148241f

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyshal-0.2.1-py3-none-any.whl:

Publisher: release.yml on determlab/shal

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page