# leash
> Composable local-LLM and robotics harness. Rust-first, MCP-first, simulation-safe.
Leash is a Rust harness runtime that lets LLM agents control robots through typed modules, safety gates, and a shared capability registry. Run in simulation with zero hardware, or connect physical robots behind explicit actuation gates.
```bash
cargo install leash-harness
leash list # built-in stacks
leash run sim-mcp # MCP stdio for LLM agents
leash run sim-http # localhost HTTP + WebSocket
leash serve mcp-http # localhost MCP JSON control surface
```
## Why Leash
- **Simulation-safe by default.** CI, demos, and smoke tests require zero hardware. Physical actuation is an explicit opt-in gate.
- **MCP-native.** Agents get 8 typed tools (health, capabilities, modules, observe, invoke_capability, stop, estop, capture) over stdio or localhost MCP HTTP.
- **Safety gates at every layer.** Deadman switch, estop, soft odometry limits, physical actuation gate. Policy-gated capability invocation.
- **Feature-gated hardware.** Waveshare UGV today, MAVLink drone + manipulator planned. No hardware compiles without explicit `--features`.
- **Stack catalog.** Runnable sim, MCP, HTTP, and compatibility demos. `leash list` + `leash run <stack>`.
- **Module graph with typed streams.** Modules declare inputs, outputs, lifecycle, health, and selected stream transport. Coordinator manages startup/shutdown order.
- **Local stream transport.** Module streams can use `local-pubsub` for async fan-out or `memory` for deterministic tests.
- **Deterministic replay.** JSONL record/replay fixtures can drive HTTP and MCP observe paths in non-physical replay mode.
## Repository Map
```mermaid
flowchart TB
root["repo root\nCargo crate, package metadata, top-level README"] --> src["src/\nReusable harness library"]
root --> cli["src/bin/\nleash CLI entrypoint"]
root --> scripts["scripts/\nSmoke tests and bot installer"]
root --> docs["docs/\nOperator, release, and source-map docs"]
root --> examples["examples/\nSafe runnable examples and fixtures"]
root --> specs["specs/leash/\nDotDog project graph source and compiled DAG"]
root --> workflows[".github/workflows/\nCI and release automation"]
src --> runtime["runtime.rs\nHarness state, drivers, telemetry"]
src --> capability["capability.rs\nSafety classes and invocation policy"]
src --> http["http.rs\nHTTP, SSE, WebSocket routes"]
src --> mcp["mcp.rs\nMCP tools and transport"]
src --> config["config.rs\nProfiles, env, CLI precedence"]
```
## Runtime Map
```mermaid
flowchart LR
human["Human operator\nCLI or curl"] --> surfaces["Control surfaces\nCLI, HTTP, MCP"]
agent["LLM agent\nMCP stdio or MCP HTTP"] --> surfaces
surfaces --> registry["CapabilityRegistry\nschemas, safety class, policy"]
registry --> harness["Harness runtime\ncommand state, modules, telemetry"]
harness --> drivers{"Profile driver"}
drivers --> sim["sim\nno hardware"]
drivers --> replay["replay\nJSONL fixtures"]
drivers --> ugv["waveshare-ugv\n/dev/ttyTHS1 serial"]
harness --> streams["Telemetry streams\nHTTP, SSE, WebSocket, MCP observe"]
registry --> safety["Safety gates\ntoken, approval, dry-run, deny, estop, deadman"]
```
## Quick Start
```bash
# Install
cargo install leash-harness
# List built-in stacks
leash list
# Run MCP in simulation (zero hardware)
leash run sim-mcp
# Run with HTTP + WebSocket
leash run sim-http
leash agent-send "inspect the battery"
leash agent-interactive
# Run as a daemon and inspect JSONL logs
leash run sim-http --daemon
leash log sim-http --json --module http --lines 20
# Run MCP HTTP for agent or CLI tooling
leash serve mcp-http --listen 127.0.0.1:9990
# Check health
leash health --url http://127.0.0.1:8000
# Replay a deterministic fixture
leash replay examples/replay/sim-basic.jsonl --speed 10
```
## MCP Tools
| `health` | Harness health and safety state |
| `capabilities` | Endpoints, MCP tools, speed modes |
| `modules` | Module graph and stream metadata |
| `observe` | Latest telemetry frame (odometry, battery, sensors) |
| `invoke_capability` | authorize, drive, stop, estop, estop_reset, speed_mode |
| `stop` | Non-latching zero-speed motor stop |
| `estop` | Latch emergency stop until reset |
| `capture` | Deterministic frame capture |
## MCP HTTP And CLI
```bash
leash serve mcp-http --listen 127.0.0.1:9990
leash mcp list-tools
leash mcp status --url http://127.0.0.1:9990
leash mcp modules
leash mcp call health
leash mcp call invoke_capability capability=authorize token=demo ttl_secs=30
leash mcp call invoke_capability --json '{"capability":"speed_mode","speed_mode":"low"}'
```
`/mcp/tools`, `/mcp/status`, `/mcp/modules`, and `/mcp/call` return JSON for
local agents and automation. Module/tool mapping exposes tool names by module
without returning pilot session tokens.
## HTTP Endpoints
```
GET / Local command center dashboard
GET /dashboard Local command center dashboard
POST /dashboard/authorize Authorize pilot token from dashboard form
POST /dashboard/stop Stop through shared capability registry
POST /dashboard/estop Latch estop through shared capability registry
POST /dashboard/estop-reset Clear estop through shared capability registry
POST /dashboard/capture Capture frame through shared capability registry
GET /health Harness health
GET /capabilities Endpoints + tools + stream transport
GET /telemetry Latest TelemetryFrame
GET /events/telemetry Server-sent telemetry stream
GET /sse/telemetry Alias for /events/telemetry
GET /agent Local web input form
GET /agent/messages Recent agent input messages
POST /agent/messages { source, text }
POST /drive { token, left, right, speed_mode, approval }
POST /estop Latch emergency stop
POST /estop/reset { token, approval } Clear estop
WS /ws/telemetry Streaming telemetry envelope frames
```
## Viewer Frames
`WS /ws/telemetry`, `GET /events/telemetry`, and `GET /sse/telemetry` emit
`TelemetryStreamFrame` JSON. Each frame includes a versioned
`visualization.version = "leash-visualization-v1"` payload with:
- `pose`: map-frame 2D pose
- `path`: viewer-ready pose list
- `occupancy_grid`: compact grid metadata + cells
- `point_cloud`: metadata only, no viewer dependency
- `detections`: generic detection boxes
- `command`: motor command overlay for operators
External viewers can subscribe to the stream endpoints and render those fields
without linking a viewer SDK into the core crate.
## Agent Input
Run a local HTTP stack, then send natural-language text into the runtime without
requiring a model provider:
```bash
leash run sim-http
leash agent-send "inspect the battery"
printf 'look for obstacles\n/quit\n' | leash agent-interactive
```
The command center at `/dashboard` is server-rendered HTML with regular form
posts for authorize, stop, estop, reset, and capture actions. Those forms call
the same shared capability registry used by JSON HTTP, CLI, and MCP paths.
`POST /agent/messages` records a bounded recent inbox and publishes each message
on the `agent` stream for local subscribers. The web form at `/agent` is only
available when the `http` feature is built, and Leash binds HTTP to
`127.0.0.1:8000` by default.
## Agent Model Providers
Leash defaults to `deterministic-test`, a no-network provider for CI and demos.
It returns a stable synthetic response with each agent input acknowledgement.
Hosted and local HTTP providers can be configured and validated without printing
secrets:
```bash
LEASH_AGENT_API_KEY=... \
leash show-config \
--agent-provider openai-compatible-http \
--agent-base-url https://example.test/v1 \
--agent-model demo-model
leash show-config \
--agent-provider local-http \
--agent-base-url http://127.0.0.1:11434/v1
```
Provider settings are available as `--agent-provider`, `--agent-model`,
`--agent-base-url`, `--agent-api-key`, and `--agent-timeout-ms`, or via matching
`LEASH_AGENT_*` environment variables. `agent_api_key` is redacted from
`show-config` fields and omitted from top-level serialized config output.
## Safety Policy
Capability invocations carry safety classes: `observe-only`, `sim-control`,
`physical-stop`, `physical-motion`, and `physical-high-risk`. `drive` is
`physical-motion`; `stop` and `estop` are `physical-stop`; `estop_reset` is
`physical-high-risk`.
`--policy-mode` and `LEASH_POLICY_MODE` support:
- `require-token` (default): physical motion and high-risk calls need a token.
- `require-approval`: physical motion and high-risk calls need `approval=true`.
- `dry-run`: physical motion and high-risk calls return a dry-run result without
changing command state.
- `deny`: physical motion and high-risk calls are blocked.
`stop` and `estop` remain available under restrictive policy modes. Approved and
denied physical actions are emitted as structured log events. Agent-origin
physical motion always requires `approval=true`; physical profiles still require
`--allow-physical-actuation` or `LEASH_ALLOW_PHYSICAL_ACTUATION=1` before the
harness starts.
## Features
| `sim` | Simulation driver (no hardware) | ✓ |
| `http` | HTTP server + WebSocket | ✓ |
| `mcp` | MCP stdio server | ✓ |
| `waveshare-ugv` | Waveshare UGV physical adapter | opt-in |
| `bridge-compat` | Legacy robot bridge compatibility | opt-in |
## Smoke Tests
```bash
scripts/smoke-all.sh
```
`scripts/smoke-all.sh` runs the no-hardware release proof and prints a JSON
summary covering HTTP routes and policy denial, stdio MCP, physical-gate
refusal, daemon lifecycle, graph export, MCP HTTP + CLI calls, replay HTTP/MCP
observe paths, and config preflight checks.
## Stream Transport
`stream_transport` selects the module-stream backend used by WebSocket and SSE
telemetry streams and shown in graph and capability output:
- `local-pubsub` is the default runtime backend. It uses bounded async broadcast
channels, supports fan-out across local tasks, drops oldest messages under
receiver lag, and reports lag to subscribers.
- `memory` is an unbounded in-process backend for deterministic tests and local
harness checks. Dropped subscribers are pruned on publish.
Use `--stream-transport memory` or `LEASH_STREAM_TRANSPORT=memory` when a run
needs deterministic in-process delivery.
`/ws/telemetry`, `/events/telemetry`, and `/sse/telemetry` emit the same
transport-backed envelope: latest telemetry, health/module state, command state,
and safety state.
## Run Logs and Resource Samples
Daemon runs write structured JSONL logs under the Leash state directory. Each
entry includes `timestamp`, `run_id`, `module`, `event`, `level`, and any
structured fields emitted with the tracing event:
```bash
leash run sim-http --daemon
leash log sim-http --json --module http --lines 20
```
Runtime telemetry keeps process resource sampling off by default. Enable it for
development or field debugging with `--resource-sampling` or
`LEASH_RESOURCE_SAMPLING=1`; use `--no-resource-sampling` to force it off when
an environment or config file enables it.
`leash_harness::stream_processing` provides generic helpers for high-rate
streams: latest-value backpressure, per-key rate limiting, quality filtering,
and timestamp pairing within a tolerance window.
## Replay
`leash record` writes compact `leash-replay-v1` JSONL events for telemetry,
sensors, camera status, and command state. `leash replay` emits those events
with original timing, scaled by `--speed`:
```bash
leash record --output /tmp/leash-demo.jsonl --samples 10 --interval-ms 50
leash replay /tmp/leash-demo.jsonl --speed 20
```
Use a replay source to serve deterministic observations through the normal HTTP
and MCP surfaces without hardware:
```bash
leash serve http --replay-source examples/replay/sim-basic.jsonl
leash serve mcp --replay-source examples/replay/sim-basic.jsonl
```
Replay mode resolves to `profile: replay`, `mode: replay`, `replay: true`, and
`physical: false` / `physical_actuation_enabled: false` in config, health, and
capability output.
Run narrower checks when you need to isolate one surface:
```bash
scripts/smoke-http.sh
scripts/smoke-mcp.sh
scripts/smoke-mcp-http.sh
scripts/smoke-replay-http.sh
scripts/smoke-replay-mcp.sh
scripts/smoke-physical-gate.sh
scripts/smoke-daemon.sh
```
## Roadmap
See [issues](https://github.com/specdog/leash/issues) for the full plan. Highlights:
- [x] Module graph with typed streams, lifecycle, and health aggregation
- [x] Stack catalog: `leash list` + `leash run`
- [x] Replay engine: deterministic sensor record + playback
- [x] Transport abstraction: memory + local async pubsub
- [x] Stream processing helpers: latest-value backpressure, quality filters, timestamp pairing
- [x] Agent input channels: one-shot CLI, interactive CLI, and localhost web input
- [ ] Cross-process and network transports
- [ ] MAVLink drone + manipulator adapters
- [x] Localhost command center dashboard
- [x] Viewer-ready visualization frames
- [ ] Spatial memory and perception primitives
- [ ] Patrol and exploration in simulation
- [x] Full no-hardware smoke suite
## License
MIT