Skip to content

webui: Relay tab with copy-paste worker connect snippets#324

Open
FMXExpress wants to merge 1 commit into
mainfrom
claude/webui-relay-connect-snippet
Open

webui: Relay tab with copy-paste worker connect snippets#324
FMXExpress wants to merge 1 commit into
mainfrom
claude/webui-relay-connect-snippet

Conversation

@FMXExpress

Copy link
Copy Markdown
Owner

Summary

  • New Relay tab in the gateway web UI (between Stats and Settings) for onboarding clients to a running gateway.
  • Auto-detected URL + masked token + five copy-paste worker snippets: pasclaw relay (flag form), env-var form, browser/WebLLM, Python+llama.cpp, Replicate cog-relay.
  • Token defaults to a <PASCLAW_GATEWAY_TOKEN> placeholder so the panel is safe to screen-share; per-tab "show" toggle swaps in the real localStorage value when an operator wants to copy the snippet somewhere private.
  • Live /v1/relay/status mini-dashboard: 5 stat cards (workers / pending / in-flight / completed / failed) + a connected-workers table with caps + last-seen. 5 s refresh. 503/non-200 surfaces "relay disabled on this gateway" rather than blanking the panel.

Test plan

  • make all clean
  • Markup checks: data-tab="relay", id="tab-relay", snippet templates, setupRelayTab / refreshRelay JS all served from /
  • Inline <script> parses cleanly (no syntax errors)
  • jsdom run: tab activates without runtime errors, URL auto-fills from window.location.origin, token rendered masked, "show" toggle reveals real token, snippets re-render with substitution, 5 code blocks present, copy buttons wired
  • Stats fall back to "—" + "relay disabled (HTTP 401)" tbody row when status endpoint returns non-2xx — graceful path verified
  • /v1/relay/status returns expected shape (gateway with default_provider=relay configured)

🤖 Generated with Claude Code

https://claude.ai/code/session_01TBcLtmpj7dqA5tyFbGnQon


Generated by Claude Code

Adds a "Relay" tab between Stats and Settings that surfaces:

- Auto-detected gateway URL (defaults to window.location.origin, editable
  if workers reach the gateway via a tunnel / proxy / external hostname).
- Bearer token from localStorage, MASKED by default so the panel is safe
  to screen-share. A per-tab "show" toggle swaps the placeholder
  <PASCLAW_GATEWAY_TOKEN> for the real value when copying snippets
  somewhere private.
- Five ready-to-copy snippets via the existing codeBlock + copy-button
  helpers: pasclaw relay CLI (flag form), pasclaw relay (env-var form),
  Browser/WebLLM worker, Python+llama.cpp worker, Replicate cog-relay
  predict() call.
- Live mini-dashboard from /v1/relay/status: connected workers, queue
  depth, completed/failed counts, plus a table of currently-attached
  workers with their advertised caps + last-seen. Refreshes every 5s
  while the tab is active. 503/non-200 falls back to a clear
  "relay disabled" message rather than a blank table.

webui.res regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TBcLtmpj7dqA5tyFbGnQon

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d6df9086d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2367 to +2369
let relayTimer = null;
let relaySetupDone = false;
let relayShowToken = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move relay state before hash activation

When the page is loaded directly at /#relay, the existing boot path calls showTab(location.hash.slice(1)) before these newly added let initializers have executed. tabLoaders.relay() then calls setupRelayTab(), which reads relaySetupDone while it is still in the temporal dead zone, so the Relay tab's snippets/status fail to initialize for bookmarked or reloaded Relay URLs. Move the relay state initialization above the boot block or defer hash activation until after it.

Useful? React with 👍 / 👎.

Comment on lines +2427 to +2429
const cli = "pasclaw relay \\\n"
+ " --gateway-url " + url + " \\\n"
+ " --gateway-token " + tok;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace the nonexistent relay CLI command

This snippet tells users to run pasclaw relay, but there is no relay subcommand registered: src/cmd/PasClaw.Cmd.Root.pas dispatches the known commands at lines 184-234 and unknown commands fall through to unknown command at lines 235-238, and a repo-wide search finds no Cmd.Relay implementation. Copying either CLI snippet from the new Relay tab therefore fails immediately instead of attaching a worker.

Useful? React with 👍 / 👎.

Comment on lines +2447 to +2451
" headers: {\n" +
" \"Authorization\": `Bearer ${TOKEN}`,\n" +
" \"X-Relay-Worker-Id\": `tab-${crypto.randomUUID()}`,\n" +
" \"X-Relay-Capabilities\": MODEL,\n" +
" },\n" +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop relying on EventSource headers

In real browsers the native EventSource constructor ignores arbitrary request headers; this file already uses gwSseUrl() for logs because EventSource cannot set headers, and the server requires X-Relay-Worker-Id in HandleRelayPoll before registering a worker. As written, the browser worker snippet cannot connect: token-protected gateways receive no Authorization header, and even untokened gateways reject the missing worker-id header with HTTP 400.

Useful? React with 👍 / 👎.

FMXExpress added a commit that referenced this pull request Jun 21, 2026
webui+gateway: relay tab + CORS for /v1/relay/* (replaces #324, #325, #326)
FMXExpress pushed a commit that referenced this pull request Jun 22, 2026
Two stacked fixes addressing the PR #333 P1 code review and the
"each new serve should generate a relay-only token" follow-up.

WHY (P1 from review):
  Loading @mlc-ai/web-llm via dynamic import in the parent page was
  gated on a button click, which deferred the network fetch -- but
  did NOT isolate the third-party code from the gateway UI's origin.
  Once loaded, the module sat in the same script realm as the webui:
  it could read localStorage["pasclaw.gw_token.v1"], call any /v1/*
  endpoint with the operator's credentials, and mutate the parent
  DOM. A compromised CDN / typo-squat / supply-chain push reached
  the whole authenticated PasClaw surface.

WHAT (sandbox):
  In-browser worker now lives inside a `<iframe sandbox="allow-scripts">`
  (no allow-same-origin -> opaque null origin). The webllm import
  and EventSource + dispatch loop run there; the parent UI only
  renders status / progress / log lines relayed via postMessage.

  Parent <-> child wire protocol (postMessage):
    parent -> child : connect{url,token,model,workerId} | disconnect
    child  -> parent: ready | log | status | progress | job-served
                      | connected | disconnected | connection-failed

  Source identity filter (event.source === iframeEl.contentWindow)
  not origin string -- null-origin iframes emit "null" but sticking
  to identity is robust to spec drift.

WHAT (scoped token):
  TGatewayServer.Create generates a fresh FRelayToken on every
  startup -- 8 Crockford base32 chars in two 4-char groups
  (A8M9-PXRT, ~40 bits entropy, phone-typable, no I/L/O/U
  confusables). Dual-token auth at the OnCommandGet gate accepts
  EITHER the main Cfg.Gateway.Token OR FRelayToken for /v1/relay/*
  paths (case-insensitive, hyphens stripped before compare so
  dictated tokens work). Main-token endpoints (/v1/chat /v1/config
  /v1/skills/...) reject the scoped token -- a compromised in-tab
  worker can pull jobs but cannot impersonate the operator.

  New GET /v1/relay/worker-token returns the scoped token as JSON.
  Gated by the MAIN token via the normal auth check (the dual-token
  helper deliberately excludes this path) so an attacker who only
  has the relay token cannot escalate to read it. CORS-stamped.

  pasclaw serve / pasclaw gateway print the token on startup so
  external `pasclaw relay` CLIs can use scoped credentials too.

WHAT (webui plumbing):
  setupInBrowserWorker creates the sandboxed iframe on tab init
  (srcdoc carries the postMessage responder + dynamic-import
  scaffold; ~3.8 KB inline). inbWorkerConnect first fetches
  /v1/relay/worker-token using gwFetch (main token), then
  postMessages the SCOPED token to the iframe -- the main token
  never crosses the origin boundary. UI hint text updated to
  surface the trust boundary so operators understand what's
  isolated and what isn't.

  Three new bindings (_relayInbIframe / _relayInbReady /
  _relayInbJobs) declared above the boot block alongside the other
  relay-tab state, same TDZ pattern PR #324/#326 already established
  (deep-linked /#relay loads the tab before the relay-impl block
  runs).

VERIFIED:
  - Static checks: iframe sandbox attr = "allow-scripts" without
    allow-same-origin; no eager `import {CreateMLCEngine} from esm.run`
    in the outer page; dynamic import present only inside the
    srcdoc; UI hint mentions sandbox and relay-scoped token.
  - jsdom render: zero runtime errors, iframe in DOM, srcdoc 3.8 KB
    with `await import('https://esm.run/...')` + postMessage logic.
  - Endpoint smoke (gateway with main token MAINTOKEN):
      no-auth /v1/relay/worker-token         -> 401
      relay-token-only worker-token          -> 401
      main-token /v1/relay/worker-token      -> 200 {"token":"KP44-5XZ3"}
      scoped /v1/relay/status                -> 200
      scoped /v1/config                      -> 401  (blast-radius
                                                       contained)
      main /v1/config                        -> 200
      lowercase / no-hyphen scoped tokens    -> 200 (case-insensitive)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TBcLtmpj7dqA5tyFbGnQon
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants