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

fix(frontend): route automation requests through cloud proxy for cloud backends - #1121

Merged
hieptl merged 2 commits into
mainfrom
hieptl/app-2159
Jun 4, 2026
Merged

hieptl merged 2 commits into
mainfrom
hieptl/app-2159

Conversation

@hieptl

@hieptl hieptl commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor
  • A human has tested these changes.

Why

Summary

With a cloud backend selected (e.g. https://staging.all-hands.dev), the Automations page (/automations) always renders "Automations Unavailable". DevTools shows GET https://staging.all-hands.dev/api/automation/health blocked at CORS preflight (provisional headers, 0 response headers).

Reproduction

  1. npm run dev in agent-server-gui
  2. BackendSelector → Add Backend → Host https://staging.all-hands.dev, Type Cloud, valid API key → select Staging – Personal Workspace
  3. Navigate to http://localhost:3001/automations

Current: "Automations Unavailable" error state.
Expected: the automations list loads, and all automation actions work.

Root cause

  • Cloud automation calls were issued directly from the browser to the cloud host with Authorization: Bearer + X-Org-Id headers (non-simple headers → CORS preflight required).
  • /api/automation/* is served by the standalone automation FastAPI service (automation repo, openhands/automation/app.py), which uses a plain CORSMiddleware whose origin allowlist defaults to the cloud app's own origin only — http://localhost:3001 is rejected at preflight.
  • Every other cloud endpoint works direct-from-browser only because the main SaaS API wraps requests in ApiKeyAwareCORSMiddleware (OpenHands/enterprise/server/middleware.py), which answers bearer-token requests with Access-Control-Allow-Origin: *. The automation service has no such middleware — hence /api/settings succeeds while /api/automation/health fails.
  • AutomationService.checkHealth() swallows the network error into {status: "error"}, and the page gates on health → permanent "Automations Unavailable".

All 8 cloud branches of AutomationService are affected (list, get, update, delete, dispatch, runs, tarball, health) — health just fails first.

Fix

Tunnel cloud automation calls through the bundled agent-server's existing server-side forwarder POST /api/cloud-proxy (SSRF allowlist already permits *.all-hands.dev), the same proven mechanism used by runtime-sandbox calls. The browser then only makes a same-origin request; the upstream call happens server-side where CORS does not apply.

  • src/api/cloud/proxy.ts: opt-in forceProxy flag on CloudProxyRequest
  • src/api/automation-service/automation-service.api.ts: all 8 cloud branches funneled through a callAutomationCloudProxy helper that injects the flag; 5s fail-fast timeout on the cloud health probe (parity with local)

Acceptance criteria

  • /automations loads with a cloud backend (staging and prod) selected
  • DevTools shows a same-origin POST /api/cloud-proxy and no direct cross-origin request to <cloud-host>/api/automation/*
  • List, detail, runs, toggle, Run Now, delete, and tarball download all work against a cloud backend
  • Local backends are unaffected (automation sidecar path unchanged)
  • Unit tests cover forceProxy routing, envelope auth/org headers, and cloud checkHealth success/error mapping

Follow-up (separate ticket, automation repo)

Consider adopting the main API's bearer-aware CORS behavior (ApiKeyAwareCORSMiddleware) in the automation service so any external client works without a proxy. Requires a staging/prod deploy; not a substitute for the client-side fix against already-deployed environments.

Summary

  • cloud/proxy.ts — new opt-in forceProxy flag on CloudProxyRequest (now exported). When set, app-host calls go through the bundled agent-server's same-origin POST /api/cloud-proxy hop instead of straight to the cloud host; the upstream call is made server-side where CORS does not apply. Auth + X-Org-Id ride in the proxy envelope exactly as the runtime-sandbox calls already do, and the proxy's SSRF allowlist already permits *.all-hands.dev.
  • automation-service.api.ts — all 8 cloud branches (list, get, update, delete, dispatch, runs, tarball, health) are funneled through a callAutomationCloudProxy helper that injects forceProxy: true, so a future method can't reintroduce the direct cross-origin call. The cloud health probe also gets timeoutSeconds: 5 for parity with the local branch's fail-fast timeout.
  • Tests — proxy.test.ts: forceProxy routes via POST /api/cloud-proxy with the upstream call as an envelope (and no direct axios.request), and the envelope carries Authorization + X-Org-Id. automation-service.test.ts: cloud checkHealth tunnels through the proxy with the fail-fast timeout and maps proxy rejections to {status: "error"} instead of throwing; the six existing exact-match cloud-routing assertions gained the new flag. Red-check: with the src fix reverted, 9 of these tests fail.

Issue Number

Resolves #2385

How to Test

  1. npm run dev
  2. BackendSelector → Add Backend → Host https://staging.all-hands.dev, Type Cloud, valid API key → select Staging – Personal Workspace
  3. Open http://localhost:3001/automations
  4. Before: failed cross-origin GET .../api/automation/health → page shows "Automations Unavailable". After: a same-origin POST http://localhost:3001/api/cloud-proxy returns 200 {status:"ok"} and the automations list renders.
  5. Spot-check the other paths through the proxy: open a detail page, toggle enabled, Run Now, delete, download tarball — each appears in DevTools as POST /api/cloud-proxy, with no direct staging.all-hands.dev automation request.
  6. Switch to a local backend and confirm automations still work (local sidecar path is unchanged).

Video/Screenshots

app-2159.mov

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

  • Follows the established cloud/local branching pattern; the proxy hop is the same mechanism runtime-sandbox calls already use in production (including blob downloads), so the blast radius is small.
  • Cloud hosts outside *.all-hands.dev would get a 403 from the proxy's SSRF allowlist (OH_CLOUD_PROXY_ALLOWED_HOSTS overrides it) — the same end state as today's CORS failure, so no regression.
  • Server-side follow-up (separate, automation repo): adopt the main API's bearer-aware CORS (ApiKeyAwareCORSMiddleware) so external clients work without a proxy; requires a deploy and doesn't help currently-deployed environments, hence the client-side fix here.

🐳 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.0a6
Commit 082cd40e83be618240d22cf5db341a1116903264

Pull (multi-arch manifest)

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

Run

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

All tags pushed for this build

ghcr.io/openhands/agent-canvas:sha-082cd40-amd64
ghcr.io/openhands/agent-canvas:hieptl-app-2159-amd64
ghcr.io/openhands/agent-canvas:pr-1121-amd64
ghcr.io/openhands/agent-canvas:sha-082cd40-arm64
ghcr.io/openhands/agent-canvas:hieptl-app-2159-arm64
ghcr.io/openhands/agent-canvas:pr-1121-arm64
ghcr.io/openhands/agent-canvas:sha-082cd40
ghcr.io/openhands/agent-canvas:hieptl-app-2159
ghcr.io/openhands/agent-canvas:pr-1121

About Multi-Architecture Support

  • Each tag (e.g., sha-082cd40) 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-082cd40-amd64) are also available if needed

@hieptl hieptl self-assigned this Jun 4, 2026
@vercel

vercel Bot commented Jun 4, 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 4, 2026 2:21pm

Request Review

Copy link
Copy Markdown
Contributor

🔍 Review in progress…

We are performing the review through OpenHands Cloud Automation.

github-actions Bot added a commit that referenced this pull request Jun 4, 2026
github-actions Bot added a commit that referenced this pull request Jun 4, 2026
@hieptl
hieptl requested a review from all-hands-bot June 4, 2026 10:18

all-hands-bot commented Jun 4, 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.

Code Review: PR #1121

Taste Rating

Acceptable - The fix is correct and well-tested.

Analysis

Files changed: 3 files - proxy.ts, automation-service.api.ts, and their test files.

The Problem: When using a cloud backend, browser requests to automation endpoints were blocked by CORS.

The Fix: Introduces forceProxy flag to route through same-origin proxy, applied via callAutomationCloudProxy helper.

What Works Well:

  1. Centralized proxy enforcement via helper function
  2. Comprehensive test coverage for routing and auth preservation
  3. Good documentation explains the CORS mismatch root cause
  4. Explicit timeout on health check matches local branch
  5. Minimal surface area - only 3 files

Minor Observations:

  • Omit type does not prevent forceProxy:false being passed (harmless)
  • Export visibility change on CloudProxyRequest is correct
  • Property-based test assertions are future-proof

Risk Assessment

LOW - Narrowly scoped routing fix with good tests. No breaking changes.

Verdict

Worth merging - Sound fix, thorough tests, clean architecture.

Key Insight

CORS configuration mismatch between main cloud API (loose bearer CORS) and standalone automation service (strict allowlist). Same-origin proxy hop avoids cross-origin entirely.

@tofarr tofarr 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.

🍰

@hieptl
hieptl enabled auto-merge (squash) June 4, 2026 14:20
@hieptl
hieptl merged commit 92a7ffd into main Jun 4, 2026
17 checks passed
@hieptl
hieptl deleted the hieptl/app-2159 branch June 4, 2026 14:27
@openhands-release-bot openhands-release-bot Bot added the released: v1.2.0 Shipped in v1.2.0 label Jul 9, 2026
@openhands-release-bot

Copy link
Copy Markdown
Contributor

🚀 Released in v1.2.0.

@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-check-backend

new snapshot

onboarding-step-1-choose-agent

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 — 082cd40e Deployed Jun 4, 2026 by vercel[bot]
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: CORS error prevents access to cloud automations

3 participants