orchestrata_ultimate.kernel.cli_client
Reference › orchestrata_ultimate › kernel › cli_client
The gateway client helpers for the orchestrata command line. Commands that talk to a running system do not open Redis or the kernel directly; they call the gateway's JSON-RPC interface over a WebSocket, and this module is that client. cli_main and cli_watch import it. It serves SPEC-000 story US-004 (REQ-000-033). The server side is the gateway server, and the token handling uses gateway.auth.
load_dotenv runs the shared two-phase environment loader (core.env_bootstrap.load_env_contract) once per process, so a bare shell resolves the same endpoints as the services. gateway_uri resolves the address in this order: CXP_WS_URI; then GATEWAY_HOST and GATEWAY_PORT, with localhost and 8765 as defaults; an explicit environment mapping can be passed for tests and then skips the loader. _rpc_call connects, sends auth.authenticate with a short-lived token (one hour, from _mint_token), and then sends the one requested JSON-RPC method and returns its result as a dictionary. gateway_rpc is the synchronous wrapper with a 15-second timeout. It turns every failure into a click error without a traceback: an authentication or method error, or an unreachable gateway, in which case the message tells the user to start the runtime (REQ-000-097).
At a glance
| Source | src/orchestrata_ultimate/kernel/cli_client.py |
| Lines of code | 162 |
| Classes | 0 |
| Functions | 5 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.core.env_bootstrap, orchestrata_ultimate.gateway.auth |
| Imported by | orchestrata_ultimate.kernel.cli_main, orchestrata_ultimate.kernel.cli_watch |
| Tests that import it | tests/kernel/test_cli_client.py, tests/kernel/test_cli_main_coverage.py, tests/kernel/test_cli_watch.py |
Class diagram
No classes; see Functions.
Dependency diagram
API
Functions
| Function | Signature | Summary |
|---|---|---|
load_dotenv |
load_dotenv() -> None |
Load the RUL-019 two-phase env contract, once per process. |
gateway_uri |
gateway_uri(env: Mapping[str, str] | None = None) -> str |
Resolve the gateway WebSocket URI from the environment. |
_mint_token |
_mint_token(subject: str = 'orchestrata-cli', ttl_seconds: int = 3600) -> str |
Mint a short-lived JWT for auth.authenticate (HS256, shared secret). |
_rpc_call traces |
async _rpc_call(uri: str, method: str, params: dict[str, Any] | None, timeout: float) -> dict[str, Any] |
Connect, authenticate, call one JSON-RPC method, return the result. |
gateway_rpc traces |
gateway_rpc(method: str, params: dict[str, Any] | None = None, timeout: float = 15.0, env: Mapping[str, str] | None = None) -> dict[str, Any] |
Call a gateway JSON-RPC method synchronously. |
Source
"""Gateway client helpers for the ``orchestrata`` CLI.
The CLI is a thin client over the running runtime (SPEC-000): interaction
commands talk to the gateway's WebSocket JSON-RPC surface instead of
booting subsystems in-process. This module centralizes:
- ``.env`` loading (same walk-up pattern as the gateway CLI),
- gateway URI resolution from env (never hardcoded hosts),
- short-lived JWT minting for ``auth.authenticate``,
- a synchronous ``gateway_rpc`` helper usable from Click commands.
Author: Mohamed
"""
# Spec: 000 - Orchestrata CLI
from __future__ import annotations
import asyncio
import json
import os
import time
from collections.abc import Mapping
from typing import Any
import click
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.core.env_bootstrap import load_env_contract
_dotenv_loaded = False
def load_dotenv() -> None:
"""Load the RUL-019 two-phase env contract, once per process.
Delegates to the ONE canonical loader
(``core.env_bootstrap.load_env_contract``); this used to be a private
``.env``-only walk-up that skipped the ``env/<profile>.env`` overlay, so
the CLI resolved the prod-BASE endpoints in ``.env`` from a bare shell.
The idempotence guard stays: the overlay writes with override, and
re-running it would clobber a value a caller exported deliberately after
the first load.
"""
global _dotenv_loaded
if _dotenv_loaded:
return
load_env_contract()
_dotenv_loaded = True
def gateway_uri(env: Mapping[str, str] | None = None) -> str:
"""Resolve the gateway WebSocket URI from the environment.
Order: CXP_WS_URI, then GATEWAY_HOST/GATEWAY_PORT, then the
conventional localhost:8765 (a default, not a hardcoded service IP).
Args:
env: Optional explicit environment mapping (mirrors
``gateway.cli.run_serve``'s ``env=`` seam). When provided,
resolution reads this mapping instead of ``os.environ`` and
skips the ``.env`` load — tests pass a hermetic mapping and
must not mutate the real process environment.
"""
if env is None:
load_dotenv()
env = os.environ
uri = env.get("CXP_WS_URI", "")
if uri:
return uri
host = env.get("GATEWAY_HOST", "localhost")
port = env.get("GATEWAY_PORT", "8765")
return f"ws://{host}:{port}"
def _mint_token(subject: str = "orchestrata-cli", ttl_seconds: int = 3600) -> str:
"""Mint a short-lived JWT for auth.authenticate (HS256, shared secret)."""
import jwt
from orchestrata_ultimate.gateway.auth import TokenAuthenticator
secret = TokenAuthenticator._get_secret()
now = int(time.time())
return jwt.encode(
{"sub": subject, "iat": now, "exp": now + ttl_seconds},
secret,
algorithm="HS256",
)
@traces(spec="000", story="US-004", reqs=["REQ-000-033"])
async def _rpc_call(
uri: str, method: str, params: dict[str, Any] | None, timeout: float
) -> dict[str, Any]:
"""Connect, authenticate, call one JSON-RPC method, return the result."""
import websockets
async with websockets.connect(uri, close_timeout=timeout) as ws:
await ws.send(
json.dumps(
{
"jsonrpc": "2.0",
"method": "auth.authenticate",
"params": {"token": _mint_token()},
"id": 0,
}
)
)
auth_resp = json.loads(await asyncio.wait_for(ws.recv(), timeout=timeout))
if "error" in auth_resp:
raise click.ClickException(
f"gateway authentication failed: "
f"{auth_resp['error'].get('message', auth_resp['error'])}"
)
await ws.send(
json.dumps(
{
"jsonrpc": "2.0",
"method": method,
"params": params or {},
"id": 1,
}
)
)
resp = json.loads(await asyncio.wait_for(ws.recv(), timeout=timeout))
if "error" in resp:
err = resp["error"]
raise click.ClickException(f"{method} failed: {err.get('message', err)}")
result = resp.get("result", {})
return result if isinstance(result, dict) else {"value": result}
@traces(spec="000", story="US-004", reqs=["REQ-000-033"])
def gateway_rpc(
method: str,
params: dict[str, Any] | None = None,
timeout: float = 15.0,
env: Mapping[str, str] | None = None,
) -> dict[str, Any]:
"""Call a gateway JSON-RPC method synchronously.
Raises a clean ClickException (no traceback, REQ-000-097) when the
gateway is unreachable, the call errors, or the reply is not a dict.
Args:
env: Optional explicit environment mapping forwarded to
:func:`gateway_uri` (see its docstring) — used by tests to
target a hermetic gateway instance without mutating
``os.environ``.
"""
uri = gateway_uri(env=env)
try:
return asyncio.run(_rpc_call(uri, method, params, timeout))
except click.ClickException:
raise
except (TimeoutError, OSError) as e:
raise click.ClickException(
f"gateway unreachable at {uri} ({e}) — is the runtime up? "
f"Start it with `orchestrata start` or scripts/start_test_services.py."
) from e
except Exception as e:
raise click.ClickException(f"{method} failed: {e}") from e