Skip to content

feat(relay): managed-boot self-provision client (Phase 3, gateway side) - #48242

Merged
benbarclay merged 1 commit into
mainfrom
feat/relay-self-provision
Jun 18, 2026
Merged

feat(relay): managed-boot self-provision client (Phase 3, gateway side)#48242
benbarclay merged 1 commit into
mainfrom
feat/relay-self-provision

Conversation

@benbarclay

Copy link
Copy Markdown
Collaborator

Summary

The gateway half of relay Phase 3 — managed agents self-provision their relay credentials on boot, completing the zero-touch managed path. Pairs with the connector's POST /relay/provision (gateway-gateway#41).

On a managed boot with relay configured and no secret pinned, the runtime resolves the agent's own Nous access tokenPOST /relay/provision → sets GATEWAY_RELAY_ID/SECRET/DELIVERY_KEY into os.environ in-process → the immediately-following register_relay_adapter() reads them and dials out authenticated. No human, no enrollment token, no disk write.

Design decisions (settled with @benjamin)

  • In-process creds, no .env write. save_env_value hard-refuses under is_managed() anyway, and the runtime reads os.environ first — so the client sets creds in process memory. The secret never touches a volume, which is the stronger posture.
  • Re-provision every managed boot. Stateless: process-env creds don't survive a restart, so each boot that needs creds gets fresh ones; the connector's rotation window covers a still-connected prior instance. An explicitly-pinned GATEWAY_RELAY_SECRET is respected (skip).
  • Endpoint is gateway-asserted, uniform code path. GATEWAY_RELAY_ENDPOINT + GATEWAY_RELAY_ROUTE_KEYS (env or gateway.relay_* config). Self-hosted: the operator sets it. Hosted: NAS stamps it (the only case NAS knows the public URL). Both absent → outbound-only provisioning (credentials, no inbound routes). The connector scopes the asserted endpoint to the verified tenant, so it stays inside the security model.
  • Fail-soft. self_provision_if_managed() never raises — a provision failure logs and the gateway boots without relay auth rather than crashing.

What's in it

  • gateway/relay/__init__.pyrelay_endpoint(), relay_route_keys(), _provision_url(), _post_provision(), self_provision_if_managed().
  • gateway/run.py — calls self_provision_if_managed() immediately before register_relay_adapter() in the startup path (relay-scoped edit).

Self-hosted is unchanged — humans keep using hermes gateway enroll.

Test plan

  • 12 unit tests — trigger logic (managed + relay + no-secret), respect-pinned-secret, in-process env wiring, endpoint+routes vs outbound-only, fail-soft on token/connector failure. Mutation-checked: dropping the is_managed guard or the pinned-secret guard fails the tests.
  • Full relay suite green (96 passed).
  • Cross-repo live E2E (gateway_managed_provision_driver.py, lands on the connector side): drives this production self_provision_if_managed() against the real /relay/provision harness — proves a managed boot lands creds in-process and the secret authenticates a real relay WS upgrade, plus the outbound-only posture. Verified locally: 3/3 ALL PASS.

Companion PR (must merge first relative to the connector driver)

The connector needs a small fix — gatewayEndpoint was unconditionally required, which rejected the outbound-only posture. That fix + the cross-repo managed-boot E2E driver are a separate connector PR that depends on this PR being on main (the connector E2E job runs against hermes-agent main). Merge order: this PR → connector fix+driver PR.

Diffstat

221 lines client + 12 boot hook + 166 test = ~400 lines, fully additive (relay-scoped; no change to non-relay boot paths).

EXPERIMENTAL: the relay auth scheme may change until ≥2 Class-1 platforms validate the contract.

managed-boot self-provision

The gateway half of relay Phase 3. On a MANAGED boot with relay configured and
no secret pinned, the runtime self-provisions its relay credentials IN-PROCESS:
resolve the agent's own Nous access token (resolve_nous_access_token) -> POST
the connector's /relay/provision asserting its own endpoint + route keys ->
set GATEWAY_RELAY_ID/SECRET/DELIVERY_KEY into os.environ so the immediately-
following register_relay_adapter() reads them and dials out authenticated.

