Skip to content

fix(dashboard): sanction plugin WS/upload auth via SDK helpers (gated mode) - #38533

Closed
benbarclay wants to merge 1 commit into
mainfrom
plugin-sdk-auth
Closed

fix(dashboard): sanction plugin WS/upload auth via SDK helpers (gated mode)#38533
benbarclay wants to merge 1 commit into
mainfrom
plugin-sdk-auth

Conversation

@benbarclay

Copy link
Copy Markdown
Collaborator

Problem

Dashboard plugins (kanban, hermes-achievements) read window.__HERMES_SESSION_TOKEN__ directly and hand-assembled WebSocket URLs with ?token=. That works in loopback / --insecure mode, but is rejected on OAuth-gated deployments (e.g. hosted Fly agents), where the session token is absent by design and _ws_auth_ok only accepts single-use ?ticket= auth.

Symptoms on a gated agent:

  • Plugin REST calls (kanban attachment upload/download) → 401
  • Kanban live-events WebSocket (/api/plugins/kanban/events) → 1008 / 403 on upgrade

The dashboard SPA already handled gated mode correctly; only the SDK-bypassing plugin call sites broke.

Impact case

Anyone running the dashboard behind OAuth (the hosted/Fly path, and any self-hoster who enables auth_required) gets a broken kanban board and broken achievements panel — the kanban WS never connects so the board never live-updates, and attachment upload/download 401s. It's invisible in the default loopback/--insecure setup, which is why it slipped through. Not a sharp edge on an exotic config — it's every gated deployment.

Fix — make the plugin SDK the single sanctioned auth surface

Rather than patch each plugin with its own inline token logic (which would let this regress again), extend the existing window.__HERMES_PLUGIN_SDK__ seam:

  • web/src/lib/api.ts — add authedFetch() (raw Response for FormData uploads / blob downloads; token-or-cookie auth; does not throw / does not 401-redirect) and buildWsUrl() (assembles a ws(s):// URL with the correct auth param for the active mode — fresh single-use ticket in gated mode, token in loopback).
  • web/src/plugins/registry.ts — expose authedFetch, buildWsUrl, buildWsAuthParam, sdkVersion on the SDK; add SDK_CONTRACT_VERSION.
  • web/src/plugins/sdk.d.ts (new) — hand-authored typed contract for the plugin SDK + registry Window globals (single source of truth; documented as a versioned API boundary).
  • kanban + hermes-achievements dist bundles — stop reading the session token directly; route uploads/downloads through SDK.authedFetch, the live-events WS through SDK.buildWsUrl.
  • plugins/kanban/dashboard/plugin_api.py_ws_upgrade_authorized() delegates the /events WS upgrade to the canonical web_server._ws_auth_ok gate, so it transparently accepts loopback-token / gated-ticket / internal-credential and can never drift from core auth again.
  • tests — guard test asserting no plugin dist/index.js reads __HERMES_SESSION_TOKEN__ directly; kanban gated-ticket WS acceptance test.

Verification

  • Python: tests/plugins/test_plugin_dashboard_auth_contract.py, tests/plugins/test_kanban_dashboard_plugin.py, tests/hermes_cli/test_dashboard_auth_ws_auth.py — all green (143 passed).
  • TS: npm run build (tsc -b + vite) clean; eslint clean on changed files. (Note: tsc -b is stricter than tsc --noEmit and caught 4 type-contract bugs in the spike .d.ts — fixed.)
  • Live on a gated staging Fly agent: instrumented the WS gate and reproduced. With the new SPA+plugin, the kanban /events upgrade carries a real minted ticket (ticket_len=43) → _ws_auth_ok consumes it → ws_auth_ok=True101 Switching Protocols, where the old ?token= code got 403.

Review

plugins/kanban/dashboard/plugin_api.py delegates to the core dashboard WS-auth gate (hermes_cli.web_server._ws_auth_ok) — flagging @teknium1 for the auth-adjacent backend change. The web/SPA + plugin-frontend changes are tightly coupled to the backend (the frontend mints the ticket the backend must accept), so they ship together as one coherent PR rather than split.

The sdk.d.ts is a spike — it documents the contract and gives plugin authors editor types. Open questions for productionising it (publish as a types package? runtime sdk_version compat gate?) are noted in the file header and don't block this auth fix.

… mode)

Dashboard plugins (kanban, hermes-achievements) read
window.__HERMES_SESSION_TOKEN__ directly and hand-assembled WebSocket
URLs with ?token=. That works in loopback/--insecure mode but is
rejected on OAuth-gated deployments, where the session token is absent
and _ws_auth_ok only accepts single-use ?ticket= auth. The result was
401s on plugin REST calls and 1008/403 on the kanban live-events WS
whenever the dashboard ran behind OAuth (e.g. hosted Fly agents).

Make the plugin SDK the single sanctioned auth surface:

- web/src/lib/api.ts: add authedFetch() (raw Response for FormData
  uploads / blob downloads, token-or-cookie auth, no throw / no 401
  redirect) and buildWsUrl() (assembles a ws(s):// URL with the correct
  auth param for the active mode — fresh single-use ticket in gated
  mode, token in loopback).
- web/src/plugins/registry.ts: expose authedFetch, buildWsUrl,
  buildWsAuthParam, and sdkVersion on window.__HERMES_PLUGIN_SDK__;
  add SDK_CONTRACT_VERSION.
- web/src/plugins/sdk.d.ts: hand-authored typed contract for the
  plugin SDK + registry globals (single source of truth for the
  Window declarations).
- plugins/kanban + hermes-achievements dist bundles: stop reading the
  session token directly; route uploads/downloads through
  SDK.authedFetch and the live-events WS through SDK.buildWsUrl.
- plugins/kanban plugin_api.py: _ws_upgrade_authorized() delegates the
  /events WS upgrade to the canonical web_server._ws_auth_ok gate, so
  it transparently accepts loopback token / gated ticket / internal
  credential and can never drift from core auth again.
- tests: guard test asserting no plugin dist reads
  __HERMES_SESSION_TOKEN__ directly; kanban gated-ticket WS test.

Verified live on a gated staging Fly agent: kanban /events upgrades
101 with a minted ticket (ticket_len=43, ws_auth_ok=True) where the
old code got 403.
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: plugin-sdk-auth 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: 9746 on HEAD, 9744 on base (🆕 +2)

🆕 New issues (1):

Rule Count
unresolved-import 1
First entries
tests/plugins/test_plugin_dashboard_auth_contract.py:28: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`

✅ Fixed issues: none

Unchanged: 5047 pre-existing issues carried over.

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

@alt-glitch alt-glitch added type/bug Something isn't working comp/plugins Plugin system and bundled plugins area/auth Authentication, OAuth, credential pools P3 Low — cosmetic, nice to have labels Jun 3, 2026
@teknium1

teknium1 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Merged via PR #38549 (commit a6e4731). Your commit was cherry-picked onto current main with your authorship preserved in git log. Thanks for the gated-mode auth fix and the live verification on the staging Fly agent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants