Skip to content
This repository was archived by the owner on Jul 27, 2026. It is now read-only.

fix(static-server): expose runtime session key via window global so fresh installs reach onboarding - #1091

Merged
chuckbutkus merged 1 commit into
mainfrom
fix/published-binary-session-key
Jun 3, 2026
Merged

chuckbutkus merged 1 commit into
mainfrom
fix/published-binary-session-key

Conversation

@chuckbutkus

@chuckbutkus chuckbutkus commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Problem

#2095

A user reported that on the published npm package (v1.0.0-beta.7):

npm install -g @openhands/agent-canvas
agent-canvas

does NOT show the onboarding flow. Instead it lands on the Manage Backends modal with "No extra backends added yet." and there is no way out — even clicking "Done" doesn't dismiss it. Running locally with npm run dev works correctly.

Root cause

scripts/dev-with-automation.mjs (npm run dev) bakes the runtime session key into the Vite dev server via viteEnv.VITE_SESSION_API_KEY = config.sessionApiKey, so the bundle has the key at module init.

.github/workflows/npm-publish.yml runs npm run build without that env var, so the published bundle has no VITE_SESSION_API_KEY. bin/agent-canvas.mjs instead generates a key at launch time and passes it to scripts/static-server.mjs --session-api-key <key>, which injects a <head> script that writes to localStorage["openhands-agent-server-config"].sessionApiKey.

PR #1046 ("Simplify backend registry selection") deleted getConfiguredSessionApiKey() — the legacy-localStorage fallback inside getAgentServerSessionApiKey(). After #1046, the React app reads the session key from VITE_SESSION_API_KEY only. The published-binary path therefore takes this walk on a fresh browser:

  1. readStoredBackends() (src/api/backend-registry/storage.ts): localStorage["openhands-backends"] is null.
  2. readLegacyBackend() requires both baseUrl AND sessionApiKey to be present in openhands-agent-server-config. The static-server only writes sessionApiKey, so it returns null.
  3. makeDefaultLocalBackend(): getAgentServerSessionApiKey() → getBakedSessionApiKey() → import.meta.env.VITE_SESSION_API_KEY → empty → returns null.
  4. Registry is []. loadAgentServerInfo() throws AgentServerUnavailableError.
  5. root.tsx renders MissingAgentServerScreen → <ManageBackendsModal onClose={noop} /> → "No extra backends added yet.", with no way to dismiss it (because onClose is a no-op behind the always-rendered backdrop).

The bug is already known internally — tests/e2e/mock-llm/utils/mock-llm-helpers.ts papers over it in seedLocalStorage()'s docstring:

The backend registry must be seeded because the static build has no baked VITE_SESSION_API_KEY — makeDefaultLocalBackend() returns null and readLegacyBackend() can't infer a host from the injected config.

That hides the bug in tests but does nothing for real users on a fresh agent-canvas launch.

Fix

Mirror the existing __AGENT_CANVAS_AUTH_REQUIRED__ mechanism that isAuthRequired() already uses to receive a --auth-required flag from the static-server:

  • scripts/static-server.mjs: the --session-api-key injection now also sets window.__AGENT_CANVAS_SESSION_API_KEY__ = "<key>" in the <head> script. The window assignment is emitted before the localStorage write so the global is set even if storage access throws (private mode, etc.).
  • src/api/agent-server-config.ts: getBakedSessionApiKey() falls back to window.__AGENT_CANVAS_SESSION_API_KEY__ when VITE_SESSION_API_KEY is empty. Whitespace-only and non-string values are treated as missing.

Net result: a fresh agent-canvas launch reaches makeDefaultLocalBackend() → returns a valid backend → registry is seeded → useConfig() succeeds → OnboardingHost mounts → the user sees the onboarding modal as expected.

Test plan

Unit tests:

  • __tests__/api/agent-server-config.test.ts — six new cases for the runtime-injection fallback (env precedence, runtime fallback, form defaults, missing both, whitespace, non-string).
  • __tests__/scripts/static-server.test.ts — asserts the <head> script contains window.__AGENT_CANVAS_SESSION_API_KEY__ with the right value, that the window assignment lands before the localStorage write, and that null sessionApiKey still produces no injection.

E2E (mock-LLM):

  • New auth mode: fresh install with runtime-injected key describe block in tests/e2e/mock-llm/mock-llm-auth-modes.spec.ts exactly reproduces the user's scenario (no seedLocalStorage, fresh browser context) and asserts the user lands on onboarding-step-choose-agent, NOT on agent-server-onboarding-screen (the Manage Backends trap).
  • The existing key-rotation and public-mode tests still pass — they pre-seed openhands-backends, which syncLauncherDefaultLocalBackend() reconciles against the (now resolvable) makeDefaultLocalBackend().

Verification commands:

npm run lint       # typecheck + eslint + prettier — clean
npm test           # 2888 passed, 12 skipped, 9 todo
npm run build:app  # production build succeeds

Manual check against a real built build/:

$ node scripts/static-server.mjs --dir build --session-api-key fake-runtime-key-12345 ...
$ curl -s http://127.0.0.1:18999/ | grep -oE '<script>[^<]*'
<script>(function(){window.__AGENT_CANVAS_SESSION_API_KEY__="fake-runtime-key-12345";try{var _k='openhands-agent-server-config',_c=...

Docs

  • AGENTS.md "Published binary auth fix" rewritten to document the window-global mechanism and clarify that the bundle ships without VITE_SESSION_API_KEY baked in.
  • AGENTS.md "Key rotation resilience" updated — syncBakedSessionApiKey() was removed by Simplify backend registry selection #1046; the active reconciliation function is now syncLauncherDefaultLocalBackend() in src/api/backend-registry/storage.ts.

Notes for reviewers

  • This PR targets main. The same fix should also be cherry-picked to rel-1.0.0 for the next beta — happy to do that as a follow-up.
  • The localStorage["openhands-agent-server-config"].sessionApiKey write is retained intentionally (a) for compatibility with any code path that still reads it, and (b) because the existing key-rotation E2E test asserts the stale stored value is overwritten on boot. Removing the write entirely is a follow-up cleanup.

This PR was created by an AI agent (OpenHands) on behalf of @neubig.

@chuckbutkus can click here to continue refining the PR


🐳 Docker images for this PR

• GHCR package: https://github.com/OpenHands/agent-canvas/pkgs/container/agent-canvas

Component Value
Image ghcr.io/openhands/agent-canvas
Architectures amd64, arm64
Agent Server ghcr.io/openhands/agent-server:1.24.0-python
Automation openhands-automation==1.0.0a5
Commit f40e63f366604b7bdf4a8d0b9e13922b147f5d4a

Pull (multi-arch manifest)

# Multi-arch manifest — Docker automatically pulls the correct architecture
docker pull ghcr.io/openhands/agent-canvas:sha-f40e63f

Run

docker run -it --rm \
  -p 8000:8000 \
  ghcr.io/openhands/agent-canvas:sha-f40e63f

All tags pushed for this build

ghcr.io/openhands/agent-canvas:sha-f40e63f-amd64
ghcr.io/openhands/agent-canvas:fix-published-binary-session-key-amd64
ghcr.io/openhands/agent-canvas:pr-1091-amd64
ghcr.io/openhands/agent-canvas:sha-f40e63f-arm64
ghcr.io/openhands/agent-canvas:fix-published-binary-session-key-arm64
ghcr.io/openhands/agent-canvas:pr-1091-arm64
ghcr.io/openhands/agent-canvas:sha-f40e63f
ghcr.io/openhands/agent-canvas:fix-published-binary-session-key
ghcr.io/openhands/agent-canvas:pr-1091

About Multi-Architecture Support

  • Each tag (e.g., sha-f40e63f) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., sha-f40e63f-amd64) are also available if needed

The published `agent-canvas` binary is built with no `VITE_SESSION_API_KEY`
baked in. At runtime, `scripts/static-server.mjs --session-api-key <key>`
injects the key into `index.html`, but only as a write to
`localStorage["openhands-agent-server-config"].sessionApiKey`.

PR #1046 ("Simplify backend registry selection") removed the legacy
localStorage fallback from `getAgentServerSessionApiKey()`, leaving it
reading only `import.meta.env.VITE_SESSION_API_KEY`. The combination
broke the first-launch experience for users who installed the npm
package globally:

  1. Fresh browser → `localStorage["openhands-backends"]` is null.
  2. `readLegacyBackend()` requires `baseUrl` AND `sessionApiKey` in
     `openhands-agent-server-config`; the injection only writes the key,
     so it returns null.
  3. `makeDefaultLocalBackend()` reads `VITE_SESSION_API_KEY` → null →
     returns null.
  4. Registry seeds empty → `MissingAgentServerScreen` renders the
     Manage Backends modal ("No extra backends added yet.") with no
     way out.

The fix mirrors how `__AGENT_CANVAS_AUTH_REQUIRED__` already passes the
public-mode flag from `static-server.mjs` to the bundle:

  - `static-server.mjs` now also injects
    `window.__AGENT_CANVAS_SESSION_API_KEY__ = <key>` in the `<head>`
    script. The window global is set first so it is available even if
    the localStorage write throws (private mode, etc.).
  - `getBakedSessionApiKey()` in `src/api/agent-server-config.ts` falls
    back to the window global when `VITE_SESSION_API_KEY` is empty.

Tests:
  - Unit tests in `__tests__/api/agent-server-config.test.ts` cover the
    window-global fallback, env-takes-precedence ordering, whitespace,
    and non-string injected values.
  - `__tests__/scripts/static-server.test.ts` asserts the new window
    assignment lands before the localStorage write.
  - A new mock-LLM E2E spec in
    `tests/e2e/mock-llm/mock-llm-auth-modes.spec.ts` exercises the
    exact user scenario: a fresh browser context with no pre-seeded
    localStorage must reach the onboarding modal (not the Manage
    Backends trap modal) when launched against the prebuilt stack.
  - Updated docstrings in the auth-modes spec and helper to remove
    references to the deleted `syncBakedSessionApiKey()` function.

AGENTS.md updated to document the window-global path and the current
key-rotation reconciliation (`syncLauncherDefaultLocalBackend()` in
`storage.ts`).

Co-authored-by: openhands <openhands@all-hands.dev>
@vercel

vercel Bot commented Jun 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-canvas Ready Ready Preview, Comment Jun 3, 2026 5:36pm

Request Review

@chuckbutkus
chuckbutkus marked this pull request as ready for review June 3, 2026 17:37
@chuckbutkus
chuckbutkus requested a review from all-hands-bot June 3, 2026 17:38

all-hands-bot commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Review Summary

This is a well-structured bug fix that addresses a legitimate user-facing issue where fresh installs of the published agent-canvas npm package would get stuck on the "Manage Backends" modal instead of the expected onboarding flow.

What Changed

File Change
scripts/static-server.mjs Injects window.__AGENT_CANVAS_SESSION_API_KEY__ global in <head> script
src/api/agent-server-config.ts Falls back to window global when VITE_SESSION_API_KEY is empty
Unit & E2E tests Comprehensive coverage of the new fallback mechanism
AGENTS.md Updated documentation

Strengths

  1. Follows existing patterns — The __AGENT_CANVAS_SESSION_API_KEY__ mechanism mirrors the existing __AGENT_CANVAS_AUTH_REQUIRED__ pattern, making the codebase consistent.

  2. Defensive coding — The window global is set before the localStorage write, ensuring it's available even if localStorage access throws (e.g., private browsing mode).

  3. Proper precedence — The env-baked VITE_SESSION_API_KEY still takes priority over the runtime-injected window global, so existing dev workflows remain unaffected.

  4. Excellent test coverage — Unit tests for all edge cases (env precedence, whitespace, non-string values), static-server integration tests verifying injection order, and a fresh-install E2E test that exactly reproduces the user's scenario.

  5. Clean PR description — The root cause analysis and fix explanation are thorough and well-documented.

Risk Assessment

Low risk — This is an additive change that introduces a fallback mechanism without modifying existing behavior. The localStorage write is retained for backwards compatibility with any code paths that still read it.

Minor Observations

  • The test cleanup in afterEach properly deletes the window global to prevent test pollution — good hygiene.
  • The type assertion as unknown as Record<string, unknown> is a reasonable TypeScript approach for accessing dynamic window properties.

LGTM! 🚀


This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

@chuckbutkus
chuckbutkus merged commit efe399f into main Jun 3, 2026
20 checks passed
@chuckbutkus
chuckbutkus deleted the fix/published-binary-session-key branch June 3, 2026 17:46
@openhands-release-bot openhands-release-bot Bot added the released: v1.2.0 Shipped in v1.2.0 label Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

📸 Snapshot Test Report

Warning

Snapshot comparison step crashed (timeout, OOM, or runner error) — diff results below may be incomplete or absent.
Check the CI logs for the full error output (look for the "Run snapshot comparison" step).

✅ No baseline found on main — all 73 snapshots are new and will become the baseline once this PR merges.

Category Count
🔴 Changed 0
🆕 New 73
✅ Unchanged 0
Total 73
🆕 New snapshots (73)

These snapshots have no baseline on main and will become the new baseline once this PR merges.

archived-conversation — 3 snapshots

conversation-panel-with-archived-badges

new snapshot

conversation-view-archived

new snapshot

conversation-view-sandbox-error

new snapshot

automations — 4 snapshots

automations-delete-modal

new snapshot

automations-list-active-inactive

new snapshot

automations-no-automations

new snapshot

automations-search-no-results

new snapshot

backends-extended — 19 snapshots

backend-add-blank-disabled

new snapshot

backend-add-cloud-advanced-open

new snapshot

backend-add-cloud-no-key-disabled

new snapshot

backend-add-cloud-with-key-enabled

new snapshot

backend-add-form-partially-filled

new snapshot

backend-add-invalid-url-disabled

new snapshot

backend-add-local-ready

new snapshot

backend-add-name-only-disabled

new snapshot

backend-add-two-column-layout

new snapshot

backend-add-whitespace-host-disabled

new snapshot

backend-after-switch

new snapshot

backend-cancel-nothing-saved

new snapshot

backend-dropdown-two-backends

new snapshot

backend-edit-prefilled

new snapshot

backend-manage-after-removal

new snapshot

backend-manage-two-listed

new snapshot

backend-remove-cancelled

new snapshot

backend-remove-confirmation

new snapshot

backend-switch-overlay

new snapshot

backends — 3 snapshots

backend-add-modal

new snapshot

backend-manage-modal

new snapshot

backend-selector-open

new snapshot

changes-tab — 3 snapshots

changes-deleted-file

new snapshot

changes-diff-viewer

new snapshot

changes-empty

new snapshot

collapsible-thinking — 4 snapshots

reasoning-content-collapsed

new snapshot

reasoning-content-expanded

new snapshot

think-action-collapsed

new snapshot

think-action-expanded

new snapshot

mcp-page — 11 snapshots

mcp-custom-server-1-editor-open

new snapshot

mcp-custom-server-2-url-filled

new snapshot

mcp-custom-server-3-all-filled

new snapshot

mcp-custom-server-4-installed

new snapshot

mcp-custom-server-editor

new snapshot

mcp-empty-installed

new snapshot

mcp-search-filtered

new snapshot

mcp-slack-install-1-marketplace

new snapshot

mcp-slack-install-2-modal

new snapshot

mcp-slack-install-3-filled

new snapshot

mcp-slack-install-4-installed

new snapshot

onboarding — 4 snapshots

onboarding-step-0-choose-agent

new snapshot

onboarding-step-1-check-backend

new snapshot

onboarding-step-2-setup-llm

new snapshot

onboarding-step-3-say-hello

new snapshot

projects-workspace-browser

projects-workspace-browser

new snapshot

settings-page — 5 snapshots

add-backend-modal

new snapshot

analytics-consent-modal

new snapshot

home-screen

new snapshot

settings-app-page

new snapshot

settings-page

new snapshot

settings-secrets — 5 snapshots

secrets-add-form-filled

new snapshot

secrets-add-form

new snapshot

secrets-after-save

new snapshot

secrets-delete-confirm

new snapshot

secrets-list

new snapshot

settings-verification — 3 snapshots

condenser-settings

new snapshot

verification-settings-off

new snapshot

verification-settings-on

new snapshot

sidebar — 3 snapshots

sidebar-collapsed

new snapshot

sidebar-conversation-panel

new snapshot

sidebar-filter-menu

new snapshot

skills-page — 5 snapshots

skills-empty

new snapshot

skills-loaded

new snapshot

skills-no-match

new snapshot

skills-search-filtered

new snapshot

skills-type-filter

new snapshot


Generated by the Snapshot Tests workflow. This comment was created by an AI agent (OpenHands) on behalf of the repo maintainers.

This branch was successfully deployed

1 active deployment
Preview — f40e63f3 Deployed Jun 3, 2026 by vercel[bot]
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

released: v1.2.0 Shipped in v1.2.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants