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). A PyPI release is coming; for now install from source:

pip install git+https://github.com/determlab/shal

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

Requires Python ≥ 3.10. 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.


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.1.0.tar.gz (131.1 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.1.0-py3-none-any.whl (69.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pyshal-0.1.0.tar.gz
  • Upload date:
  • Size: 131.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for pyshal-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f669b34dfb68e4d9e577dbd1fec5d4af664d9b1a8a0bd65fbf36c4cb6b0c9aee
MD5 06c1e00a5b6a4cd906e28d393ec4c063
BLAKE2b-256 9dc353439fa478516f589c8da7df38f109c7b73654ba4ab966a6e80d3c5b4a3a

See more details on using hashes here.

File details

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

File metadata

  • Download URL: pyshal-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 69.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for pyshal-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 075cfd8aa3e403682e19fa40ad57b61ae1c53d25e2a0d9aa64d339f9f6689355
MD5 d0d098ccece5d10f2159fcdc14fd21fe
BLAKE2b-256 f196ebf2110b56f1680716aaf1321d602aa079543687d8739ab1bcd409f406d5

See more details on using hashes here.

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