Why this design
Before this design, the same values were duplicated across multiple packages with no single source of truth. The specific problems it resolves: Duplicated values drifting apart. The default gateway port8765 appeared in four separate files. The ping interval 30 appeared in three. The log rotation size had two different values (5 MB in the channel SDK, 10 MB in hiro-commons) with no way to tell which was intentional. Any change required a manual search across the whole monorepo with no guarantee all instances were found.
Protocol values as inline magic numbers. WebSocket close codes like 4003, 4009, and 4010 were written directly in relay.py with no names. A reader had to cross-reference comments or commit history to understand what each code meant. The same applied to JSON-RPC method strings like "channel.register" — a typo in any consumer would produce a silent protocol mismatch.
No clear ownership. When a value needed to change, it was unclear which package owned it and which ones were just copying it. This made refactoring risky and discouraged changes.
The design resolves these by:
- Giving every constant a single named definition at the lowest layer that needs it
- Making the wire protocol self-documenting —
WS_CLOSE_DUPLICATE_DEVICEis unambiguous,4009is not - Letting
hirocli/config.pyderive its defaults from constants rather than repeating them, soConfigand the constants can never drift apart - Providing a flat re-export from
hiro_commons.constantsso consumers don’t need to know which submodule a constant lives in
Growth considerations
New workspace services. If a fifth service is added per workspace slot (e.g. a metrics endpoint), addPORT_OFFSET_METRICS: int = 4 to network.py and bump PORTS_PER_SLOT to 5. No string-searching required — every port calculation already uses the formula. (The fourth slot, offset 3, is already taken by the admin UI.)
New channel packages. A new channel package imports from hiro_channel_sdk.constants and gets all method names, close codes, and reconnect policy for free. It never needs to redeclare them.
Protocol versioning. When the wire format changes, JSONRPC_VERSION and the METHOD_* strings are the single place to update. All consumers pick up the change on the next install.
New top-level packages. If a third server-side package is added alongside hirocli and hirogateway, it follows the same rule: package-local constants go in its own constants.py, shared ones go in hiro_commons. The hierarchy does not need to change shape.
Cross-language clients. The Flutter app currently hardcodes ws://localhost:8765 and "devices". As the project grows, a small generated manifest (e.g. a JSON or Dart file produced from hiro_channel_sdk/constants.py) can keep the mobile client in sync with the server-side source of truth without manual coordination.
Submodule granularity. The four submodules in hiro_commons.constants (network, timing, storage, domain) are sized to group values by concern, not by consumer. If a new concern emerges (e.g. agent-specific defaults grow large enough to warrant their own file), add agent.py alongside the others and re-export from __init__.py. Existing imports are unaffected.
Dependency graph
hirocli and hirogateway depend on hiro-commons and hiro-channel-sdk. Channel packages depend only on hiro-channel-sdk. This means every constant lives at the lowest layer that needs it, and flows upward — never sideways.
Level 1 — hiro_commons.constants
Cross-cutting values consumed by more than one top-level package. Split into four submodules.
network.py
Network addresses and workspace port allocation.
| Constant | Value | Purpose |
|---|---|---|
DEFAULT_GATEWAY_PORT | 8765 | Default gateway WebSocket port |
DEFAULT_GATEWAY_HOST | "0.0.0.0" | Default gateway bind host |
DEFAULT_LOCALHOST | "127.0.0.1" | Loopback bind address for local services |
PORT_RANGE_START | 18080 | Base port for workspace slot allocation |
PORTS_PER_SLOT | 4 | Number of ports reserved per workspace slot |
PORT_OFFSET_HTTP | 0 | HTTP server offset within a slot |
PORT_OFFSET_PLUGIN | 1 | Plugin WebSocket server offset within a slot |
| (+2 reserved) | — | Previously used for a local gateway port; no longer allocated |
PORT_OFFSET_ADMIN | 3 | Admin UI offset within a slot |
http=18080, plugin=18081, admin=18083.
timing.py
Timeouts, intervals, and log rotation sizes.
| Constant | Value | Purpose |
|---|---|---|
DEFAULT_PING_INTERVAL_SECONDS | 30.0 | WebSocket keep-alive ping interval |
DEFAULT_AUTH_TIMEOUT_SECONDS | 30.0 | Auth handshake deadline |
DEFAULT_PAIRING_WAIT_SECONDS | 120.0 | Max wait for pairing approval |
LOG_ROTATION_MAX_BYTES | 10 MB | Log file rotation threshold |
LOG_ROTATION_BACKUP_COUNT | 5 | Number of rotated log files to keep |
storage.py
Standard filenames and directory names used across the workspace layout.
| Constant | Value | Purpose |
|---|---|---|
REGISTRY_FILENAME | "registry.json" | Workspace / gateway instance registry |
CONFIG_FILENAME | "config.json" | Per-workspace or per-instance config |
LOGS_DIR | "logs" | Log subdirectory name |
CHANNELS_DIR | "channels" | Channel config subdirectory name |
AGENT_DIR | "agent" | Agent config subdirectory name |
MASTER_KEY_FILENAME | "master_key.pem" | RSA master key file |
PAIRING_SESSION_FILENAME | "pairing_session.json" | Active pairing session state |
APPROVED_DEVICES_FILENAME | "devices.json" | Approved device attestations |
SYSTEM_PROMPT_FILENAME | "system_prompt.md" | Agent system prompt |
domain.py
Business-logic defaults for pairing, attestation, and workspace identity.
| Constant | Value | Purpose |
|---|---|---|
DEFAULT_PAIRING_CODE_LENGTH | 6 | Digits in a generated pairing code |
DEFAULT_PAIRING_CODE_TTL_SECONDS | 300 | Pairing code validity window (5 min) |
DEFAULT_ATTESTATION_EXPIRY_DAYS | 30 | Device attestation lifetime |
NONCE_BYTE_LENGTH | 32 | Nonce size (→ 64 hex chars) |
MANDATORY_CHANNEL_NAME | "devices" | Built-in channel that is always registered |
DEFAULT_WORKSPACE_NAME | "default" | Name used when no workspace is specified |
Flat import
All constants are re-exported from the package root, so consumers can choose between namespaced or flat access:Level 2 — hiro_channel_sdk.constants
Authoritative source for the JSON-RPC wire protocol and WebSocket close codes. Both hirocli and hirogateway import from here; channel packages also use it directly.
JSON-RPC
| Constant | Value |
|---|---|
JSONRPC_VERSION | "2.0" |
JSONRPC_ERROR_METHOD_NOT_FOUND | -32601 |
JSONRPC_ERROR_INTERNAL | -32603 |
Channel RPC methods
| Constant | Wire value |
|---|---|
METHOD_REGISTER | "channel.register" |
METHOD_RECEIVE | "channel.receive" |
METHOD_EVENT | "channel.event" |
METHOD_SEND | "channel.send" |
METHOD_CONFIGURE | "channel.configure" |
METHOD_STOP | "channel.stop" |
METHOD_STATUS | "channel.status" |
WebSocket close codes
Hiro League uses the application-reserved range4000–4999.
| Constant | Code | Meaning |
|---|---|---|
WS_CLOSE_NORMAL | 1000 | Clean closure after pairing completes |
WS_CLOSE_AUTH_FAILED | 4003 | Auth failed or timed out |
WS_CLOSE_PAIRING_FIELD_MISSING | 4004 | Required pairing field absent |
WS_CLOSE_DESKTOP_NOT_CONNECTED | 4006 | Device connected but desktop is offline |
WS_CLOSE_PAIRING_TIMEOUT | 4008 | Pairing approval not received in time |
WS_CLOSE_DUPLICATE_DEVICE | 4009 | Same device ID already connected |
WS_CLOSE_CHANNEL_REPLACED | 4010 | Channel registration superseded by newer one |
Auth roles, content types, reconnect policy
| Constant | Value | Purpose |
|---|---|---|
AUTH_ROLE_DESKTOP | "desktop" | Auth role for the hirocli server |
AUTH_ROLE_DEVICE | "device" | Auth role for mobile / device clients |
CONTENT_TYPE_TEXT | "text" | Plain-text message content type |
CONTENT_TYPE_JSON | "json" | JSON message content type |
RECONNECT_DELAY_SECONDS | 5.0 | Flat delay between reconnection attempts |
RECONNECT_BACKOFF_BASE | 1.0 | Initial exponential backoff value |
RECONNECT_BACKOFF_MAX | 60.0 | Backoff ceiling |
Level 3 — Package-local constants
Values that belong to exactly one package’s identity and are never shared.hirocli/constants.py
| Constant | Value | Purpose | |
|---|---|---|---|
APP_NAME | "hirocli" | platformdirs app name for data directory | |
PID_FILENAME | "hirocli.pid" | PID file written at startup | |
ENV_WORKSPACE | "HIRO_WORKSPACE" | Env var to override active workspace | |
ENV_WORKSPACE_PATH | "HIRO_WORKSPACE_PATH" | Env var to pass workspace path in background mode | |
ENV_ADMIN_UI | "HIRO_ADMIN_UI" | Env var to enable admin UI in background mode | |
DEVICE_ID_PREFIX | "mobile-" | Prefix for auto-generated device IDs | |
DEVICE_ID_SUFFIX_LENGTH | 12 | Hex chars appended after the prefix |
hirogateway/constants.py
| Constant | Value | Purpose |
|---|---|---|
APP_NAME | "hirogateway" | platformdirs app name for data directory |
PID_FILENAME | "gateway.pid" | PID file written at startup |
ENV_INSTANCE | "HIRO_GATEWAY_INSTANCE" | Env var to override active gateway instance |
DEFAULT_INSTANCE_NAME | "default" | Instance name used when none is specified |
PAIRING_REQUEST_ID_BYTES | 12 | Byte length for generated pairing request IDs |
WS_REASON_MAX_LENGTH | 120 | Max chars in a WebSocket close reason string |
Adding new constants
Follow the placement rule: put the constant at the lowest layer that needs it.| Needed by | Place it in |
|---|---|
Only hirocli | hirocli/constants.py |
Only hirogateway | hirogateway/constants.py |
| Only channel packages | hiro_channel_sdk/constants.py |
hirocli and hirogateway | hiro_commons/constants/<submodule>.py |
| Wire protocol (any package) | hiro_channel_sdk/constants.py |
hiro_commons/constants/, also add the name to the __all__ list in hiro_commons/constants/__init__.py so flat imports keep working.