No human, no enrollment token, no disk write — the creds live only in process
memory (save_env_value refuses under managed anyway, and keeping the secret off
any volume is the stronger posture). Stateless: process-env creds don't survive
a restart, so a managed container re-provisions every boot; the connector's
rotation window covers a still-connected prior instance. An explicitly-pinned
GATEWAY_RELAY_SECRET is respected (skip). Self-hosted is unchanged: humans keep
using `hermes gateway enroll`.

Endpoint provenance is gateway-asserted (GATEWAY_RELAY_ENDPOINT +
GATEWAY_RELAY_ROUTE_KEYS, env or gateway.relay_* config) — uniform code path
whether the operator sets it (self-hosted) or NAS stamps it (hosted, the only
case NAS knows the public URL). Both absent -> outbound-only provisioning
(credentials, no inbound routes). The connector scopes the asserted endpoint to
the verified tenant, so it stays within the security model.

- gateway/relay/__init__.py: relay_endpoint(), relay_route_keys(),
  _provision_url(), _post_provision(), self_provision_if_managed() (never
  raises — a provision failure logs and boots without relay auth).
- gateway/run.py: call self_provision_if_managed() immediately before
  register_relay_adapter() in the startup path.

Tests: 12 unit (trigger logic, respect-pinned-secret, in-process env wiring,
endpoint+routes vs outbound-only, fail-soft on token/connector failure);
mutation-checked (drop is_managed guard / pinned-secret guard -> tests fail).
Cross-repo live E2E driver lands on the connector side (depends on this).

EXPERIMENTAL: relay auth scheme may change until >=2 Class-1 platforms validate.
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: feat/relay-self-provision vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10995 on HEAD, 10996 on base (✅ -1)

🆕 New issues (2):

Rule Count
unresolved-import 1
invalid-assignment 1
First entries
tests/gateway/relay/test_self_provision.py:11: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`

✅ Fixed issues (2):

Rule Count
unresolved-attribute 2
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`
run_agent.py:2941: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`

Unchanged: 5760 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists labels Jun 18, 2026
@benbarclay
benbarclay merged commit 0ddd21c into main Jun 18, 2026
35 checks passed
@benbarclay
benbarclay deleted the feat/relay-self-provision branch June 18, 2026 05:25
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…e) (NousResearch#48242)

The gateway half of relay Phase 3. On a MANAGED boot with relay configured and
no secret pinned, the runtime self-provisions its relay credentials IN-PROCESS:
resolve the agent's own Nous access token (resolve_nous_access_token) -> POST
the connector's /relay/provision asserting its own endpoint + route keys ->
set GATEWAY_RELAY_ID/SECRET/DELIVERY_KEY into os.environ so the immediately-
following register_relay_adapter() reads them and dials out authenticated.

No human, no enrollment token, no disk write — the creds live only in process
memory (save_env_value refuses under managed anyway, and keeping the secret off
any volume is the stronger posture). Stateless: process-env creds don't survive
a restart, so a managed container re-provisions every boot; the connector's
rotation window covers a still-connected prior instance. An explicitly-pinned
GATEWAY_RELAY_SECRET is respected (skip). Self-hosted is unchanged: humans keep
using `hermes gateway enroll`.

Endpoint provenance is gateway-asserted (GATEWAY_RELAY_ENDPOINT +
GATEWAY_RELAY_ROUTE_KEYS, env or gateway.relay_* config) — uniform code path
whether the operator sets it (self-hosted) or NAS stamps it (hosted, the only
case NAS knows the public URL). Both absent -> outbound-only provisioning
(credentials, no inbound routes). The connector scopes the asserted endpoint to
the verified tenant, so it stays within the security model.

- gateway/relay/__init__.py: relay_endpoint(), relay_route_keys(),
  _provision_url(), _post_provision(), self_provision_if_managed() (never
  raises — a provision failure logs and boots without relay auth).
- gateway/run.py: call self_provision_if_managed() immediately before
  register_relay_adapter() in the startup path.

Tests: 12 unit (trigger logic, respect-pinned-secret, in-process env wiring,
endpoint+routes vs outbound-only, fail-soft on token/connector failure);
mutation-checked (drop is_managed guard / pinned-secret guard -> tests fail).
Cross-repo live E2E driver lands on the connector side (depends on this).

EXPERIMENTAL: relay auth scheme may change until >=2 Class-1 platforms validate.
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…e) (NousResearch#48242)

The gateway half of relay Phase 3. On a MANAGED boot with relay configured and
no secret pinned, the runtime self-provisions its relay credentials IN-PROCESS:
resolve the agent's own Nous access token (resolve_nous_access_token) -> POST
the connector's /relay/provision asserting its own endpoint + route keys ->
set GATEWAY_RELAY_ID/SECRET/DELIVERY_KEY into os.environ so the immediately-
following register_relay_adapter() reads them and dials out authenticated.

No human, no enrollment token, no disk write — the creds live only in process
memory (save_env_value refuses under managed anyway, and keeping the secret off
any volume is the stronger posture). Stateless: process-env creds don't survive
a restart, so a managed container re-provisions every boot; the connector's
rotation window covers a still-connected prior instance. An explicitly-pinned
GATEWAY_RELAY_SECRET is respected (skip). Self-hosted is unchanged: humans keep
using `hermes gateway enroll`.

Endpoint provenance is gateway-asserted (GATEWAY_RELAY_ENDPOINT +
GATEWAY_RELAY_ROUTE_KEYS, env or gateway.relay_* config) — uniform code path
whether the operator sets it (self-hosted) or NAS stamps it (hosted, the only
case NAS knows the public URL). Both absent -> outbound-only provisioning
(credentials, no inbound routes). The connector scopes the asserted endpoint to
the verified tenant, so it stays within the security model.

- gateway/relay/__init__.py: relay_endpoint(), relay_route_keys(),
  _provision_url(), _post_provision(), self_provision_if_managed() (never
  raises — a provision failure logs and boots without relay auth).
- gateway/run.py: call self_provision_if_managed() immediately before
  register_relay_adapter() in the startup path.

Tests: 12 unit (trigger logic, respect-pinned-secret, in-process env wiring,
endpoint+routes vs outbound-only, fail-soft on token/connector failure);
mutation-checked (drop is_managed guard / pinned-secret guard -> tests fail).
Cross-repo live E2E driver lands on the connector side (depends on this).

EXPERIMENTAL: relay auth scheme may change until >=2 Class-1 platforms validate.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…e) (NousResearch#48242)

The gateway half of relay Phase 3. On a MANAGED boot with relay configured and
no secret pinned, the runtime self-provisions its relay credentials IN-PROCESS:
resolve the agent's own Nous access token (resolve_nous_access_token) -> POST
the connector's /relay/provision asserting its own endpoint + route keys ->
set GATEWAY_RELAY_ID/SECRET/DELIVERY_KEY into os.environ so the immediately-
following register_relay_adapter() reads them and dials out authenticated.

No human, no enrollment token, no disk write — the creds live only in process
memory (save_env_value refuses under managed anyway, and keeping the secret off
any volume is the stronger posture). Stateless: process-env creds don't survive
a restart, so a managed container re-provisions every boot; the connector's
rotation window covers a still-connected prior instance. An explicitly-pinned
GATEWAY_RELAY_SECRET is respected (skip). Self-hosted is unchanged: humans keep
using `hermes gateway enroll`.

Endpoint provenance is gateway-asserted (GATEWAY_RELAY_ENDPOINT +
GATEWAY_RELAY_ROUTE_KEYS, env or gateway.relay_* config) — uniform code path
whether the operator sets it (self-hosted) or NAS stamps it (hosted, the only
case NAS knows the public URL). Both absent -> outbound-only provisioning
(credentials, no inbound routes). The connector scopes the asserted endpoint to
the verified tenant, so it stays within the security model.

- gateway/relay/__init__.py: relay_endpoint(), relay_route_keys(),
  _provision_url(), _post_provision(), self_provision_if_managed() (never
  raises — a provision failure logs and boots without relay auth).
- gateway/run.py: call self_provision_if_managed() immediately before
  register_relay_adapter() in the startup path.

Tests: 12 unit (trigger logic, respect-pinned-secret, in-process env wiring,
endpoint+routes vs outbound-only, fail-soft on token/connector failure);
mutation-checked (drop is_managed guard / pinned-secret guard -> tests fail).
Cross-repo live E2E driver lands on the connector side (depends on this).

EXPERIMENTAL: relay auth scheme may change until >=2 Class-1 platforms validate.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…e) (NousResearch#48242)

The gateway half of relay Phase 3. On a MANAGED boot with relay configured and
no secret pinned, the runtime self-provisions its relay credentials IN-PROCESS:
resolve the agent's own Nous access token (resolve_nous_access_token) -> POST
the connector's /relay/provision asserting its own endpoint + route keys ->
set GATEWAY_RELAY_ID/SECRET/DELIVERY_KEY into os.environ so the immediately-
following register_relay_adapter() reads them and dials out authenticated.

No human, no enrollment token, no disk write — the creds live only in process
memory (save_env_value refuses under managed anyway, and keeping the secret off
any volume is the stronger posture). Stateless: process-env creds don't survive
a restart, so a managed container re-provisions every boot; the connector's
rotation window covers a still-connected prior instance. An explicitly-pinned
GATEWAY_RELAY_SECRET is respected (skip). Self-hosted is unchanged: humans keep
using `hermes gateway enroll`.

Endpoint provenance is gateway-asserted (GATEWAY_RELAY_ENDPOINT +
GATEWAY_RELAY_ROUTE_KEYS, env or gateway.relay_* config) — uniform code path
whether the operator sets it (self-hosted) or NAS stamps it (hosted, the only
case NAS knows the public URL). Both absent -> outbound-only provisioning
(credentials, no inbound routes). The connector scopes the asserted endpoint to
the verified tenant, so it stays within the security model.

- gateway/relay/__init__.py: relay_endpoint(), relay_route_keys(),
  _provision_url(), _post_provision(), self_provision_if_managed() (never
  raises — a provision failure logs and boots without relay auth).
- gateway/run.py: call self_provision_if_managed() immediately before
  register_relay_adapter() in the startup path.

Tests: 12 unit (trigger logic, respect-pinned-secret, in-process env wiring,
endpoint+routes vs outbound-only, fail-soft on token/connector failure);
mutation-checked (drop is_managed guard / pinned-secret guard -> tests fail).
Cross-repo live E2E driver lands on the connector side (depends on this).

EXPERIMENTAL: relay auth scheme may change until >=2 Class-1 platforms validate.
teknium1 added a commit that referenced this pull request Jul 29, 2026
- New page user-guide/messaging/relay.md: what Relay is, enrollment
  (hermes gateway enroll), capability handshake (media, native
  approval/clarify, threads, typing), config keys, troubleshooting —
  derived from gateway/relay/ + gateway_enroll.py (PRs #48147 #48242
  #71300 #71363 #71404 #71624 #69721).
- desktop.md: artifacts viewer, timeline rail, find-in-page,
  multi-window, git review/worktrees, multi-terminal + persistence,
  theme import, rebindable shortcuts, quick entry, context-usage
  popover, keep-awake, command palette, Hermes Cloud mode, memory
  graph — all verified against apps/desktop/ source.
teknium1 added a commit that referenced this pull request Jul 29, 2026
- New page user-guide/messaging/relay.md: what Relay is, enrollment
  (hermes gateway enroll), capability handshake (media, native
  approval/clarify, threads, typing), config keys, troubleshooting —
  derived from gateway/relay/ + gateway_enroll.py (PRs #48147 #48242
  #71300 #71363 #71404 #71624 #69721).
- desktop.md: artifacts viewer, timeline rail, find-in-page,
  multi-window, git review/worktrees, multi-terminal + persistence,
  theme import, rebindable shortcuts, quick entry, context-usage
  popover, keep-awake, command palette, Hermes Cloud mode, memory
  graph — all verified against apps/desktop/ source.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…e) (NousResearch#48242)

The gateway half of relay Phase 3. On a MANAGED boot with relay configured and
no secret pinned, the runtime self-provisions its relay credentials IN-PROCESS:
resolve the agent's own Nous access token (resolve_nous_access_token) -> POST
the connector's /relay/provision asserting its own endpoint + route keys ->
set GATEWAY_RELAY_ID/SECRET/DELIVERY_KEY into os.environ so the immediately-
following register_relay_adapter() reads them and dials out authenticated.

No human, no enrollment token, no disk write — the creds live only in process
memory (save_env_value refuses under managed anyway, and keeping the secret off
any volume is the stronger posture). Stateless: process-env creds don't survive
a restart, so a managed container re-provisions every boot; the connector's
rotation window covers a still-connected prior instance. An explicitly-pinned
GATEWAY_RELAY_SECRET is respected (skip). Self-hosted is unchanged: humans keep
using `hermes gateway enroll`.

Endpoint provenance is gateway-asserted (GATEWAY_RELAY_ENDPOINT +
GATEWAY_RELAY_ROUTE_KEYS, env or gateway.relay_* config) — uniform code path
whether the operator sets it (self-hosted) or NAS stamps it (hosted, the only
case NAS knows the public URL). Both absent -> outbound-only provisioning
(credentials, no inbound routes). The connector scopes the asserted endpoint to
the verified tenant, so it stays within the security model.

- gateway/relay/__init__.py: relay_endpoint(), relay_route_keys(),
  _provision_url(), _post_provision(), self_provision_if_managed() (never
  raises — a provision failure logs and boots without relay auth).
- gateway/run.py: call self_provision_if_managed() immediately before
  register_relay_adapter() in the startup path.

Tests: 12 unit (trigger logic, respect-pinned-secret, in-process env wiring,
endpoint+routes vs outbound-only, fail-soft on token/connector failure);
mutation-checked (drop is_managed guard / pinned-secret guard -> tests fail).
Cross-repo live E2E driver lands on the connector side (depends on this).

EXPERIMENTAL: relay auth scheme may change until >=2 Class-1 platforms validate.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
- New page user-guide/messaging/relay.md: what Relay is, enrollment
  (hermes gateway enroll), capability handshake (media, native
  approval/clarify, threads, typing), config keys, troubleshooting —
  derived from gateway/relay/ + gateway_enroll.py (PRs NousResearch#48147 NousResearch#48242
  NousResearch#71300 NousResearch#71363 NousResearch#71404 NousResearch#71624 NousResearch#69721).
- desktop.md: artifacts viewer, timeline rail, find-in-page,
  multi-window, git review/worktrees, multi-terminal + persistence,
  theme import, rebindable shortcuts, quick entry, context-usage
  popover, keep-awake, command palette, Hermes Cloud mode, memory
  graph — all verified against apps/desktop/ source.
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
- New page user-guide/messaging/relay.md: what Relay is, enrollment
  (hermes gateway enroll), capability handshake (media, native
  approval/clarify, threads, typing), config keys, troubleshooting —
  derived from gateway/relay/ + gateway_enroll.py (PRs NousResearch#48147 NousResearch#48242
  NousResearch#71300 NousResearch#71363 NousResearch#71404 NousResearch#71624 NousResearch#69721).
- desktop.md: artifacts viewer, timeline rail, find-in-page,
  multi-window, git review/worktrees, multi-terminal + persistence,
  theme import, rebindable shortcuts, quick entry, context-usage
  popover, keep-awake, command palette, Hermes Cloud mode, memory
  graph — all verified against apps/desktop/ source.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants