fix(auth): resolve agent runtime consoles through workspace membership - #2
Merged
MichaelAtram merged 1 commit intoAug 17, 2026
Merged
Conversation
The Hermes dashboard embed, the Hermes Skills routes, and the whole OpenClaw gateway surface authorized browser sessions with `WHERE id = $1 AND user_id = $2` — direct ownership only. Their sibling routes (/agents/:id/hermes-ui and its chat/cron/channel endpoints) resolve access through workspace membership, so a member who could see and manage a shared agent got a 404 from exactly these paths. The "Official Dashboard" tab was the visible symptom: the metadata call reported the dashboard ready, the iframe mounted, and the embed proxy answered "agent not found or not running" forever. Add buildAccessibleAgentQuery to middleware/ownership: one statement resolving owner-or-sharing-member and exposing the caller's highest role as effective_role. The embed and gateway lookups need it because their SSRF allowlist authorizes against a pinned column projection and so cannot route through findAccessibleAgent's SELECT *. user_id stays the owner in that projection — the remote-host grant check depends on it. Role thresholds follow the capability, not the transport: - Hermes dashboard embed: reads take viewer, mutations take editor, matching the native panels. The proxy relays DELETE/PATCH/POST/PUT into the dashboard's own API, so a viewer reaching it for writes now gets 403 instead of silently succeeding. - Hermes Skills: viewer to list, editor to install/delete. - OpenClaw gateway (embed, assets, bootstrap.js, REST router, WS relay): editor throughout, with no per-method split. bootstrap.js inlines the decrypted gateway password and rebinds the UI socket onto the relay, so even a GET grants live control. Viewers lose nothing here; the surface was owner-only before, so editor only widens access. Roles are re-resolved per request and per WS connection — the embed cookie carries none — so a demotion applies on the next request rather than at token expiry. Five existing fixtures pinned the old owner-only SQL string or omitted user_id/effective_role from mock rows; corrected without changing the assertions they guard (remote-host grant revocation, credential non-exposure). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Give a user access to a workspace and they can see and manage its agents — but opening Hermes WebUI → Official Dashboard never loads. The agent's owner (or a super admin who happens to own the row) sees it fine.
The tab has two authorization models layered on one feature.
GET /api/agents/:id/hermes-uiand the chat/cron/channel routes resolve access through workspace membership, so the metadata call returnsdashboard.ready: trueand the iframe mounts. The embed behind it looked the agent up withWHERE id = $1 AND user_id = $2and answered404 agent not found or not running— forever. There is no admin bypass on that path; a super admin only works there when they own the row.Two more surfaces had the same owner-only lookup:
routes/hermesSkills.ts) — the sibling sub-tab in the same UI, broken the same way.bootstrap.js, the REST router, and the/ws/gateway/:agentIdrelay. A shared OpenClaw agent's console was unreachable for every workspace member.The fix
buildAccessibleAgentQueryinmiddleware/ownership.tsresolves owner-or-sharing-member in one statement and exposes the caller's highest role aseffective_role. The embed and gateway lookups need their own query rather thanfindAccessibleAgentbecause their SSRF allowlist authorizes against a pinned column projection, notSELECT *.user_idstays the owner in that projection — the remote-host grant check depends on it and is unchanged.Thresholds follow the capability rather than the transport:
viewerto read,editorforDELETE/PATCH/POST/PUTviewerto list,editorto install/deleteeditorthroughoutThe Hermes embed relays writes straight into the dashboard's own API, so a viewer reaching it for a write now gets 403 instead of silently succeeding. The OpenClaw surface gets no per-method split on purpose:
bootstrap.jsinlines the decrypted gateway password and rebinds the UI's WebSocket onto the relay, so even aGETgrants live control. Viewers lose nothing there — the surface was owner-only before, soeditoronly widens access.Roles are re-resolved on every request and every WS connection. The embed cookie carries no role, so a demotion takes effect on the next request rather than at token expiry.
Verification
server.tsback in: 6 fail → 6 pass.user_id/effective_rolefrom mock rows. Corrected as fixtures — the assertions they guard (remote-host grant revocation, gateway-credential non-exposure) are unchanged and still pass.docs/concepts/workspaces.mdxdocuments both consoles under the workspace role table.Note for review
The OpenClaw change means a workspace
editornow receives that agent's gateway password viabootstrap.js, the same way its owner already does. That was a deliberate call — it keeps the credential inside the set of principals who can already operate the agent. Reworking the OpenClaw embed to log in server-side and hold an HttpOnly session, the way the Hermes proxy already does, would remove the exposure entirely and is the natural follow-up.🤖 Generated with Claude Code