feat(sync): default the sync plane to production

Skill Sync had no default base URL, so a user with no `sync.base_url` in
config.yaml and no HERMES_SYNC_BASE_URL got:

    sync inert: no sync base URL configured (config.yaml sync.base_url
    or HERMES_SYNC_BASE_URL).

Every sync command was unusable out of the box. The URL was left unset
because the plane did not exist yet when the client was written; it does now.

- Adds DEFAULT_SYNC_BASE_URL = "https://gateway-gateway.nousresearch.com" and
  returns it as the last step of resolve_sync_base_url().
- Resolution order is unchanged otherwise: HERMES_SYNC_BASE_URL ->
  config.yaml sync.base_url -> production default. The env var and config key
  now exist to point a dev/staging build at another plane rather than to make
  the feature work at all.
- Follows the existing precedent for production endpoints in this codebase
  (DEFAULT_NOUS_PORTAL_URL in hermes_cli/auth.py, HERMES_DIAGNOSTICS_BASE_URL
  in diagnostics_upload.py): a module constant with env/config override.

The "no sync base URL configured" guards are kept — they are now unreachable
in practice but remain correct if the default is ever blanked.

Tests: 3 new — the default is returned when nothing is configured, config
still overrides it, and the constant is a bare https origin (no trailing
slash, no path) since the client appends /v1/sync/. 2349 passed / 0 failed
across 56 suites via scripts/run_tests.sh.

Verified against a temp HERMES_HOME with no config: resolves to the
production plane; HERMES_SYNC_BASE_URL and sync.base_url both still win, and
trailing slashes are stripped.
This commit is contained in:
Ben Barclay 2026-07-29 08:14:03 -07:00
parent f9c4d835f9
commit e327eaa2a0
2 changed files with 44 additions and 10 deletions

View file

@ -694,6 +694,33 @@ class TestEnvConfig:
monkeypatch.setenv("HERMES_SYNC_BASE_URL", "https://plane.example/")
assert ssc.resolve_sync_base_url() == "https://plane.example"
def test_base_url_defaults_to_production(self, monkeypatch):
# With nothing configured a user must still reach the real plane —
# otherwise every sync command fails with "no base URL configured".
monkeypatch.delenv("HERMES_SYNC_BASE_URL", raising=False)
monkeypatch.setattr("hermes_cli.config.load_config", lambda: {}, raising=False)
assert ssc.resolve_sync_base_url() == ssc.DEFAULT_SYNC_BASE_URL
def test_default_is_a_bare_https_origin(self):
# The client appends /v1/sync/, so the default must be a scheme+host
# origin with no trailing slash and no path.
from urllib.parse import urlparse
parsed = urlparse(ssc.DEFAULT_SYNC_BASE_URL)
assert parsed.scheme == "https"
assert parsed.netloc
assert parsed.path == ""
assert not ssc.DEFAULT_SYNC_BASE_URL.endswith("/")
def test_config_overrides_default(self, monkeypatch):
monkeypatch.delenv("HERMES_SYNC_BASE_URL", raising=False)
monkeypatch.setattr(
"hermes_cli.config.load_config",
lambda: {"sync": {"base_url": "https://cfg.example/"}},
raising=False,
)
assert ssc.resolve_sync_base_url() == "https://cfg.example"
def test_feature_enabled_env(self, monkeypatch):
# Default off.
monkeypatch.delenv("HERMES_SYNC_ENABLED", raising=False)

View file

@ -295,18 +295,25 @@ def dev_gate_open() -> bool:
# ---------------------------------------------------------------------------
# Sync-plane endpoint resolution
#
# The sync routes are mounted under /v1/sync/. The base URL is
# configurable (config.yaml sync.base_url or HERMES_SYNC_BASE_URL bridge env);
# it is NOT the inference base_url. When unset, sync is inert -- there is no
# server to talk to yet (the server is being built in parallel).
# The sync routes are mounted under /v1/sync/. The base URL defaults to the
# production plane, so a normal user configures nothing; config.yaml
# sync.base_url (or the HERMES_SYNC_BASE_URL bridge env) overrides it to point
# a dev/staging build at another plane. It is NOT the inference base_url.
# ---------------------------------------------------------------------------
def resolve_sync_base_url() -> Optional[str]:
"""Resolve the sync-plane base URL, or None when unconfigured.
#: Production Skill Sync plane. Overridable per the resolution order below.
DEFAULT_SYNC_BASE_URL = "https://gateway-gateway.nousresearch.com"
Order: HERMES_SYNC_BASE_URL env bridge -> config.yaml ``sync.base_url``.
Returns a base without a trailing slash (e.g. ``https://host``); the
``/v1/sync/`` prefix is appended by the client.
def resolve_sync_base_url() -> Optional[str]:
"""Resolve the sync-plane base URL.
Order: HERMES_SYNC_BASE_URL env bridge -> config.yaml ``sync.base_url`` ->
the production plane. Returns a base without a trailing slash (e.g.
``https://host``); the ``/v1/sync/`` prefix is appended by the client.
The production default means a normal user never configures a URL the
env var and config key exist to point a dev/staging build at another
plane. Returns None only if the default is somehow blanked out.
"""
env = os.getenv("HERMES_SYNC_BASE_URL")
if env and env.strip():
@ -324,7 +331,7 @@ def resolve_sync_base_url() -> Optional[str]:
return base.strip().rstrip("/")
except Exception as e:
logger.debug("skills_sync_client: config sync.base_url read failed: %s", e)
return None
return DEFAULT_SYNC_BASE_URL or None
# ---------------------------------------------------------------------------