refactor(hermes): rename the owner agent host to worker.bstein.dev

Introduce worker.bstein.dev as the canonical hostname for the owner-only
Hermes coordinator, previously agent.hermes.bstein.dev.

The rename is additive, matching the shape #38 restored for chat and triage.
CoreDNS, both agent Ingresses and the hermes-sites certificate now serve BOTH
names, so merging this cannot take away the endpoint the operator uses to
reach the coordinator. Retiring agent.hermes.bstein.dev is a separate,
separately scheduled change. No redirect middleware is added.

What switches to the new host:
- HERMES_DASHBOARD_PUBLIC_URL and the oauth2-proxy --redirect-url
- the Keycloak agent proxy rootUrl
- operator docs, skills, the ZAP baseline target and the triage monitor default

What stays dual-homed until retirement:
- CoreDNS hosts entry, both agent Ingress rules, certificate SANs
- API_SERVER_CORS_ORIGINS (now a comma-separated pair)
- the Keycloak redirect URIs, web origins and post-logout origins, so a
  rollback only needs the oauth2-proxy --redirect-url reverted and does not
  require re-running the ensure job

The agent client passes its legacy origin through the optional fourth argument
#38 added to ensure_proxy_client, so no second mechanism is introduced. The
immutable ensure Job goes -11 -> -12 because #38 already consumed -11 and that
run has completed; without a further bump this change would never be applied.
Login on the new host fails until the -12 Job completes.

Because the session and CSRF cookies use the __Host- prefix they are bound to
one origin, so a fresh login must start on worker.bstein.dev and existing
sessions do not carry over -- re-login is required after rollout.

#38's public-host continuity test now covers the agent proxy's dual origins
rather than asserting the agent surface was untouched by the rename.

Knowledge catalogs and diagrams regenerated with `make knowledge`.
This commit is contained in:
Hermes Agent 2026-08-21 10:29:46 +00:00
parent 004c41629e
commit 94106bf252
27 changed files with 241 additions and 33 deletions

View File

@ -49,6 +49,7 @@ data:
192.168.22.9 triage.bstein.dev
192.168.22.9 triage.hermes.bstein.dev
192.168.22.9 vault.bstein.dev
192.168.22.9 worker.bstein.dev
fallthrough
}
}

View File

