hero_assistance _admin tab calls via /admin/rpc hit _server instead of _admin #109

Closed
opened 2026-05-23 03:33:05 +00:00 by mik-tf · 3 comments
Owner

When the hero_assistance admin dashboard is loaded through hero_router at /admin/, its embedded operator-side RPCs (the ui.* namespace, served locally by hero_assistance_admin) return Method not found. The same calls succeed when the dashboard is loaded by connecting directly to hero_assistance_admin's admin.sock. Looking at the request path, /admin/rpc through the router is being forwarded to hero_assistance_server's rpc.sock rather than hero_assistance_admin's admin.sock, which has no implementation of ui.* methods. The expected behavior is that //admin/rpc routes to that service's admin.sock so the local ui.* dispatch keeps working through the router. Reproduce by running lab service hero_assistance --start, opening hero_router, and clicking the Admin tab on the hero_assistance dashboard.

When the hero_assistance admin dashboard is loaded through hero_router at /admin/, its embedded operator-side RPCs (the ui.* namespace, served locally by hero_assistance_admin) return Method not found. The same calls succeed when the dashboard is loaded by connecting directly to hero_assistance_admin's admin.sock. Looking at the request path, /admin/rpc through the router is being forwarded to hero_assistance_server's rpc.sock rather than hero_assistance_admin's admin.sock, which has no implementation of ui.* methods. The expected behavior is that /<service>/admin/rpc routes to that service's admin.sock so the local ui.* dispatch keeps working through the router. Reproduce by running lab service hero_assistance --start, opening hero_router, and clicking the Admin tab on the hero_assistance dashboard.
Author
Owner

Reproduced today by clicking the Admin tab in hero_assistance_admin, which fires ui.getAccessList and renders Failed to load access list: Method not found: ui.getAccessList. The same fall-through is implied for the other ADMIN_SECRETS RPCs that share the ui.* prefix. The byte-passthrough /rpc proxy on _admin (in crates/hero_assistance_admin/src/main.rs, the proxy that backs both <hero-api-docs spec-url="/hero_assistance/admin/openrpc.json" rpc-url="/hero_assistance/admin/rpc"> and <hero-logs-viewer rpc-url="/hero_assistance/admin/rpc">) forwards every request straight to _server, including ui.* methods that _server has no handler for. The fix is to add a prefix check in that proxy so request bodies whose method field begins with ui. dispatch locally on _admin (against the five ADMIN_SECRETS handlers) before falling through to the _server passthrough.

Despite the title's phrasing, the load-bearing change is in lhumina_code/hero_assistance's _admin crate, not in hero_router: the router does deliver /hero_assistance/admin/rpc to _admin's admin.sock correctly (the same socket is also reachable at /hero_assistance/ui/). Recommend re-filing this issue (or moving its body) to lhumina_code/hero_assistance and closing here as not-a-router-bug.

Reproduced today by clicking the Admin tab in `hero_assistance_admin`, which fires `ui.getAccessList` and renders `Failed to load access list: Method not found: ui.getAccessList`. The same fall-through is implied for the other ADMIN_SECRETS RPCs that share the `ui.*` prefix. The byte-passthrough `/rpc` proxy on `_admin` (in `crates/hero_assistance_admin/src/main.rs`, the proxy that backs both `<hero-api-docs spec-url="/hero_assistance/admin/openrpc.json" rpc-url="/hero_assistance/admin/rpc">` and `<hero-logs-viewer rpc-url="/hero_assistance/admin/rpc">`) forwards every request straight to `_server`, including `ui.*` methods that `_server` has no handler for. The fix is to add a prefix check in that proxy so request bodies whose `method` field begins with `ui.` dispatch locally on `_admin` (against the five ADMIN_SECRETS handlers) before falling through to the `_server` passthrough. Despite the title's phrasing, the load-bearing change is in `lhumina_code/hero_assistance`'s `_admin` crate, not in `hero_router`: the router does deliver `/hero_assistance/admin/rpc` to `_admin`'s `admin.sock` correctly (the same socket is also reachable at `/hero_assistance/ui/`). Recommend re-filing this issue (or moving its body) to `lhumina_code/hero_assistance` and closing here as not-a-router-bug.
Author
Owner

Correcting my earlier comment above. Re-investigating today against the current lhumina_code/hero_assistance development HEAD (49ea76a7), the local ui.* interception on _admin's /rpc proxy actually IS in place. It sits in crates/hero_assistance_admin/src/routes.rs lines 198-257 (the rpc_proxy function parses the JSON-RPC envelope, matches method.starts_with("ui."), and dispatches via ui_rpc::try_dispatch at ui_rpc.rs line 52 before the byte-passthrough forward to _server). Direct curl confirms this works:

curl --unix-socket ~/hero/var/sockets/hero_assistance/admin.sock \
  -X POST -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"ui.getAccessList","params":{}}' \
  http://localhost/rpc
# -> {"id":1,"jsonrpc":"2.0","result":{"enabled":true,"entries":["192.0.2.10","127.0.0.1"],"hero_proc_available":true,"secret_key":"ADMIN_SECRETS"}}

The router-fronted path however explicitly targets the wrong socket. hero_router's own 502 response body names the destination it attempted:

curl http://127.0.0.1:9988/hero_assistance/admin/rpc -X POST \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"ui.getAccessList","params":{}}'
# -> HTTP 502
# -> Could not connect to socket '/home/pctwo/hero/var/sockets/hero_assistance/rpc.sock': client error (Connect)

Note the named destination: rpc.sock, not admin.sock. The /admin/rpc request never reaches _admin's interception logic because hero_router routes it to _server's rpc.sock by some component-resolution rule that treats admin differently from sibling components. The app component routes correctly under the same shape, which makes the contrast clean:

URL hero_router targets Correct
/hero_assistance/app/rpc app.sock yes
/hero_assistance/app/health app.sock yes
/hero_assistance/admin/rpc rpc.sock no, should be admin.sock
/hero_assistance/admin/health (looks up ui.sock) no, should be admin.sock
/hero_assistance/admin/static/* (looks up ui.sock) no, should be admin.sock
/hero_assistance/rpc/rpc rpc.sock yes

So /admin/* routing is half pre-admin.sock legacy (looks up the old ui.sock for non-/rpc subpaths, see hero_router error body Socket 'ui.sock (admin)' not found for 'hero_assistance') and half hardcoded to the service-level rpc.sock for /rpc specifically. Both branches of the admin component resolution need attention. The fix is in hero_router; my earlier recommendation to re-file under hero_assistance was wrong on both of its load-bearing premises and should be retracted. The original issue body framing is correct as written.

Reproduction details: hero_assistance --start brings the three daemons up; the customer UI at /hero_assistance/app/ works correctly; the operator admin at /hero_assistance/admin/ fails. Note that the _admin and _ui daemons also restart-loop on a separate health-check bug filed at lhumina_code/hero_assistance#21, which is independent of this routing issue but means reproduction needs to land curl probes within the brief alive window after each restart.

Correcting my earlier comment above. Re-investigating today against the current `lhumina_code/hero_assistance` `development` HEAD (49ea76a7), the local `ui.*` interception on `_admin`'s `/rpc` proxy actually IS in place. It sits in `crates/hero_assistance_admin/src/routes.rs` lines 198-257 (the `rpc_proxy` function parses the JSON-RPC envelope, matches `method.starts_with("ui.")`, and dispatches via `ui_rpc::try_dispatch` at `ui_rpc.rs` line 52 before the byte-passthrough forward to `_server`). Direct curl confirms this works: ``` curl --unix-socket ~/hero/var/sockets/hero_assistance/admin.sock \ -X POST -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"ui.getAccessList","params":{}}' \ http://localhost/rpc # -> {"id":1,"jsonrpc":"2.0","result":{"enabled":true,"entries":["192.0.2.10","127.0.0.1"],"hero_proc_available":true,"secret_key":"ADMIN_SECRETS"}} ``` The router-fronted path however explicitly targets the wrong socket. hero_router's own 502 response body names the destination it attempted: ``` curl http://127.0.0.1:9988/hero_assistance/admin/rpc -X POST \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":2,"method":"ui.getAccessList","params":{}}' # -> HTTP 502 # -> Could not connect to socket '/home/pctwo/hero/var/sockets/hero_assistance/rpc.sock': client error (Connect) ``` Note the named destination: `rpc.sock`, not `admin.sock`. The `/admin/rpc` request never reaches `_admin`'s interception logic because hero_router routes it to `_server`'s rpc.sock by some component-resolution rule that treats `admin` differently from sibling components. The `app` component routes correctly under the same shape, which makes the contrast clean: | URL | hero_router targets | Correct | |---|---|---| | `/hero_assistance/app/rpc` | `app.sock` | yes | | `/hero_assistance/app/health` | `app.sock` | yes | | `/hero_assistance/admin/rpc` | `rpc.sock` | no, should be `admin.sock` | | `/hero_assistance/admin/health` | (looks up `ui.sock`) | no, should be `admin.sock` | | `/hero_assistance/admin/static/*` | (looks up `ui.sock`) | no, should be `admin.sock` | | `/hero_assistance/rpc/rpc` | `rpc.sock` | yes | So `/admin/*` routing is half pre-`admin.sock` legacy (looks up the old `ui.sock` for non-`/rpc` subpaths, see hero_router error body `Socket 'ui.sock (admin)' not found for 'hero_assistance'`) and half hardcoded to the service-level `rpc.sock` for `/rpc` specifically. Both branches of the `admin` component resolution need attention. The fix is in hero_router; my earlier recommendation to re-file under hero_assistance was wrong on both of its load-bearing premises and should be retracted. The original issue body framing is correct as written. Reproduction details: `hero_assistance --start` brings the three daemons up; the customer UI at `/hero_assistance/app/` works correctly; the operator admin at `/hero_assistance/admin/` fails. Note that the `_admin` and `_ui` daemons also restart-loop on a separate health-check bug filed at https://forge.ourworld.tf/lhumina_code/hero_assistance/issues/21, which is independent of this routing issue but means reproduction needs to land curl probes within the brief alive window after each restart.
Author
Owner

Closed by 0682283 on development (squash via local merge + direct push; remote development_mik is held by a stale branch from another author so no PR was opened, matching the workaround used at the prior hero_router fix).

The fix is in crates/hero_router/src/server/routes.rs: service_proxy_inner no longer rewrites POST /<service>/admin/rpc to forward to rpc.sock. The sibling-shortcut now applies only to the legacy ui webname. /admin/rpc falls through to the existing "admin" match arm which dispatches to admin.sock directly via resolve_ui_socket. The admin-local proxy on hero_admin_lib (the byte-passthrough /rpc proxy on admin.sock) then intercepts ui.* methods locally and forwards everything else to _server's rpc.sock via its own byte-passthrough.

The shortcut decision is extracted into a pure sibling_shortcut_rewrite helper with four unit tests pinning: ui shortcut still works, admin no longer takes it, sibling webnames never took it, ui with non-rpc paths never took it. The context-0 restriction on admin is preserved unchanged (now lives solely on the "admin" match arm).

Live verify against lhumina_code/hero_assistance post-install:

curl http://127.0.0.1:9988/hero_assistance/admin/rpc -X POST \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"ui.getAccessList","params":{}}'
# -> HTTP 200
# -> {"id":1,"jsonrpc":"2.0","result":{"enabled":true,"entries":["192.0.2.10","127.0.0.1"],"hero_proc_available":true,"secret_key":"ADMIN_SECRETS"}}

Byte-identical to what the same call returns when sent directly to admin.sock. The control test against /hero_assistance/rpc/rpc system.ping still returns {"pong":true} HTTP 200 (no regression on the regular RPC path).

As a side effect this also fixes /<service>/admin/health, /<service>/admin/static/*, and /<service>/admin/openrpc.json which previously returned Socket 'ui.sock (admin)' not found because the legacy ui.sock lookup is no longer the canonical admin path.

Closed by 0682283 on `development` (squash via local merge + direct push; remote `development_mik` is held by a stale branch from another author so no PR was opened, matching the workaround used at the prior hero_router fix). The fix is in `crates/hero_router/src/server/routes.rs`: `service_proxy_inner` no longer rewrites `POST /<service>/admin/rpc` to forward to `rpc.sock`. The sibling-shortcut now applies only to the legacy `ui` webname. `/admin/rpc` falls through to the existing `"admin"` match arm which dispatches to `admin.sock` directly via `resolve_ui_socket`. The admin-local proxy on `hero_admin_lib` (the byte-passthrough `/rpc` proxy on `admin.sock`) then intercepts `ui.*` methods locally and forwards everything else to `_server`'s `rpc.sock` via its own byte-passthrough. The shortcut decision is extracted into a pure `sibling_shortcut_rewrite` helper with four unit tests pinning: `ui` shortcut still works, `admin` no longer takes it, sibling webnames never took it, `ui` with non-rpc paths never took it. The context-0 restriction on `admin` is preserved unchanged (now lives solely on the `"admin"` match arm). Live verify against `lhumina_code/hero_assistance` post-install: ``` curl http://127.0.0.1:9988/hero_assistance/admin/rpc -X POST \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"ui.getAccessList","params":{}}' # -> HTTP 200 # -> {"id":1,"jsonrpc":"2.0","result":{"enabled":true,"entries":["192.0.2.10","127.0.0.1"],"hero_proc_available":true,"secret_key":"ADMIN_SECRETS"}} ``` Byte-identical to what the same call returns when sent directly to `admin.sock`. The control test against `/hero_assistance/rpc/rpc system.ping` still returns `{"pong":true}` HTTP 200 (no regression on the regular RPC path). As a side effect this also fixes `/<service>/admin/health`, `/<service>/admin/static/*`, and `/<service>/admin/openrpc.json` which previously returned `Socket 'ui.sock (admin)' not found` because the legacy `ui.sock` lookup is no longer the canonical admin path.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_router#109
No description provided.