BYOK dispatcher for robot-md — accepts tasks over HTTP, runs them through a local Claude Agent SDK session against a ROBOT.md-described robot.
Project description
robot-md-dispatcher
BYOK Claude Agent SDK dispatcher for a
ROBOT.md-described robot. Accepts natural-language tasks over HTTP and runs them through a local Claude agent on the robot host. Reasoning, tool-calling, and audit stay local — only the goal and final result cross the network. Each caller brings their own Anthropic API key.
Where this fits in the stack
robot-md-mcp is the right answer when a human operator drives a robot from an interactive MCP client (Claude Code, Claude Desktop, Cursor, Zed). robot-md-dispatcher is the right answer when an external system — a cron job, a Slack bot, a phone app, another agent — needs to hand a robot a task and wait for the result. You keep the ROBOT.md safety gates; you get tier-gated auth; you don't open remote MCP to the internet.
| Layer | Piece | What it is |
|---|---|---|
| Declaration | ROBOT.md | The file a robot ships at its root. YAML frontmatter + markdown prose. |
| Agent bridge | robot-md-mcp | MCP server that exposes a ROBOT.md to any MCP-aware agent over stdio. |
| Remote dispatch ← this | robot-md-dispatcher | BYOK Claude Agent SDK dispatcher — accepts tasks over HTTP, runs them through a local Claude agent, consumes robot-md-mcp unchanged. |
| Wire protocol | RCAN | How robots, gateways, and planners talk. |
| Registry | Robot Registry Foundation | Permanent RRN identities. |
| Reference runtime | OpenCastor | Open-source robot runtime. |
Why not remote MCP?
Three shapes were considered; this repo is shape (c):
(a) --http on robot-md-mcp |
(b) Python shim around robot-md-mcp | (c) Agent SDK dispatcher | |
|---|---|---|---|
| Reasoning runs at | Remote Claude surface | Remote Claude surface | Robot host |
| Unit crossing the network | Every tool call + every result | Every tool call + every result | Goal + final result |
| Upstream change required | Yes | None (shim) | None (consumer) |
| Sensor data leaves the LAN | Yes | Yes | No |
| Fits "fix upstream first" | Yes | No | Yes |
robot-md-mcp remains a clean stdio server. The dispatcher consumes it; it does not wrap or mirror it.
How it works
- Caller POSTs to
/dispatchwith:Authorization: Bearer <tier-token>— determines actuate vs read tierX-Anthropic-Api-Key: sk-ant-...— the caller's own Anthropic key; inference bills to them- Body
{"goal": "<natural language task>"}
- The dispatcher spins up a fresh
ClaudeSDKClientfor this request withenv={"ANTHROPIC_API_KEY": <caller_key>},mcp_servers={"robot": {"type":"stdio","command":"robot-md-mcp"}}, and asystem_promptthat includes the fullROBOT.md. - The agent loop runs on the robot host. Every tool invocation passes through two gates:
can_use_tool— denies actuation tools (estop,execute_task,record_skill, ...) for read-tier callers. Default-deny: new tools added upstream are gated until you allowlist them.PreToolUseaudit hook — writes(caller, tier, key-fingerprint, tool, args)to the journal.
- Agent messages stream back to the caller as NDJSON.
Quick start
python3 -m venv .venv
.venv/bin/pip install robot-md-dispatcher robot-md # robot-md-mcp ships with robot-md
.venv/bin/robot-md-dispatcher init --yes
.venv/bin/robot-md-dispatcher serve --bearers ./bearers.yaml --robot-md ./ROBOT.md
init --yes writes bearers.yaml, .env, and dispatch-test.sh next to your
ROBOT.md and prints a generated actuate-tier token once. Save the token — it's
not stored anywhere else. Run robot-md-dispatcher init (no --yes) for a
guided walk that explains each knob.
From a Claude Code session with the robot-md-mcp plugin enabled, you can
alternatively run the slash command /enable-dispatch — it runs init --yes
for you but does not print the generated token into the conversation.
Dispatch a task:
curl -N http://127.0.0.1:8080/dispatch \
-H "Authorization: Bearer replace-me-actuate" \
-H "X-Anthropic-Api-Key: sk-ant-..." \
-H "Content-Type: application/json" \
-d '{"goal": "validate the manifest and describe what the robot can do"}'
The response is NDJSON — one JSON object per agent message. Tail it.
Production install
systemd/install.sh handles the full setup: dedicated robot system user, /opt/robot-md-dispatcher/.venv with hardened unit, DeviceAllow=/dev/ttyACM0 rw, MemoryMax=1G, CPUQuota=80%, journal logging.
Run robot-md-dispatcher init --yes first (next to your ROBOT.md) to generate
bearers.yaml, .env, and dispatch-test.sh. Then:
sudo ./systemd/install.sh
sudo cp ./bearers.yaml ./.env /etc/robot-md-dispatcher/
sudo cp ./ROBOT.md /etc/robot-md-dispatcher/ROBOT.md
sudo systemctl daemon-reload && sudo systemctl enable --now robot-md-dispatcher
Ingress — do not port-forward
The dispatcher binds to 127.0.0.1 by design. Expose it via Tailscale Funnel (named, revocable, TLS-terminated):
tailscale serve --bg --https=443 http://127.0.0.1:8080
tailscale funnel 443 on
Safety model
- Default-deny tier gate. Read-tier callers can only invoke allowlisted observation tools (
render,validate,get_*,list_*,describe_*,vision_find,discover,status). Every other tool — includingestopandestop_clear, which are safety-critical but still disruptive — requires actuate-tier. Operators who want read-tier estop must install a custom policy. - ROBOT.md is in the system prompt. Claude sees every safety gate in the manifest on every dispatch. Gate violations are refused with an explanation.
- Resource rails.
max_turnsandmax_budget_usdare Pydantic-bounded; the systemd unit addsMemoryMax,CPUQuota, andTasksMaxat the process level. - Audit log. Every tool call lands in the journal with the caller ID, tier, API-key fingerprint (sha256[:12], never the key itself), tool name, and args.
- No key persistence. The caller's API key lives only in the per-request subprocess env and the in-memory
AuthContext. It is never logged, stored, or forwarded.
BYOK and billing
v0.1 is Bring Your Own Key: every caller supplies their own sk-ant-... and Anthropic bills them directly. This keeps the dispatcher out of the billing path entirely — you host the control plane, your users pay for the inference they request.
A managed billing layer (shared keys, per-caller quotas, Stripe metering) is out of scope for this repo and belongs in downstream infrastructure; see opencastor-ops for that shape.
Configuration
Environment variables (also settable via CLI flags — flags win):
| Variable | Purpose | Default |
|---|---|---|
ROBOT_MD_PATH |
Path to the ROBOT.md loaded into the system prompt |
unset |
ROBOT_MD_BEARERS_FILE |
Path to bearers.yaml |
required |
ROBOT_MD_MCP_COMMAND |
Stdio MCP command the agent connects to | robot-md-mcp |
ROBOT_MD_MCP_ARGS |
Space-separated args for the MCP command | (none) |
ROBOT_MD_LOG_LEVEL |
Python log level | INFO |
Development
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest -q
.venv/bin/ruff check src tests
The test suite mocks the Claude Agent SDK boundary via a ClientFactory protocol, so pytest runs offline and does not require claude CLI, an API key, or the claude-agent-sdk package to actually function — only to import. The tier gate, auth, and HTTP surface are exercised end-to-end with a TestClient.
Real tool names from robot-md-mcp's server are pinned in tests/test_gating.py; if the upstream tool surface shifts in a way that inverts a read/actuate classification, the test fails loudly.
License
Apache-2.0. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file robot_md_dispatcher-0.2.0.tar.gz.
File metadata
- Download URL: robot_md_dispatcher-0.2.0.tar.gz
- Upload date:
- Size: 47.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46017c2f6baa317852db023f5440b61e1bdbc5242767b73ed16f7ca0024ebb5c
|
|
| MD5 |
1f2cf743d927015740ea5eb6692c17e7
|
|
| BLAKE2b-256 |
4540e190da12fde05223b73e9e2ce81125c68a52a484ddd1b4eeda8ef97f8489
|
Provenance
The following attestation bundles were made for robot_md_dispatcher-0.2.0.tar.gz:
Publisher:
release.yml on RobotRegistryFoundation/robot-md-dispatcher
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
robot_md_dispatcher-0.2.0.tar.gz -
Subject digest:
46017c2f6baa317852db023f5440b61e1bdbc5242767b73ed16f7ca0024ebb5c - Sigstore transparency entry: 1376332294
- Sigstore integration time:
-
Permalink:
RobotRegistryFoundation/robot-md-dispatcher@a1bca1509e3a3b903568ce1b478a278ed8f1de82 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/RobotRegistryFoundation
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a1bca1509e3a3b903568ce1b478a278ed8f1de82 -
Trigger Event:
push
-
Statement type:
File details
Details for the file robot_md_dispatcher-0.2.0-py3-none-any.whl.
File metadata
- Download URL: robot_md_dispatcher-0.2.0-py3-none-any.whl
- Upload date:
- Size: 20.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
90ed2c2b6b3cbe2b3260ab7ebd63a8e2c2c8f94c866cb766d1365c2dacb35ffe
|
|
| MD5 |
554e20e2c93bcda58bf9c9c6cd5c2933
|
|
| BLAKE2b-256 |
b307472b12d241d44ddaf4ef3ac074b7cc7997544ec40b0ec9bcd5dc1e351cb0
|
Provenance
The following attestation bundles were made for robot_md_dispatcher-0.2.0-py3-none-any.whl:
Publisher:
release.yml on RobotRegistryFoundation/robot-md-dispatcher
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
robot_md_dispatcher-0.2.0-py3-none-any.whl -
Subject digest:
90ed2c2b6b3cbe2b3260ab7ebd63a8e2c2c8f94c866cb766d1365c2dacb35ffe - Sigstore transparency entry: 1376332338
- Sigstore integration time:
-
Permalink:
RobotRegistryFoundation/robot-md-dispatcher@a1bca1509e3a3b903568ce1b478a278ed8f1de82 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/RobotRegistryFoundation
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a1bca1509e3a3b903568ce1b478a278ed8f1de82 -
Trigger Event:
push
-
Statement type: