Skip to main content
All configuration defaults, protocol values, filenames, and timing parameters live in a two-level constants hierarchy. No package hardcodes a magic string or number that is used in more than one place.

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 port 8765 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_DEVICE is unambiguous, 4009 is not
  • Letting hirocli/config.py derive its defaults from constants rather than repeating them, so Config and the constants can never drift apart
  • Providing a flat re-export from hiro_commons.constants so 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), add PORT_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          hirogateway      hiro-channel-*
    │                │                │
    └────────────────┴────────────────┘

             hiro-channel-sdk          ← protocol constants

               hiro-commons            ← infrastructure constants
               └── constants/
                   ├── network.py
                   ├── timing.py
                   ├── storage.py
                   └── domain.py
Both 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.
ConstantValuePurpose
DEFAULT_GATEWAY_PORT8765Default 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_START18080Base port for workspace slot allocation
PORTS_PER_SLOT4Number of ports reserved per workspace slot
PORT_OFFSET_HTTP0HTTP server offset within a slot
PORT_OFFSET_PLUGIN1Plugin WebSocket server offset within a slot
(+2 reserved)Previously used for a local gateway port; no longer allocated
PORT_OFFSET_ADMIN3Admin UI offset within a slot
Workspace port formula:
http_port   = PORT_RANGE_START + slot * PORTS_PER_SLOT + PORT_OFFSET_HTTP
plugin_port = PORT_RANGE_START + slot * PORTS_PER_SLOT + PORT_OFFSET_PLUGIN
admin_port  = PORT_RANGE_START + slot * PORTS_PER_SLOT + PORT_OFFSET_ADMIN
Example for slot 0: http=18080, plugin=18081, admin=18083.

timing.py

Timeouts, intervals, and log rotation sizes.
ConstantValuePurpose
DEFAULT_PING_INTERVAL_SECONDS30.0WebSocket keep-alive ping interval
DEFAULT_AUTH_TIMEOUT_SECONDS30.0Auth handshake deadline
DEFAULT_PAIRING_WAIT_SECONDS120.0Max wait for pairing approval
LOG_ROTATION_MAX_BYTES10 MBLog file rotation threshold
LOG_ROTATION_BACKUP_COUNT5Number of rotated log files to keep

storage.py

Standard filenames and directory names used across the workspace layout.
ConstantValuePurpose
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.
ConstantValuePurpose
DEFAULT_PAIRING_CODE_LENGTH6Digits in a generated pairing code
DEFAULT_PAIRING_CODE_TTL_SECONDS300Pairing code validity window (5 min)
DEFAULT_ATTESTATION_EXPIRY_DAYS30Device attestation lifetime
NONCE_BYTE_LENGTH32Nonce 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:
# namespaced
from hiro_commons.constants import network
network.DEFAULT_GATEWAY_PORT

# flat
from hiro_commons.constants import DEFAULT_GATEWAY_PORT

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

ConstantValue
JSONRPC_VERSION"2.0"
JSONRPC_ERROR_METHOD_NOT_FOUND-32601
JSONRPC_ERROR_INTERNAL-32603

Channel RPC methods

ConstantWire 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 range 4000–4999.
ConstantCodeMeaning
WS_CLOSE_NORMAL1000Clean closure after pairing completes
WS_CLOSE_AUTH_FAILED4003Auth failed or timed out
WS_CLOSE_PAIRING_FIELD_MISSING4004Required pairing field absent
WS_CLOSE_DESKTOP_NOT_CONNECTED4006Device connected but desktop is offline
WS_CLOSE_PAIRING_TIMEOUT4008Pairing approval not received in time
WS_CLOSE_DUPLICATE_DEVICE4009Same device ID already connected
WS_CLOSE_CHANNEL_REPLACED4010Channel registration superseded by newer one

Auth roles, content types, reconnect policy

ConstantValuePurpose
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_SECONDS5.0Flat delay between reconnection attempts
RECONNECT_BACKOFF_BASE1.0Initial exponential backoff value
RECONNECT_BACKOFF_MAX60.0Backoff ceiling

Level 3 — Package-local constants

Values that belong to exactly one package’s identity and are never shared.

hirocli/constants.py

ConstantValuePurpose
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_LENGTH12Hex chars appended after the prefix

hirogateway/constants.py

ConstantValuePurpose
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_BYTES12Byte length for generated pairing request IDs
WS_REASON_MAX_LENGTH120Max 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 byPlace it in
Only hiroclihirocli/constants.py
Only hirogatewayhirogateway/constants.py
Only channel packageshiro_channel_sdk/constants.py
hirocli and hirogatewayhiro_commons/constants/<submodule>.py
Wire protocol (any package)hiro_channel_sdk/constants.py
When adding to hiro_commons/constants/, also add the name to the __all__ list in hiro_commons/constants/__init__.py so flat imports keep working.