Skip to main content

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.

License Python

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 dispatchthis 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

  1. Caller POSTs to /dispatch with:
    • Authorization: Bearer <tier-token> — determines actuate vs read tier
    • X-Anthropic-Api-Key: sk-ant-... — the caller's own Anthropic key; inference bills to them
    • Body {"goal": "<natural language task>"}
  2. The dispatcher spins up a fresh ClaudeSDKClient for this request with env={"ANTHROPIC_API_KEY": <caller_key>}, mcp_servers={"robot": {"type":"stdio","command":"robot-md-mcp"}}, and a system_prompt that includes the full ROBOT.md.
  3. 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.
    • PreToolUse audit hook — writes (caller, tier, key-fingerprint, tool, args) to the journal.
  4. 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 — including estop and estop_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_turns and max_budget_usd are Pydantic-bounded; the systemd unit adds MemoryMax, CPUQuota, and TasksMax at 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


Download files

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

Source Distribution

robot_md_dispatcher-0.2.0.tar.gz (47.4 kB view details)

Uploaded Source

Built Distribution

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

robot_md_dispatcher-0.2.0-py3-none-any.whl (20.4 kB view details)

Uploaded Python 3

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

Hashes for robot_md_dispatcher-0.2.0.tar.gz
Algorithm Hash digest
SHA256 46017c2f6baa317852db023f5440b61e1bdbc5242767b73ed16f7ca0024ebb5c
MD5 1f2cf743d927015740ea5eb6692c17e7
BLAKE2b-256 4540e190da12fde05223b73e9e2ce81125c68a52a484ddd1b4eeda8ef97f8489

See more details on using hashes here.

Provenance

The following attestation bundles were made for robot_md_dispatcher-0.2.0.tar.gz:

Publisher: release.yml on RobotRegistryFoundation/robot-md-dispatcher

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

File details

Details for the file robot_md_dispatcher-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for robot_md_dispatcher-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 90ed2c2b6b3cbe2b3260ab7ebd63a8e2c2c8f94c866cb766d1365c2dacb35ffe
MD5 554e20e2c93bcda58bf9c9c6cd5c2933
BLAKE2b-256 b307472b12d241d44ddaf4ef3ac074b7cc7997544ec40b0ec9bcd5dc1e351cb0

See more details on using hashes here.

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

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