Compare commits

...

11 Commits

Author SHA1 Message Date
dd305aabeb Merge pull request 'Bump version to 0.1.7' (#34) from release/v0.1.7 into main
Some checks failed
Build Sphinx Docs Set / Build Docs (push) Successful in 28s
Build Project / Build Project (3.10) (push) Successful in 3m46s
Test with tox / Test with tox (3.10) (push) Failing after 5m22s
Test with tox / Test with tox (3.12) (push) Successful in 18m39s
Build Project / Build Project (3.12) (push) Successful in 21m57s
Build Project / Build Project (3.11) (push) Successful in 22m6s
Test with tox / Test with tox (3.11) (push) Successful in 20m10s
Reviewed-on: #34
2026-05-26 15:38:20 -04:00
816bc84f9a Bump version to 0.1.7
Some checks failed
Test with tox / Test with tox (3.11) (pull_request) Successful in 14m19s
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 21m42s
Build Project / Build Project (3.10) (pull_request) Successful in 21m45s
Build Project / Build Project (3.11) (pull_request) Successful in 22m13s
Build Project / Build Project (3.12) (pull_request) Successful in 22m16s
Test with tox / Test with tox (3.12) (pull_request) Successful in 7m28s
Test with tox / Test with tox (3.10) (pull_request) Failing after 26m7s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 15:34:23 -04:00
b27b04dbc0 Merge pull request 'screens-connection' (#33) from screens-connection into main
Some checks failed
Build Sphinx Docs Set / Build Docs (push) Successful in 21s
Test with tox / Test with tox (3.11) (push) Successful in 4m29s
Build Project / Build Project (3.10) (push) Successful in 4m37s
Test with tox / Test with tox (3.12) (push) Successful in 4m17s
Build Project / Build Project (3.12) (push) Successful in 4m34s
Build Project / Build Project (3.11) (push) Successful in 4m35s
Test with tox / Test with tox (3.10) (push) Has been cancelled
Reviewed-on: #33
Reviewed-by: gillian <gillian@qoherent.ai>
2026-05-26 15:32:17 -04:00
53f912f21a docs(changelog): add 0.1.7 entry for ria-agent register fixes
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 23s
Build Project / Build Project (3.12) (pull_request) Successful in 2m57s
Build Project / Build Project (3.11) (pull_request) Successful in 3m9s
Build Project / Build Project (3.10) (pull_request) Successful in 3m11s
Test with tox / Test with tox (3.11) (pull_request) Successful in 3m3s
Test with tox / Test with tox (3.12) (pull_request) Successful in 3m37s
Test with tox / Test with tox (3.10) (pull_request) Failing after 4m6s
Covers the User-Agent / Cloudflare fix, request timeout, structured
error reasons, human-readable default agent names, and updated CLI
help text for the per-user `ria_reg_*` registration key flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:44:00 -04:00
543517f0ca fix(agent): address review findings on register flow
- Restore agent_id in success output. Pre-PR the user saw the hub's
  canonical identifier; the merge had reduced this to just `(name)`,
  which made it impossible to correlate the registered agent with
  anything on the hub side without inspecting the config file.
- Add a 15s timeout to the register POST. urllib's default is none,
  so a stuck hub would block the CLI indefinitely.
- Read User-Agent version from package metadata instead of hardcoding
  "0.1", so it tracks releases. Also corrected the URL to the canonical
  Source URL listed in pyproject.toml (was pointing at a likely-404
  github.com path).
- Add two tests guarding the User-Agent. The whole point of the Cloudflare
  fix was to set a non-default UA; previously no test asserted this, so
  a refactor could silently reintroduce the 403 code 1010 bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:40:29 -04:00
ba1804a5f9 Merge remote-tracking branch 'origin/main' into screens-connection
# Conflicts:
#	poetry.lock
2026-05-26 12:29:08 -04:00
febb1bd6cf Merge remote-tracking branch 'origin/agent-naming-fix' into screens-connection
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 27s
Test with tox / Test with tox (3.12) (pull_request) Successful in 7m45s
Test with tox / Test with tox (3.11) (pull_request) Successful in 8m3s
Build Project / Build Project (3.12) (pull_request) Successful in 13m8s
Build Project / Build Project (3.11) (pull_request) Successful in 13m13s
Build Project / Build Project (3.10) (pull_request) Successful in 13m15s
Test with tox / Test with tox (3.10) (pull_request) Failing after 13m16s
# Conflicts:
#	src/ria_toolkit_oss/agent/cli.py
2026-05-26 12:15:40 -04:00
5f68fd936d fix(agent): set explicit User-Agent on register POST
Python's `urllib.request.Request` defaults to `Python-urllib/<ver>`,
which Cloudflare's Browser Integrity Check on `riahub.ai` blacklists
(HTTP 403 with edge error code 1010). The register POST never reached
the hub. Any non-default UA passes the check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:06:00 -04:00
99447a581a poetry.lock: refresh
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 11:16:52 -04:00
2f6b5ced18 agent register: surface structured hub failure reasons
Map the hub's structured `{detail: {reason}}` responses (invalid_key,
expired, revoked, already_consumed) and 429 rate-limits to actionable
CLI messages so users know to mint a fresh key from Settings → RIA
Agents instead of seeing a raw HTTPError.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 11:16:06 -04:00
eb5b4ce839 agent naming fix, now human readable 2026-04-21 12:50:59 -04:00
6 changed files with 265 additions and 9 deletions

View File

@ -1,5 +1,24 @@
# Changelog
## [0.1.7] - 2026-05-26
### Added
- **Human-readable agent names**`ria-agent register` now generates a default `adjective-colour-animal` name (e.g. `swift-teal-falcon`) via the new `namegen` module when `--name` is omitted, instead of registering with an empty string.
- **Structured registration error messages**`ria-agent register` translates hub responses into actionable English for the known failure reasons (`invalid_key`, `expired`, `revoked`, `already_consumed`) and rate-limit (`HTTP 429`) responses, instead of surfacing raw `HTTP 4xx` text.
### Changed
- **`ria-agent register` `--api-key` help** — now describes the personal `ria_reg_*` registration key flow (minted from **Settings → RIA Agents** on the hub, shown once at mint time). The legacy shared `[wac] API_KEY` is still accepted by the hub for back-compat, but the CLI documents the per-user flow as preferred.
- **`ria-agent register` success output** — now prints both the hub-assigned agent ID and the chosen name: `Registered agent: <id> (<name>)`.
### Fixed
- **`ria-agent register` blocked by Cloudflare on hubs behind it** — set an explicit `User-Agent` (`ria-agent/<package-version> (+https://riahub.ai/qoherent/ria-toolkit-oss)`) so the request isn't rejected as `Python-urllib/<ver>` (Cloudflare Browser Integrity Check returns HTTP 403, edge error code 1010). Version is read from package metadata so it tracks releases automatically.
- **`ria-agent register` could hang indefinitely** — added a 15-second timeout to the hub request; previously `urllib`'s default of no timeout meant a stuck hub would block the CLI forever.
---
## [0.1.0] - 2026-02-20
### Added

View File

@ -14,7 +14,7 @@ sys.path.insert(0, os.path.abspath(os.path.join('..', '..')))
project = 'ria-toolkit-oss'
copyright = '2026, Qoherent Inc'
author = 'Qoherent Inc.'
release = '0.1.6'
release = '0.1.7'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

4
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
[[package]]
name = "alabaster"
@ -1264,7 +1264,7 @@ files = [
[package.dependencies]
attrs = ">=22.2.0"
jsonschema-specifications = ">=2023.3.6"
jsonschema-specifications = ">=2023.03.6"
referencing = ">=0.28.4"
rpds-py = ">=0.25.0"

View File

@ -1,6 +1,6 @@
[project]
name = "ria-toolkit-oss"
version = "0.1.6"
version = "0.1.7"
description = "An open-source version of the RIA Toolkit, including the fundamental tools to get started developing, testing, and deploying radio intelligence applications"
license = { text = "AGPL-3.0-only" }
readme = "README.md"

View File

@ -5,8 +5,11 @@ Subcommands:
- ``ria-agent run [legacy args]`` legacy long-poll NodeAgent (unchanged).
- ``ria-agent stream`` new WebSocket-based IQ streamer.
- ``ria-agent detect`` print SDR drivers whose modules import cleanly.
- ``ria-agent register --hub URL --api-key KEY`` register with the hub and
save credentials (and optional TX interlocks) to ``~/.ria/agent.json``.
- ``ria-agent register --hub URL --api-key KEY`` register with the hub
using a personal registration key (minted from **Settings RIA Agents**
on the hub, shown once at mint time) and save credentials (and optional
TX interlocks) to ``~/.ria/agent.json``. The hub also accepts the legacy
shared ``[wac] API_KEY`` for back-compat, but that path is deprecated.
Invoking ``ria-agent`` with no subcommand falls through to the legacy
long-poll behavior for back-compatibility with existing deployments.
@ -28,6 +31,79 @@ from .namegen import generate_agent_name
_LEGACY_ALIASES = {"--hub", "--key", "--name", "--device", "--insecure", "--log-level", "--config"}
def _user_agent() -> str:
"""Build the User-Agent header for hub requests.
Set explicitly so we don't fall back to Python's default `Python-urllib/<ver>`,
which is blocked by Cloudflare's Browser Integrity Check on `riahub.ai`
(HTTP 403 edge code 1010). Version is read from package metadata so it
tracks releases instead of going stale.
"""
from importlib.metadata import PackageNotFoundError, version
try:
pkg_version = version("ria-toolkit-oss")
except PackageNotFoundError:
pkg_version = "unknown"
return f"ria-agent/{pkg_version} (+https://riahub.ai/qoherent/ria-toolkit-oss)"
# How long to wait on the hub before giving up. The register endpoint is a
# small DB lookup + insert; anything past this is a stuck hub, not a slow one.
_REGISTER_TIMEOUT_S = 15
REGISTRATION_REASON_MESSAGES = {
"invalid_key": (
"Registration key not recognized. Generate a fresh key from "
"Settings → RIA Agents on the hub."
),
"expired": (
"This registration key has expired. Generate a new one from "
"Settings → RIA Agents on the hub."
),
"revoked": (
"This registration key was revoked. Generate a new one from "
"Settings → RIA Agents on the hub."
),
"already_consumed": (
"This single-use registration key has already been used. "
"Generate a new one, or mint a reusable key instead."
),
}
def _explain_registration_failure(status: int, body: bytes) -> str:
"""Return a human-readable explanation for a failed register call."""
try:
parsed = json.loads(body) if body else None
except ValueError:
parsed = None
if status == 429:
# 429 carries a plain string detail, never a reason code.
if isinstance(parsed, dict) and parsed.get("detail"):
detail = parsed["detail"]
else:
detail = body.decode("utf-8", "replace") or "rate limited"
return f"Registration rate-limited by the hub: {detail}"
if not isinstance(parsed, dict):
text = body.decode("utf-8", "replace")
return f"HTTP {status}: {text or 'no body'}"
detail = parsed.get("detail")
if isinstance(detail, dict):
reason = detail.get("reason")
if reason in REGISTRATION_REASON_MESSAGES:
return REGISTRATION_REASON_MESSAGES[reason]
if reason:
return f"Registration rejected ({reason})"
if isinstance(detail, str) and detail:
return f"Registration rejected: {detail}"
return f"HTTP {status}: {parsed}"
def _cmd_detect(_args: argparse.Namespace) -> int:
devices = available_devices()
if not devices:
@ -39,6 +115,7 @@ def _cmd_detect(_args: argparse.Namespace) -> int:
def _cmd_register(args: argparse.Namespace) -> int:
import urllib.error
import urllib.request
hub_url = args.hub.rstrip("/")
@ -51,11 +128,20 @@ def _cmd_register(args: argparse.Namespace) -> int:
headers={
"Content-Type": "application/json",
"X-API-Key": args.api_key,
"User-Agent": _user_agent(),
},
)
try:
with urllib.request.urlopen(req) as resp:
with urllib.request.urlopen(req, timeout=_REGISTER_TIMEOUT_S) as resp:
data = json.loads(resp.read())
except urllib.error.HTTPError as e:
try:
err_body = e.read()
except Exception:
err_body = b""
msg = _explain_registration_failure(e.code, err_body)
print(f"error: registration failed: {msg}", file=sys.stderr)
return 1
except Exception as e:
print(f"error: registration failed: {e}", file=sys.stderr)
return 1
@ -80,7 +166,7 @@ def _cmd_register(args: argparse.Namespace) -> int:
cfg.tx_allowed_freq_ranges = [[float(lo), float(hi)] for lo, hi in freq_ranges]
path = _config.save(cfg)
print(f"Registered agent: {agent_id}")
print(f"Registered agent: {agent_id} ({name})")
if cfg.tx_enabled:
caps: list[str] = []
if cfg.tx_max_gain_db is not None:
@ -141,7 +227,16 @@ def main() -> None:
p_reg = sub.add_parser("register", help="Register agent with RIA Hub and save credentials")
p_reg.add_argument("--hub", required=True, help="RIA Hub URL (e.g. http://whitehorse:3005)")
p_reg.add_argument("--api-key", dest="api_key", required=True, help="Hub API key")
p_reg.add_argument(
"--api-key",
dest="api_key",
required=True,
help=(
"Personal registration key from the RIA Agents page on the hub "
"(format: ria_reg_...). Shown once when generated; save it then. "
"The legacy shared API key is also accepted but deprecated."
),
)
p_reg.add_argument("--name", default=None, help="Human-friendly agent name")
p_reg.add_argument("--insecure", action="store_true", help="Skip TLS verification")
p_reg.add_argument(

View File

@ -0,0 +1,142 @@
"""Structured error reporting for `ria-agent register` (T2)."""
from __future__ import annotations
import json
import sys
import urllib.error
from io import BytesIO
from unittest.mock import patch
import pytest
from ria_toolkit_oss.agent import cli as agent_cli
def _structured(reason: str) -> bytes:
return json.dumps({"detail": {"reason": reason}}).encode()
@pytest.mark.parametrize(
"reason",
["invalid_key", "expired", "revoked", "already_consumed"],
)
def test_explain_maps_known_reasons(reason):
msg = agent_cli._explain_registration_failure(403, _structured(reason))
assert msg == agent_cli.REGISTRATION_REASON_MESSAGES[reason]
def test_explain_unknown_reason_falls_through_with_code():
msg = agent_cli._explain_registration_failure(403, _structured("brand_new_thing"))
assert "brand_new_thing" in msg
assert "rejected" in msg.lower()
def test_explain_string_detail():
body = json.dumps({"detail": "Forbidden"}).encode()
msg = agent_cli._explain_registration_failure(403, body)
assert msg == "Registration rejected: Forbidden"
def test_explain_429_with_string_detail():
body = json.dumps({"detail": "Too many attempts; try again shortly"}).encode()
msg = agent_cli._explain_registration_failure(429, body)
assert "rate-limited" in msg
assert "Too many attempts" in msg
def test_explain_429_with_no_body():
msg = agent_cli._explain_registration_failure(429, b"")
assert "rate-limited" in msg
def test_explain_malformed_json():
msg = agent_cli._explain_registration_failure(500, b"<html>boom</html>")
assert msg.startswith("HTTP 500")
assert "boom" in msg
def test_explain_empty_body():
msg = agent_cli._explain_registration_failure(502, b"")
assert msg == "HTTP 502: no body"
def _http_error(status: int, body: bytes) -> urllib.error.HTTPError:
return urllib.error.HTTPError(
url="http://hub/screens/agents/register",
code=status,
msg="",
hdrs=None, # type: ignore[arg-type]
fp=BytesIO(body),
)
def test_user_agent_is_set_and_not_python_default():
"""Cloudflare on `riahub.ai` returns 403 code 1010 to `Python-urllib/*`.
Guarding the UA explicitly is the entire point of the register-flow fix;
if this test ever breaks, the production bug is back.
"""
ua = agent_cli._user_agent()
assert ua, "User-Agent must not be empty"
assert not ua.lower().startswith("python-urllib"), (
f"User-Agent must not be Python's default (got {ua!r}) — Cloudflare blocks it"
)
assert ua.startswith("ria-agent/")
def test_register_request_carries_explicit_user_agent(tmp_path):
"""Capture the outbound urllib Request and verify the UA header is set."""
cfg_path = tmp_path / "agent.json"
captured: dict = {}
def _fake_urlopen(req, *args, **kwargs):
# urllib normalizes header names; get_header takes the title-cased form.
captured["ua"] = req.get_header("User-agent")
captured["api_key"] = req.get_header("X-api-key")
captured["timeout"] = kwargs.get("timeout")
raise urllib.error.HTTPError(
url=req.full_url, code=403, msg="", hdrs=None, # type: ignore[arg-type]
fp=BytesIO(_structured("invalid_key")),
)
with (
patch.dict("os.environ", {"RIA_AGENT_CONFIG": str(cfg_path)}, clear=False),
patch("urllib.request.urlopen", side_effect=_fake_urlopen),
patch.object(
sys,
"argv",
["ria-agent", "register", "--hub", "http://hub:3005", "--api-key", "ria_reg_x"],
),
):
with pytest.raises(SystemExit):
agent_cli.main()
assert captured["ua"], "User-Agent header was not sent"
assert not captured["ua"].lower().startswith("python-urllib")
assert captured["api_key"] == "ria_reg_x"
assert captured["timeout"] is not None, "register must pass a timeout to urlopen"
def test_register_surfaces_reason_on_http_error(tmp_path, capsys):
cfg_path = tmp_path / "agent.json"
err = _http_error(403, _structured("revoked"))
with (
patch.dict("os.environ", {"RIA_AGENT_CONFIG": str(cfg_path)}, clear=False),
patch("urllib.request.urlopen", side_effect=err),
patch.object(
sys,
"argv",
["ria-agent", "register", "--hub", "http://hub:3005", "--api-key", "ria_reg_x"],
),
):
with pytest.raises(SystemExit) as exc:
agent_cli.main()
assert exc.value.code == 1
captured = capsys.readouterr()
assert "revoked" in captured.err.lower()
assert "Settings → RIA Agents" in captured.err
# Config must NOT be written on failure.
assert not cfg_path.exists()