robot_lab-discovery
Zero-configuration mDNS/DNS-SD robot discovery for RobotLab on local networks.
Robots advertise themselves via multicast DNS (mDNS, RFC 6762) and find each other without any central registry or hardcoded URLs. Augments robot_lab-a2a but has no dependency on it — any transport can use the discovered URL.
How it works
Each robot registers as a _robot-lab._tcp.local. mDNS service. Its HTTP path, gem version, and capability taxonomy are stored in the TXT record. Each capability is also registered as a DNS-SD subtype, enabling clients to browse by capability type without enumerating all robots.
Advertiser Browser
────────────────────────────────────── ────────────────────────────────────────────
ZeroConf::Service ZeroConf.browse("_robot-lab._tcp.local.")
instance: "headline" → parse SRV + TXT
host: my-server.local → Result(name, hostname, port, path,
port: 9292 capabilities, url)
TXT: path=/headline
capabilities=writing,research
subtypes: _writing._sub._robot-lab._tcp.local.
_research._sub._robot-lab._tcp.local.
ZeroConf.browse("_research._sub._robot-lab._tcp.local.")
→ only research robots
Installation
bundle add robot_lab-discovery
Or add to your Gemfile:
gem "robot_lab-discovery"
System prerequisites
None beyond Ruby >= 3.2. The zeroconf gem is pure Ruby and implements mDNS itself using standard UDP multicast sockets — it does not wrap Bonjour, Avahi, or any system mDNS daemon. A multicast-capable network interface is all that is required, which is standard on any LAN.
Usage
Advertising a robot
require "robot_lab/discovery"
adv = RobotLab::Discovery::Advertiser.new(
name: "headline", # mDNS instance name — no dots allowed
port: 9292,
path: "/headline", # stored in TXT record
hostname: Socket.gethostname,
capabilities: ["writing", "research"] # free-form taxonomy terms
)
adv.start # begins advertising in a background thread
# ... server runs ...
adv.stop # sends mDNS goodbye packet; removes from peers' caches
Capability strings are free-form. They are normalised into valid DNS labels
("Web Search" → "web-search") and registered as DNS-SD subtypes, so peers
can browse for them directly.
Discovering robots
require "robot_lab/discovery"
# All robots on the LAN
results = RobotLab::Discovery.browse(timeout: 3)
results.each { |r| puts "#{r.name}: #{r.url} caps=#{r.capabilities.join(", ")}" }
# Find one robot by name
result = RobotLab::Discovery.find("headline", timeout: 5)
result.url # => "http://my-server.local:9292/headline"
result.capabilities # => ["writing", "research"]
# Find all robots offering a specific capability
researchers = RobotLab::Discovery.find_by_capability("research", timeout: 3)
# List every capability type advertised on the LAN right now
RobotLab::Discovery.list_capabilities(timeout: 3)
# => ["analysis", "coding", "research", "writing"]
Connecting after discovery
Result#url returns a plain URL string. Wire it to whatever client you use:
result = RobotLab::Discovery.find("headline")
# With robot_lab-a2a:
client = A2A.client(url: result.url)
client.send_task(message: A2A::Models::Message.user("Summarise today's news"))
# With Net::HTTP directly:
Net::HTTP.get(URI(result.url))
Result fields
| Field | Type | Description |
|---|---|---|
name |
String | mDNS instance name (e.g. "headline") |
hostname |
String | Fully-qualified .local hostname |
port |
Integer | Port the robot server listens on |
path |
String | HTTP path to the robot (e.g. "/headline") |
capabilities |
Array |
Capability taxonomy terms advertised by this robot |
url |
String | "http://#{hostname}:#{port}#{path}" |
Capability taxonomy
Capabilities are free-form strings — there is no enforced registry. Use short, lowercase, hyphen-separated terms that describe what the robot does, not how it works. Examples:
research analysis writing coding
vision memory tool-use orchestration
web-search summarisation classification
A robot may advertise any number of capabilities. Clients browse by capability
using find_by_capability, which targets the DNS-SD subtype directly and avoids
scanning all robots on the network.
dns_label is available for normalising arbitrary strings:
RobotLab::Discovery.dns_label("Web Search / NLP") # => "web-search-nlp"
Examples
bundle exec ruby examples/01_basic_usage.rb
Covers all six operations: register with capabilities, browse all, find by name, find by capability, list capability inventory, and unregister.
Architecture
lib/robot_lab/discovery/
constants.rb # SERVICE_TYPE = "_robot-lab._tcp.local."
txt_record.rb # encode/decode TXT record arrays (path, rl_version, capabilities)
result.rb # immutable Data value object (name, hostname, port, path, capabilities)
advertiser.rb # wraps ZeroConf::Service, runs in a thread, registers subtypes
browser.rb # wraps ZeroConf.browse, parses DNS responses
RobotLab::Discovery module exposes browse, find, find_by_capability, and
list_capabilities as class methods. If RobotLab is loaded, the gem
self-registers as an extension via RobotLab.register_extension(:discovery, ...).
Relationship to other robot_lab gems
| Gem | Concern |
|---|---|
robot_lab |
Core robots, networks, memory |
robot_lab-a2a |
A2A HTTP+SSE transport protocol |
robot_lab-discovery |
Where are the robots and what can they do (mDNS addressing + capability taxonomy) |
Discovery is intentionally transport-agnostic. It finds a URL; you decide what protocol to speak to it.
Development
bundle install
bundle exec rake test # run the test suite
bin/console # IRB with the gem loaded
License
MIT — see LICENSE.txt.