Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
- run: cargo clippy --all-targets --all-features -- -D warnings
- run: cargo test --all-targets --all-features
- run: cargo test --no-default-features --features sim,mcp
- run: cargo run --features mcp --bin leash-schema -- --check
- run: cargo package --locked
- run: scripts/smoke-all.sh

Expand Down
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "leash-harness"
version = "0.1.1"
edition = "2021"
default-run = "leash"
license = "MIT"
description = "Composable local-LLM and robotics harness with MCP, CLI, and safe robot adapters"
repository = "https://github.com/specdog/leash"
Expand All @@ -17,6 +18,7 @@ include = [
"README.md",
"docs/**",
"examples/**",
"schemas/**",
"scripts/**",
"src/**",
]
Expand All @@ -25,6 +27,11 @@ include = [
name = "leash"
path = "src/bin/leash.rs"

[[bin]]
name = "leash-schema"
path = "src/bin/leash-schema.rs"
required-features = ["mcp"]

[features]
default = ["sim", "http", "mcp"]
sim = []
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,20 @@ POST /estop/reset { token, approval } Clear estop
WS /ws/telemetry Streaming telemetry envelope frames
```

## External Schemas And Clients

Leash publishes generated JSON Schema for external tools at
`schemas/leash-messages.schema.json`. Regenerate it from Rust wire types with:

```bash
cargo run --features mcp --bin leash-schema -- --output schemas/leash-messages.schema.json
```

CI checks schema freshness with `--check`, and
[docs/SCHEMAS.md](docs/SCHEMAS.md) documents compatibility rules. Standard-library
Python and Node examples in `examples/clients/` read `/health`, consume
`/telemetry`, and invoke `POST /stop` against sim HTTP.

## Viewer Frames

`WS /ws/telemetry`, `GET /events/telemetry`, and `GET /sse/telemetry` emit
Expand Down
35 changes: 35 additions & 0 deletions docs/SCHEMAS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Message Schemas

Leash publishes JSON Schema for the wire messages external tools consume:

- HTTP health, capabilities, telemetry, stream frames, and stop responses
- MCP HTTP status, tool list, module map, and call responses
- capability descriptors, safety classes, module graph messages, and adapter messages
- perception, visualization, planner, patrol, spatial-memory, drone, and manipulator payloads

The canonical artifact is [schemas/leash-messages.schema.json](../schemas/leash-messages.schema.json).
It is generated from Rust `serde` + `schemars` types:

```bash
cargo run --features mcp --bin leash-schema -- --output schemas/leash-messages.schema.json
```

CI runs the generator in `--check` mode. If a Rust wire type changes without
updating the checked-in schema, CI fails.

## Compatibility Rules

`schema_version` is the external message contract version. Change it when a
consumer must update code to keep parsing messages safely, including field
removal, field rename, enum value removal or rename, or a required-field change.

Backward-compatible changes keep the same `schema_version`: adding optional
fields, adding fields with serde defaults, adding new schemas, widening numeric
ranges, or adding enum values that clients can safely ignore. Consumers should
ignore unknown object fields and switch on known enum values with an explicit
fallback path.

Versioned payloads such as `visualization.version` and manipulator `version`
stay scoped to that nested payload. A nested payload version bump does not
require a top-level `schema_version` bump unless the cross-message contract also
breaks.
16 changes: 16 additions & 0 deletions examples/clients/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# External Client Examples

These examples use only language standard libraries. Start sim HTTP first:

```bash
cargo run -- serve http --profile sim --listen 127.0.0.1:8000
```

Then run either client:

```bash
LEASH_URL=http://127.0.0.1:8000 python3 examples/clients/python/http_client.py
LEASH_URL=http://127.0.0.1:8000 node examples/clients/node/http-client.mjs
```

Each client reads `/health`, consumes `/telemetry`, and invokes `POST /stop`.
31 changes: 31 additions & 0 deletions examples/clients/node/http-client.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const baseUrl = (process.env.LEASH_URL || "http://127.0.0.1:8000").replace(/\/$/, "");

async function requestJson(method, path) {
const response = await fetch(`${baseUrl}${path}`, { method });
if (!response.ok) {
throw new Error(`${method} ${path} returned HTTP ${response.status}`);
}
return response.json();
}

const health = await requestJson("GET", "/health");
const telemetry = await requestJson("GET", "/telemetry");
const stop = await requestJson("POST", "/stop");

if (health.ok !== true) {
throw new Error("health did not report ok=true");
}
if (!telemetry.robot || telemetry.profile !== "sim") {
throw new Error("telemetry did not look like a sim frame");
}
if (stop.ok !== true) {
throw new Error("stop did not report ok=true");
}

console.log(JSON.stringify({
ok: true,
runtime: "node",
profile: health.profile,
robot: telemetry.robot,
stop,
}));
40 changes: 40 additions & 0 deletions examples/clients/python/http_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import json
import os
import urllib.request


BASE_URL = os.environ.get("LEASH_URL", "http://127.0.0.1:8000").rstrip("/")


def request_json(method, path):
request = urllib.request.Request(f"{BASE_URL}{path}", method=method)
if method == "POST":
request.data = b""
with urllib.request.urlopen(request, timeout=5) as response:
return json.loads(response.read().decode("utf-8"))


def main():
health = request_json("GET", "/health")
telemetry = request_json("GET", "/telemetry")
stop = request_json("POST", "/stop")

if health.get("ok") is not True:
raise SystemExit("health did not report ok=true")
if not telemetry.get("robot") or telemetry.get("profile") != "sim":
raise SystemExit("telemetry did not look like a sim frame")
if stop.get("ok") is not True:
raise SystemExit("stop did not report ok=true")

print(json.dumps({
"ok": True,
"runtime": "python",
"profile": health["profile"],
"robot": telemetry["robot"],
"stop": stop,
}, sort_keys=True))


if __name__ == "__main__":
main()
17 changes: 17 additions & 0 deletions schemas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Leash Schemas

`leash-messages.schema.json` is generated from Rust wire types with:

```bash
cargo run --features mcp --bin leash-schema -- --output schemas/leash-messages.schema.json
```

CI checks the file with:

```bash
cargo run --features mcp --bin leash-schema -- --check
```

The top-level `schema_version` changes only when the external message contract
has a breaking change. Additive optional fields and fields with serde defaults
are backward compatible under the same version.
Loading
Loading