@ -1,7 +1,7 @@
{
"counts": {
"helmrelease_host_hints": 23,
"http_endpoints": 63,
"http_endpoints": 65,
"services": 98,
"workloads": 130
}

View File

@ -5550,6 +5550,46 @@
"name": "wolf",
"source": "game-stream"
}
},
{
"host": "worker.bstein.dev",
"path": "/",
"backend": {
"namespace": "hermes",
"service": "oauth2-proxy-hermes-agent",
"port": "http",
"workloads": [
{
"kind": "Deployment",
"name": "hermes-agent"
}
]
},
"via": {
"kind": "Ingress",
"name": "hermes-agent-dashboard",
"source": "hermes"
}
},
{
"host": "worker.bstein.dev",
"path": "/terminal",
"backend": {
"namespace": "hermes",
"service": "oauth2-proxy-hermes-agent",
"port": "http",
"workloads": [
{
"kind": "Deployment",
"name": "hermes-agent"
}
]
},
"via": {
"kind": "Ingress",
"name": "hermes-agent-terminal",
"source": "hermes"
}
}
],
"helmrelease_host_hints": {

View File

@ -3607,6 +3607,28 @@ http_endpoints:
kind: Ingress
name: wolf
source: game-stream
- host: worker.bstein.dev
path: /
backend:
namespace: hermes
service: oauth2-proxy-hermes-agent
port: http
workloads: *id001
via:
kind: Ingress
name: hermes-agent-dashboard
source: hermes
- host: worker.bstein.dev
path: /terminal
backend:
namespace: hermes
service: oauth2-proxy-hermes-agent
port: http
workloads: *id001
via:
kind: Ingress
name: hermes-agent-terminal
source: hermes
helmrelease_host_hints:
cert-manager:cert-manager/cert-manager:
- atlas.bstein.dev

File diff suppressed because one or more lines are too long

View File

@ -198,6 +198,8 @@ flowchart LR
host_wolf_bstein_dev --> svc_game_stream_oauth2_proxy_wolf
wl_game_stream_oauth2_proxy_wolf["game-stream/oauth2-proxy-wolf (Deployment)"]
svc_game_stream_oauth2_proxy_wolf --> wl_game_stream_oauth2_proxy_wolf
host_worker_bstein_dev["worker.bstein.dev"]
host_worker_bstein_dev --> svc_hermes_oauth2_proxy_hermes_agent
subgraph bstein_dev_home[bstein-dev-home]
svc_bstein_dev_home_bstein_dev_home_frontend

View File

@ -99,7 +99,7 @@ in-process call removed a whole pod launch from the critical path.
`hermes-demo-test-<N>` (failed) and `hermes-demo-repair-<N>` (succeeded);
both TTL-clean after 1 h.
- Hermes side: the run appears in the dashboard at
`https://agent.hermes.bstein.dev` (session/run history).
`https://worker.bstein.dev` (session/run history).
- Escalation path: alert `HermesTriageHumanRequired` in vmalert
(`vmalert-atlas-availability` deployment, 1 m interval, `for: 2m`)
fires to Alertmanager for any `human_required` incident. Gauges are

View File

@ -40,7 +40,7 @@ JENKINS = os.environ.get("JENKINS_URL", "https://ci.bstein.dev")
GITEA = os.environ.get("GITEA_URL", "https://scm.bstein.dev")
CODE_REPO_OWNER = os.environ.get("CODE_REPO_OWNER", "atlas")
GRAFANA = os.environ.get("GRAFANA_URL", "https://metrics.bstein.dev")
HERMES_UI = os.environ.get("HERMES_URL", "https://agent.hermes.bstein.dev")
HERMES_UI = os.environ.get("HERMES_URL", "https://worker.bstein.dev")
POLL_SECONDS = 6
# `--filter <text>` restricts the monitor to incidents whose id contains that

View File

@ -1,7 +1,7 @@
{
"counts": {
"helmrelease_host_hints": 23,
"http_endpoints": 63,
"http_endpoints": 65,
"services": 98,
"workloads": 130
}

View File

@ -5550,6 +5550,46 @@
"name": "wolf",
"source": "game-stream"
}
},
{
"host": "worker.bstein.dev",
"path": "/",
"backend": {
"namespace": "hermes",
"service": "oauth2-proxy-hermes-agent",
"port": "http",
"workloads": [
{
"kind": "Deployment",
"name": "hermes-agent"
}
]
},
"via": {
"kind": "Ingress",
"name": "hermes-agent-dashboard",
"source": "hermes"
}
},
{
"host": "worker.bstein.dev",
"path": "/terminal",
"backend": {
"namespace": "hermes",
"service": "oauth2-proxy-hermes-agent",
"port": "http",
"workloads": [
{
"kind": "Deployment",
"name": "hermes-agent"
}
]
},
"via": {
"kind": "Ingress",
"name": "hermes-agent-terminal",
"source": "hermes"
}
}
],
"helmrelease_host_hints": {

View File

@ -3607,6 +3607,28 @@ http_endpoints:
kind: Ingress
name: wolf
source: game-stream
- host: worker.bstein.dev
path: /
backend:
namespace: hermes
service: oauth2-proxy-hermes-agent
port: http
workloads: *id001
via:
kind: Ingress
name: hermes-agent-dashboard
source: hermes
- host: worker.bstein.dev
path: /terminal
backend:
namespace: hermes
service: oauth2-proxy-hermes-agent
port: http
workloads: *id001
via:
kind: Ingress
name: hermes-agent-terminal
source: hermes
helmrelease_host_hints:
cert-manager:cert-manager/cert-manager:
- atlas.bstein.dev

File diff suppressed because one or more lines are too long

View File

@ -198,6 +198,8 @@ flowchart LR
host_wolf_bstein_dev --> svc_game_stream_oauth2_proxy_wolf
wl_game_stream_oauth2_proxy_wolf["game-stream/oauth2-proxy-wolf (Deployment)"]
svc_game_stream_oauth2_proxy_wolf --> wl_game_stream_oauth2_proxy_wolf
host_worker_bstein_dev["worker.bstein.dev"]
host_worker_bstein_dev --> svc_hermes_oauth2_proxy_hermes_agent
subgraph bstein_dev_home[bstein-dev-home]
svc_bstein_dev_home_bstein_dev_home_frontend

View File

@ -99,7 +99,7 @@ in-process call removed a whole pod launch from the critical path.
`hermes-demo-test-<N>` (failed) and `hermes-demo-repair-<N>` (succeeded);
both TTL-clean after 1 h.
- Hermes side: the run appears in the dashboard at
`https://agent.hermes.bstein.dev` (session/run history).
`https://worker.bstein.dev` (session/run history).
- Escalation path: alert `HermesTriageHumanRequired` in vmalert
(`vmalert-atlas-availability` deployment, 1 m interval, `for: 2m`)
fires to Alertmanager for any `human_required` incident. Gauges are

View File

@ -5,7 +5,7 @@ This is the mental model and demonstration script for the operator instance at
consumer instance at `chat.bstein.dev` is intentionally separate and is not the
place to perform infrastructure triage.
`agent.hermes.bstein.dev` is the owner-only engineering control plane. Its root
`worker.bstein.dev` is the owner-only engineering control plane. Its root
serves the stock Hermes dashboard, including embedded chat/TUI and the native
sessions, files, models, logs, Kanban, skills, plugins, MCP, profiles, and
configuration surfaces. `/terminal/` remains the raw full-screen TUI. The
@ -143,7 +143,7 @@ off. The final TypeScript build, Python compile, and source assertions remain
mandatory, so an omitted or drifted replay fails before any image can pass
release verification.
From agent.hermes, trigger that one fixed job with:
From worker.bstein.dev, trigger that one fixed job with:
```sh
jenkins_image_build_trigger.py '<reviewed full main commit>'

View File

@ -17,3 +17,6 @@ spec:
# the rename in #34 must not break links or sessions already in flight.
- chat.hermes.bstein.dev
- triage.hermes.bstein.dev
# worker.bstein.dev is the canonical owner-agent host; the legacy
# agent.hermes.bstein.dev SAN above stays until it is retired on purpose.
- worker.bstein.dev

View File

@ -142,7 +142,7 @@ data:
- "*git clean -f*"
dashboard:
public_url: https://agent.hermes.bstein.dev
public_url: https://worker.bstein.dev
display:
compact: true
@ -174,7 +174,7 @@ data:
backup_keep: 5
non_interactive_local_changes: stash
SOUL.md: |
You are Brad's private Hermes coordinator at agent.hermes.bstein.dev. Turn
You are Brad's private Hermes coordinator at worker.bstein.dev. Turn
objectives into organized, reviewable delivery without making Brad manage
model names, terminals, or provider capacity.
@ -408,7 +408,7 @@ data:
START-HERE.md: |
# Agent Hermes
The authenticated root of agent.hermes.bstein.dev opens Hermes' stock
The authenticated root of worker.bstein.dev opens Hermes' stock
dashboard with embedded chat/TUI, sessions, files, models, logs, Kanban,
skills, plugins, MCP, profiles, and configuration. `/terminal/` opens the
raw full-screen Hermes TUI. Give Hermes

View File

@ -624,14 +624,15 @@ spec:
- {name: HERMES_DASHBOARD_HOST, value: 127.0.0.1}
- {name: HERMES_DASHBOARD_PORT, value: "9119"}
- {name: HERMES_DASHBOARD_FILES_ROOT, value: /opt/data/workspace}
- {name: HERMES_DASHBOARD_PUBLIC_URL, value: https://agent.hermes.bstein.dev}
- {name: HERMES_DASHBOARD_PUBLIC_URL, value: https://worker.bstein.dev}
- {name: HERMES_TUI_AGENT_INIT_TIMEOUT_S, value: "180"}
- {name: HERMES_STREAM_STALE_TIMEOUT, value: "600"}
- {name: HERMES_API_CALL_STALE_TIMEOUT, value: "600"}
- {name: API_SERVER_ENABLED, value: "true"}
- {name: API_SERVER_HOST, value: 0.0.0.0}
- {name: API_SERVER_PORT, value: "8642"}
- {name: API_SERVER_CORS_ORIGINS, value: https://agent.hermes.bstein.dev}
# Comma-separated; the legacy origin stays until agent.hermes.bstein.dev is retired.
- {name: API_SERVER_CORS_ORIGINS, value: "https://worker.bstein.dev,https://agent.hermes.bstein.dev"}
- {name: HERMES_MEDIA_DELIVERY_STRICT, value: "1"}
- {name: HERMES_MEDIA_ALLOW_DIRS, value: /opt/data/workspace}
- {name: HERMES_AUTO_ROUTER_PROFILE, value: agent}
@ -694,7 +695,11 @@ spec:
args:
- --provider=oidc
- --config=/vault/secrets/oidc-config
- --redirect-url=https://agent.hermes.bstein.dev/oauth2/callback
# Single canonical callback. The __Host- cookie prefix below binds both the
# session and the CSRF cookie to one host, so a fresh login must start on
# worker.bstein.dev; agent.hermes.bstein.dev keeps serving already
# authenticated sessions until it is retired separately.
- --redirect-url=https://worker.bstein.dev/oauth2/callback
- --oidc-issuer-url=https://sso.bstein.dev/realms/atlas
- --user-id-claim=sub
- --oidc-groups-claim=groups

View File

@ -6,8 +6,10 @@ metadata:
namespace: hermes
spec:
redirectRegex:
regex: ^https://agent\.hermes\.bstein\.dev/terminal$
replacement: https://agent.hermes.bstein.dev/terminal/
# Host-preserving so the trailing-slash fixup works on both the canonical
# worker.bstein.dev and the legacy agent.hermes.bstein.dev host.
regex: ^https://(worker\.bstein\.dev|agent\.hermes\.bstein\.dev)/terminal$
replacement: https://${1}/terminal/
permanent: false
---
apiVersion: traefik.io/v1alpha1
@ -33,9 +35,21 @@ spec:
ingressClassName: traefik
tls:
- hosts:
- worker.bstein.dev
- agent.hermes.bstein.dev
secretName: hermes-sites-tls
rules:
- host: worker.bstein.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: oauth2-proxy-hermes-agent
port:
name: http
# Legacy host, served unchanged until it is retired in a separate change.
- host: agent.hermes.bstein.dev
http:
paths:
@ -60,9 +74,21 @@ spec:
ingressClassName: traefik
tls:
- hosts:
- worker.bstein.dev
- agent.hermes.bstein.dev
secretName: hermes-sites-tls
rules:
- host: worker.bstein.dev
http:
paths:
- path: /terminal
pathType: Prefix
backend:
service:
name: oauth2-proxy-hermes-agent
port:
name: http
# Legacy host, served unchanged until it is retired in a separate change.
- host: agent.hermes.bstein.dev
http:
paths:
@ -90,6 +116,7 @@ spec:
- agent.hermes.bstein.dev
- chat.bstein.dev
- triage.bstein.dev
- worker.bstein.dev
- chat.hermes.bstein.dev
- triage.hermes.bstein.dev
secretName: hermes-sites-tls

View File

@ -113,7 +113,7 @@ data:
This is the dedicated triage appliance at triage.bstein.dev. Keep
automated Ariadne intake and testing conversations here. Project delivery
and coding orchestration belong to agent.hermes.bstein.dev; general user
and coding orchestration belong to worker.bstein.dev; general user
chat belongs to chat.bstein.dev.
Start in AUTO routing with a careful, intelligence-biased posture. Every

View File

@ -8,7 +8,7 @@ asserting health, placement, ownership, or current model availability.
| Surface | Purpose | Identity boundary | State and permissions |
| --- | --- | --- | --- |
| `triage.bstein.dev` | Brad's automated testing triage | Keycloak plus an outer oauth2-proxy exact-email allow-list for `brad@bstein.dev` | `hermes` namespace, its own PVC and service account; read-only cluster triage plus approved internal evidence endpoints |
| `agent.hermes.bstein.dev` | Brad's project coordinator | Keycloak plus an outer oauth2-proxy exact-email allow-list for `brad@bstein.dev` | `hermes` namespace and separate PVC; native Hermes delegates bounded work while Herdr preserves real Codex and Claude Code CLI sessions when needed |
| `worker.bstein.dev` | Brad's project coordinator | Keycloak plus an outer oauth2-proxy exact-email allow-list for `brad@bstein.dev` | `hermes` namespace and separate PVC; native Hermes delegates bounded work while Herdr preserves real Codex and Claude Code CLI sessions when needed |
| `chat.bstein.dev` | Private consumer chat and research through Hermes WebUI or a linked Telegram DM | Keycloak login plus one-time Telegram account link | One Hermes process and PVC per assigned Keycloak subject; no Kubernetes RBAC, terminal, or private-service access |
The instances do not share conversation state, credentials, profiles, skills

View File

@ -3,7 +3,7 @@
apiVersion: batch/v1
kind: Job
metadata:
name: hermes-access-oidc-client-ensure-11
name: hermes-access-oidc-client-ensure-12
namespace: sso
spec:
backoffLimit: 3

View File

@ -343,7 +343,8 @@ ensure_telegram_config() {
ensure_hermes_owner
ensure_proxy_client "hermes-chat-proxy" "https://chat.bstein.dev" "hermes/chat-oidc" \
"https://chat.hermes.bstein.dev"
ensure_proxy_client "hermes-agent-proxy" "https://agent.hermes.bstein.dev" "hermes/agent-oidc"
ensure_proxy_client "hermes-agent-proxy" "https://worker.bstein.dev" "hermes/agent-oidc" \
"https://agent.hermes.bstein.dev"
ensure_proxy_client "hermes-triage-proxy" "https://triage.bstein.dev" "hermes/triage-oidc" \
"https://triage.hermes.bstein.dev"
ensure_service_account_client "hermes-automation" "hermes/developer-keycloak"

View File

@ -26,7 +26,7 @@ data:
https://budget.bstein.dev
https://money.bstein.dev
https://health.bstein.dev
https://agent.hermes.bstein.dev
https://worker.bstein.dev
https://chat.bstein.dev
https://triage.bstein.dev
https://cassandra.bstein.dev

View File

@ -63,7 +63,7 @@ def test_owner_agent_installs_the_pinned_operator_toolchain():
assert 'chmod 0644 "${profile_file}"' in init_command
def test_owner_agent_uses_only_the_canonical_hostname():
def test_owner_agent_uses_the_canonical_hostname():
paths = [
HERMES / "agent-configmap.yaml",
HERMES / "agent-deployment.yaml",
@ -73,7 +73,40 @@ def test_owner_agent_uses_only_the_canonical_hostname():
for path in paths:
content = path.read_text()
assert "agent.bstein.dev" not in content
assert "agent.hermes.bstein.dev" in content
assert "worker.bstein.dev" in content
def test_legacy_owner_host_stays_served_until_it_is_retired_separately():
"""The rename must not cut off the host the operator reaches Hermes on.
``agent.hermes.bstein.dev`` keeps resolving, routing and validating TLS
until it is retired in its own change, so rolling the rename back never
needs more than reverting the oauth2-proxy callback.
"""
legacy = "agent.hermes.bstein.dev"
canonical = "worker.bstein.dev"
coredns = (
Path(__file__).parents[2] / "infrastructure/core/coredns-custom.yaml"
).read_text()
assert f"192.168.22.9 {legacy}" in coredns
assert f"192.168.22.9 {canonical}" in coredns
certificate = yaml.safe_load((HERMES / "agent-certificate.yaml").read_text())
assert {legacy, canonical} <= set(certificate["spec"]["dnsNames"])
ingresses = [
doc
for doc in yaml.safe_load_all((HERMES / "agent-ingress.yaml").read_text())
if doc
and doc.get("kind") == "Ingress"
and doc["metadata"]["name"].startswith("hermes-agent-")
]
assert len(ingresses) == 2
for ingress in ingresses:
assert {legacy, canonical} <= {rule["host"] for rule in ingress["spec"]["rules"]}
for tls in ingress["spec"]["tls"]:
assert {legacy, canonical} <= set(tls["hosts"])
def test_agent_reconnect_retains_complete_history_and_long_tool_budget():

View File

@ -122,7 +122,7 @@ def test_owner_agent_installs_the_pinned_operator_toolchain():
assert 'chmod 0644 "${profile_file}"' in init_command
def test_owner_agent_uses_only_the_canonical_hostname():
def test_owner_agent_uses_the_canonical_hostname():
paths = [
HERMES / "agent-configmap.yaml",
HERMES / "agent-deployment.yaml",
@ -132,7 +132,7 @@ def test_owner_agent_uses_only_the_canonical_hostname():
for path in paths:
content = path.read_text()
assert "agent.bstein.dev" not in content
assert "agent.hermes.bstein.dev" in content
assert "worker.bstein.dev" in content
def test_agent_reconnect_retains_complete_history_and_long_tool_budget():

View File

@ -35,8 +35,10 @@ PUBLIC_HOSTS = {
"triage.hermes.bstein.dev": "oauth2-proxy-hermes-triage",
}
# The agent surface was deliberately untouched by the rename.
# The agent surface is mid-rename too: worker.bstein.dev is canonical and the
# legacy name stays served until a deliberate retirement change removes it.
AGENT_HOST = "agent.hermes.bstein.dev"
AGENT_CANONICAL_HOST = "worker.bstein.dev"
def _docs(path: Path) -> list[dict]:
@ -107,21 +109,29 @@ def test_host_is_registered_with_keycloak(host: str, ensure_script: str):
assert f"https://{host}" in ensure_script
def test_agent_surface_was_not_touched_by_the_rename(
certificate: dict, coredns_hosts: set[str], ensure_script: str
@pytest.mark.parametrize("host", [AGENT_HOST, AGENT_CANONICAL_HOST])
def test_agent_surface_serves_both_hosts_during_its_rename(
host: str, certificate: dict, coredns_hosts: set[str], ensure_script: str
):
assert AGENT_HOST in certificate["spec"]["dnsNames"]
assert AGENT_HOST in coredns_hosts
assert f"https://{AGENT_HOST}" in ensure_script
"""Retiring the legacy owner host is a separate, deliberate change.
The agent hosts are served by hermes-agent-dashboard/-terminal rather than
hermes-sites, so their Ingress rules are asserted in
``test_hermes_agent_runtime_patches.py`` instead of the table above.
"""
assert host in certificate["spec"]["dnsNames"]
assert host in coredns_hosts
assert f"https://{host}" in ensure_script
def test_ensure_script_registers_legacy_and_renamed_origins_together(
ensure_script: str,
):
"""The renamed proxies must carry both origins; the agent proxy only one."""
"""Every renamed proxy must accept its old and new origin at the same time."""
for client, canonical, legacy in (
("hermes-chat-proxy", "chat.bstein.dev", "chat.hermes.bstein.dev"),
("hermes-triage-proxy", "triage.bstein.dev", "triage.hermes.bstein.dev"),
("hermes-agent-proxy", "worker.bstein.dev", "agent.hermes.bstein.dev"),
):
call = re.search(
rf'ensure_proxy_client "{client}".*?(?=\nensure_)',