> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiroleague.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Protocol contract

> The shared wire contract used by Hiro Server, channel plugins, Hiro Gate, and device apps.

export const DiagramViewer = ({children, title = "Diagram"}) => {
  const dialogRef = useRef(null);
  const viewportRef = useRef(null);
  const contentRef = useRef(null);
  const [isOpen, setIsOpen] = useState(false);
  const [zoom, setZoom] = useState(1);
  const [pan, setPan] = useState({
    x: 0,
    y: 0
  });
  const zoomRef = useRef(1);
  const panRef = useRef({
    x: 0,
    y: 0
  });
  const dragRef = useRef(null);
  const open = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(true);
  };
  const close = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(false);
  };
  const resetView = () => {
    fitDiagramToViewport();
    setPan({
      x: 0,
      y: 0
    });
  };
  const setView = (nextZoom, nextPan) => {
    zoomRef.current = nextZoom;
    panRef.current = nextPan;
    setZoom(nextZoom);
    setPan(nextPan);
  };
  const MaximizeIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5" />
    </svg>;
  const ResetIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
      <path d="M3 3v5h5" />
    </svg>;
  const CloseIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M18 6 6 18M6 6l12 12" />
    </svg>;
  const fitDiagramToViewport = () => {
    const viewport = viewportRef.current;
    const content = contentRef.current;
    const svg = content?.querySelector(".mermaid svg[viewBox]");
    if (!viewport || !svg) return;
    const viewBox = svg.getAttribute("viewBox")?.split(/\s+/).map(Number);
    if (!viewBox || viewBox.length !== 4 || !viewBox[2] || !viewBox[3]) return;
    const [, , width, height] = viewBox;
    svg.style.width = `${width}px`;
    svg.style.maxWidth = "none";
    const rect = viewport.getBoundingClientRect();
    const nextZoom = Math.min(2.2, Math.max(0.75, Number((Math.min((rect.width - 96) / width, (rect.height - 96) / height) * 0.98).toFixed(3))));
    setView(nextZoom, {
      x: 0,
      y: 0
    });
  };
  const handleWheel = event => {
    if (!isOpen) return;
    event.preventDefault();
    event.stopPropagation();
    const viewport = viewportRef.current;
    if (!viewport) return;
    const rect = viewport.getBoundingClientRect();
    const pointerX = event.clientX - rect.left - rect.width / 2;
    const pointerY = event.clientY - rect.top - rect.height / 2;
    const currentZoom = zoomRef.current;
    const currentPan = panRef.current;
    const nextZoom = Math.max(0.25, Math.min(5, Number((currentZoom * (event.deltaY < 0 ? 1.12 : 0.88)).toFixed(3))));
    const ratio = nextZoom / currentZoom;
    setView(nextZoom, {
      x: pointerX - (pointerX - currentPan.x) * ratio,
      y: pointerY - (pointerY - currentPan.y) * ratio
    });
  };
  const onPointerDown = event => {
    if (!isOpen || event.button !== 0) return;
    event.preventDefault();
    dragRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      pan: panRef.current
    };
    try {
      event.currentTarget.setPointerCapture?.(event.pointerId);
    } catch {}
  };
  const onPointerMove = event => {
    const drag = dragRef.current;
    if (!isOpen || !drag || drag.pointerId !== event.pointerId) return;
    event.preventDefault();
    const nextPan = {
      x: drag.pan.x + event.clientX - drag.startX,
      y: drag.pan.y + event.clientY - drag.startY
    };
    panRef.current = nextPan;
    setPan(nextPan);
  };
  const endDrag = event => {
    if (dragRef.current?.pointerId === event.pointerId) {
      dragRef.current = null;
    }
  };
  useEffect(() => {
    const dialog = dialogRef.current;
    if (isOpen && dialog && !dialog.open) {
      dialog.showModal();
      requestAnimationFrame(() => {
        requestAnimationFrame(fitDiagramToViewport);
      });
    }
    if (!isOpen && dialog?.open) {
      dialog.close();
    }
  }, [isOpen]);
  useEffect(() => {
    if (!isOpen) return;
    const onKeyDown = event => {
      if (event.key === "Escape") close();
    };
    const onWheel = event => {
      event.preventDefault();
      event.stopPropagation();
      if (viewportRef.current?.contains(event.target)) {
        handleWheel(event);
      }
    };
    const previousOverflow = document.body.style.overflow;
    const previousHtmlOverflow = document.documentElement.style.overflow;
    document.body.style.overflow = "hidden";
    document.documentElement.style.overflow = "hidden";
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("wheel", onWheel, {
      capture: true,
      passive: false
    });
    return () => {
      document.body.style.overflow = previousOverflow;
      document.documentElement.style.overflow = previousHtmlOverflow;
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("wheel", onWheel, {
        capture: true
      });
    };
  }, [isOpen]);
  const toolbar = <div className="diagram-viewer__bar">
      <span className="diagram-viewer__title">{title}</span>
      <div className="diagram-viewer__actions">
        {isOpen && <span className="diagram-viewer__zoom" aria-live="polite">
            {Math.round(zoom * 100)}%
          </span>}
        {isOpen && <button className="diagram-viewer__button" type="button" onClick={resetView} aria-label="Reset diagram">
            <ResetIcon />
          </button>}
        <button className="diagram-viewer__button" type="button" onClick={isOpen ? close : open} aria-label={isOpen ? "Close" : "Maximize"}>
          {isOpen ? <CloseIcon /> : <MaximizeIcon />}
        </button>
      </div>
    </div>;
  const diagram = <div ref={viewportRef} className="diagram-viewer__viewport" onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={endDrag} onPointerCancel={endDrag}>
      <div className="diagram-viewer__stage" style={{
    transform: isOpen ? `translate(${pan.x}px, ${pan.y}px)` : undefined
  }}>
        <div className="diagram-viewer__centerer">
          <div ref={contentRef} className="diagram-viewer__content" style={{
    transform: isOpen ? `scale(${zoom})` : "none"
  }}>
            {children}
          </div>
        </div>
      </div>
    </div>;
  return <>
      {!isOpen && <div className="diagram-viewer not-prose">
          {toolbar}
          {diagram}
        </div>}

      <dialog ref={dialogRef} className="diagram-viewer-dialog" onWheel={event => {
    if (event.target === event.currentTarget) {
      event.preventDefault();
      event.stopPropagation();
    }
  }} onCancel={event => {
    event.preventDefault();
    close();
  }} onClose={() => setIsOpen(false)}>
        {isOpen && <div className="diagram-viewer diagram-viewer--open not-prose">
            {toolbar}
            {diagram}
          </div>}
      </dialog>
    </>;
};

The protocol contract is the language-neutral wire contract for Hiro League messages.

It defines the JSON shapes that move between Hiro Server, channel plugins, Hiro Gate, and device apps. Python, Dart, and any future client implementation must parse and emit the same contract.

## Contract surfaces

| Surface                   | Owner              | Purpose                                                                   |
| ------------------------- | ------------------ | ------------------------------------------------------------------------- |
| `UnifiedMessage`          | Hiro protocol      | Canonical message, event, request, and response envelope.                 |
| Gateway relay envelope    | Hiro Gate protocol | Routes payloads between connected devices and the devices channel plugin. |
| Auth frames               | Hiro Gate protocol | Authenticates an already-paired device connection.                        |
| Pairing frames            | Hiro Gate protocol | Pairs a new device with a workspace.                                      |
| Request and response body | Hiro protocol      | Carries device RPC calls and server replies inside JSON content items.    |
| Metadata keys             | Hiro protocol      | Carries routing extras such as device identity and channel context.       |

The machine-readable contract lives under the repo-level `protocol/` folder. The explanatory protocol references live under `docs/protocol/`.

## UnifiedMessage

`UnifiedMessage` is the canonical cross-channel message envelope.

| Field          | Type           | Required | Meaning                                                                 |
| -------------- | -------------- | -------- | ----------------------------------------------------------------------- |
| `version`      | string         | yes      | Protocol version. Current value is `"0.1"`.                             |
| `message_type` | string         | yes      | One of `message`, `event`, `request`, `response`, or reserved `stream`. |
| `request_id`   | string or null | no       | Correlation ID for request/response messages.                           |
| `routing`      | object         | yes      | Sender, recipient, channel, direction, timestamp, and metadata.         |
| `content`      | array          | yes      | Ordered content items. Empty for event messages.                        |
| `event`        | object or null | no       | Event payload. Present only for event messages.                         |

### Message type rules

| `message_type` | Required shape                                                             |
| -------------- | -------------------------------------------------------------------------- |
| `message`      | `content` has at least one item. `event` is absent or null.                |
| `event`        | `event` is present. `content` is empty.                                    |
| `request`      | `request_id` is present. `content` contains a JSON request body.           |
| `response`     | `request_id` matches the request. `content` contains a JSON response body. |
| `stream`       | Reserved for future streaming chunks.                                      |

## Routing

`routing` identifies where the message came from, where it should go, and how it should be grouped.

| Field          | Meaning                                                                          |
| -------------- | -------------------------------------------------------------------------------- |
| `id`           | Message identifier.                                                              |
| `channel`      | Channel name, such as `devices` or `telegram`.                                   |
| `direction`    | Direction from the Hiro Server perspective.                                      |
| `sender_id`    | Originating device, user, service, or server actor.                              |
| `recipient_id` | Target device, user, service, or null for broadcast or server-directed messages. |
| `timestamp`    | Message timestamp.                                                               |
| `metadata`     | Protocol-defined routing extras.                                                 |

`direction` is normalized before a message enters Hiro Server:

| Value      | Meaning                                                              |
| ---------- | -------------------------------------------------------------------- |
| `inbound`  | Arriving into Hiro Server from a device, plugin, or third-party app. |
| `outbound` | Leaving Hiro Server toward a device, plugin, or third-party app.     |

Device-originated messages are normalized by the devices channel before they enter Communication Manager. Inside Hiro Server, consumers can treat `direction` as server-relative.

## Content items

Each content item represents one authored payload.

| Field          | Meaning                                                                                                                                |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `content_type` | Payload kind, such as `text`, `audio`, `image`, `file`, or `json`.                                                                     |
| `body`         | Payload value. Text content is plain text. Binary media is represented by a URL, path, or encoded value according to the content type. |
| `metadata`     | Content-specific extras such as filename, MIME type, duration, transcript, or adapter error.                                           |

Content order is meaningful. Consumers preserve the order emitted by the sender.

## Events

Event messages use `message_type: "event"` and carry an `event` payload.

| Field    | Meaning                                                   |
| -------- | --------------------------------------------------------- |
| `type`   | Dotted event name.                                        |
| `ref_id` | Optional ID of the message or entity the event refers to. |
| `data`   | Event-specific payload.                                   |

Active message events:

| Event                 | Purpose                                                  |
| --------------------- | -------------------------------------------------------- |
| `message.received`    | Acknowledges that an inbound message was accepted.       |
| `message.transcribed` | Adds transcript data for an audio message.               |
| `message.voiced`      | Adds synthesized voice data for a text reply.            |
| `resource.changed`    | Tells connected devices to refresh a workspace resource. |

## Requests and responses

Requests and responses are `UnifiedMessage` objects with `content_type: "json"`.

Request body:

```json theme={null}
{
  "method": "channels.list",
  "params": {}
}
```

Response body:

```json theme={null}
{
  "status": "ok",
  "data": {}
}
```

Error response body:

```json theme={null}
{
  "status": "error",
  "error": {
    "code": "method_not_found",
    "message": "Unknown method"
  }
}
```

`request_id` is generated by the caller and echoed by the responder.

## Gateway envelopes

Hiro Gate wraps forwarded device payloads in a relay envelope.

```json theme={null}
{
  "sender_device_id": "device-id",
  "target_device_id": "optional-target-device-id",
  "payload": {}
}
```

`sender_device_id` identifies the connected device that produced the payload. `target_device_id` is optional and targets a specific connected device when present. `payload` contains the protocol payload being relayed.

## Auth and pairing frames

Gateway auth and pairing frames are `type`-discriminated JSON objects.

| Frame type         | Direction         | Purpose                                               |
| ------------------ | ----------------- | ----------------------------------------------------- |
| `auth_challenge`   | Gateway to device | Starts device authentication.                         |
| `auth_response`    | Device to gateway | Proves the device has valid pairing credentials.      |
| `auth_ok`          | Gateway to device | Confirms the authenticated connection.                |
| `pairing_request`  | Device to gateway | Starts pairing for a new device.                      |
| `pairing_pending`  | Gateway to device | Confirms that pairing is waiting for server approval. |
| `pairing_response` | Gateway to device | Returns approved pairing credentials or a rejection.  |

## Metadata keys

Protocol metadata keys are part of the contract when behavior depends on them.

| Key                   | Location           | Meaning                                                 |
| --------------------- | ------------------ | ------------------------------------------------------- |
| `channel_id`          | `routing.metadata` | Conversation or channel grouping key.                   |
| `device_name`         | `routing.metadata` | Human-readable device name.                             |
| `sender_device_id`    | `routing.metadata` | Device ID attached by the gateway or devices channel.   |
| `friendly_name`       | `routing.metadata` | Display name for a device or participant.               |
| `request_voice_reply` | `routing.metadata` | Requests a voice reply when the server can produce one. |

## Compatibility contract

The protocol is valid when all supported implementations agree on the same fixtures:

| Fixture class     | Requirement                                                               |
| ----------------- | ------------------------------------------------------------------------- |
| Valid fixtures    | Python and Dart parse them successfully.                                  |
| Invalid fixtures  | Python and Dart reject them consistently.                                 |
| Outbound fixtures | Serialization from each implementation matches the documented wire shape. |

Schema files and fixtures are the reviewable source of truth. Runtime models in Python and Dart implement that contract.

***

## See also

<CardGroup cols={2}>
  <Card title="UnifiedMessage reference" icon="envelope" href="/architecture/protocol/unified-message">
    Detailed field reference, validation rules, constants, and wire examples.
  </Card>

  <Card title="Communication Manager" icon="arrows-left-right" href="/architecture/concepts/communication-manager">
    How Hiro Server validates, routes, adapts, and dispatches UnifiedMessages.
  </Card>

  <Card title="Channel Manager" icon="plug" href="/architecture/concepts/channel-manager">
    How channel plugins send and receive UnifiedMessages through Hiro Server.
  </Card>

  <Card title="Gateway runtime" icon="shield" href="/hiro/gateway/gateway-runtime">
    How Hiro Gate handles device connections.
  </Card>

  <Card title="Channel Plugins" icon="plug" href="/architecture/concepts/channel-plugins">
    The plugin-side JSON-RPC contract around UnifiedMessage payloads.
  </Card>
</CardGroup>
