← Home

Agent Canvas + OpenHands app_server bridge study

A gap analysis of OpenHands/OpenHands#14909 and companion OpenHands/agent-canvas#1435, with software-agent-sdk agent-server endpoints as the runtime contract. Reviewed from source in June 2026. Prepared by OpenHands (AI) on behalf of the user.

target topology

Agent Canvas should not need the old OpenHands frontend. The useful OpenHands piece is a smaller app_server slice: it owns sandbox and app-conversation metadata, creates or resumes a sandbox, finds the sandbox's exposed agent-server, and relays selected HTTP and WebSocket traffic back to Agent Canvas.

Minimal answer

Keep the app_server orchestration core plus proxy/gateway routers. Drop the local GUI React app for this path.

Current PR shape

Good first bridge for app-conversation discovery, /ws/events, /ask_agent, and git proxy routes.

Main risk

Agent Canvas still sends several runtime calls around app_server, so the two PRs are not yet a full gateway topology.

The flow we actually want

In this architecture, Agent Canvas treats OpenHands app_server as an app/cloud backend, while each sandbox continues to run a normal software-agent-sdk agent-server. app_server is not the agent runtime; it is the control plane and network gateway.

sequenceDiagram
  participant AC as Agent Canvas
  participant AS as OpenHands app_server
  participant SB as Sandbox service
  participant RT as Sandbox-hosted agent-server

  AC->>AS: POST /oauth/device/authorize + /token
  AC->>AS: POST /api/v1/app-conversations
  AS->>SB: create or select sandbox
  SB-->>AS: exposed AGENT_SERVER URL + session key
  AS->>RT: POST /api/conversations
  RT-->>AS: ConversationInfo
  AS-->>AC: AppConversationStartTask
  AC->>AS: GET /api/v1/app-conversations?ids=...
  AS-->>AC: conversation_url + session_api_key + websocket_url=/ws/events/{id}
  AC->>AS: WS /ws/events/{id}
  AS->>RT: WS /sockets/events/{id}
  RT-->>AS: SDK events
  AS-->>AC: same SDK events
    

Terminology: in this note, “OpenHands OSS” means the minimal app_server subset necessary for sandbox orchestration with Agent Canvas. It does not mean the old OpenHands local GUI frontend.

Kripper's device OAuth flow

PR #14909 adds a new base-app openhands/app_server/oauth/device_flow.py. This is not the existing Enterprise/SaaS device flow from enterprise/server/routes/oauth_device.py; it is a lightweight compatibility surface for Agent Canvas.

AspectPR #14909 app_server flowStandard / production expectation
StorageIn-memory DEVICE_STORE and USER_CODE_INDEX.Durable store with expiry, cleanup, restart tolerance, and multi-worker safety.
User authenticationverify-authenticated accepts a user_code form post; no dependency on an authenticated user.The verifier must prove the browser user is logged in and authorize the device for that user.
Token issuanceGenerates sk-... with secrets.token_urlsafe and returns it as access_token.Mint or return a real app_server API key that the rest of app_server recognizes.
Route auth alignmentReturns token_type: Bearer, while base app_server dependencies primarily check SESSION_API_KEY / X-Session-API-Key; Enterprise uses API keys/bearer via its own stack.One agreed auth contract for Agent Canvas calls: bearer, X-Access-Token, or X-Session-API-Key.
RFC behaviorHas basic authorization_pending, access_denied, and expiry responses.Also needs polling rate-limit / slow_down, denial UI, CSRF protection, auditability, and revocation.

Completeness read: useful as a demo handshake, but not complete from a standard OAuth/device-auth or production-use point of view. Repo check: Agent Canvas already stores an API key per registered backend. Local/remote agent-server backends send that key as X-Session-API-Key; cloud backends send it as Authorization: Bearer .... Base app_server can already protect routes with SESSION_API_KEY and X-Session-API-Key. So a minimal self-hosted app_server backend can avoid OAuth entirely by using a user-provided/preconfigured session key. What is missing is not auth plumbing in app_server; it is an Agent Canvas backend mode that uses app_server/cloud-style routes with session-key auth instead of treating protocol and auth as the same kind="cloud" choice.

Minimum viable OpenHands/OpenHands slice

The minimum is not one router. app_server's conversation path is stitched through app-conversation records, sandbox records, sandbox specs, and the HTTP/WebSocket gateway. User settings, secrets, LLM profiles, and local git-provider tokens should stay in Agent Canvas + agent-server unless app_server is deliberately acting as a hosted SaaS identity layer.

1. FastAPI shell and router mount

Keep openhands/app_server/app.py and v1_router.py only as the app_server host: lifespan, middleware, /api/v1, and optional /ws mount. OAuth device routes are optional unless this bridge needs browser-mediated pairing instead of a pasted/preconfigured key.

  • ENABLE_WEBSOCKET_GATEWAY=true gates /ws/events/{id}.
  • SERVE_FRONTEND and frontend/build are not conceptually required for Agent Canvas.

2. App-conversation control plane

Keep app_conversation_router.py, models, services, start-task store, and live-status service. This is where app_server turns a UI request into a sandbox + agent-server conversation.

  • POST /api/v1/app-conversations
  • GET /api/v1/app-conversations/search
  • GET /api/v1/app-conversations?ids=...
  • GET /api/v1/app-conversations/start-tasks

3. Sandbox orchestration

Keep sandbox services, routers, and sandbox-spec catalog. app_server must know sandbox status, exposed URLs, and the sandbox session key.

  • /api/v1/sandboxes/* for pause/resume/lifecycle
  • /api/v1/sandbox-specs/* for default sandbox shape
  • AGENT_SERVER exposed URL lookup

4. Agent-server context resolver

Keep the shared resolver currently living in app_conversation_router._get_agent_server_context. Every proxy route depends on it.

  • loads app conversation
  • loads sandbox and sandbox spec
  • rejects paused/non-running sandboxes
  • returns internal agent-server URL + session key

5. WebSocket event gateway

Keep websocket_router.py and return AppConversation.websocket_url from live-status responses.

  • Agent Canvas uses websocket_url as the gateway signal.
  • Relative /ws/events/{id} must resolve against app_server, not the sandbox.
  • Frames can be tunneled without schema awareness.

6. Runtime HTTP proxy routes

Keep thin HTTP routes for the agent-server calls the browser cannot make directly.

  • POST /api/conversations/{id}/ask_agent
  • GET /api/conversations/{id}/events/count
  • POST /api/conversations/{id}/pause and /run
  • GET /api/v1/git/changes|diff?conversation_id=...

7. Event history

Keep event_router.py enough for GET /api/v1/conversation/{id}/events/search. Agent Canvas loads history by REST before opening WebSocket.

  • app_server event store is fine when callbacks are configured
  • PR #14909's fallback to agent-server is useful when the local store is empty

8. Do not pull settings into app_server

For the minimal bridge, user settings, secrets, LLM profiles, and local git-provider tokens should remain owned by Agent Canvas + the sandbox agent-server. app_server should only pass conversation-start inputs through or inject narrowly-scoped sandbox facts.

  • Use agent-server /api/settings, /api/settings/secrets, and profile APIs for runtime configuration.
  • Use Agent Canvas' existing local/self-hosted settings UI instead of re-creating /api/v1/settings.
  • Keep app_server git-provider search only as an optional SaaS/repository-browser feature, not part of the minimal sandbox gateway.

Settings scope for an app_server backend

This is the part that must be explicit. There are three different scopes, and only one is desirable for the minimal bridge.

ScopeWhere storedWhat it meansUse for minimal bridge?
Current local Agent Canvas backendStable agent-server /api/settings, backed by agent-server persistence: OH_PERSISTENCE_DIR/settings.json or default workspace/.openhands/settings.json.Effectively per registered agent-server backend / installation. One settings set is reused across conversations started through that backend.Yes, this is the model to preserve.
Recommended minimal app_server backendA new Agent Canvas-owned profile/settings store keyed by backend id. Repo check: this does not exist today. Current SettingsService stores local settings on the active agent-server; cloud settings on app_server /api/v1/settings; ProfilesService only talks to the active local agent-server.Per app_server backend registration, reused for all sandboxes created through that backend. At conversation start, Agent Canvas resolves the selected profile and sends the resulting runtime settings/secrets to app_server for one-time forwarding.Desired, but requires Agent Canvas product work.
Current cloud/app_server pathapp_server /api/v1/settings and app_server user/secret stores.Per app_server user/org, shared across all sandboxes created by that app_server. This is how OpenHands Cloud works today: app_server builds the full runtime StartConversationRequest from user.agent_settings, user.conversation_settings, and app_server secrets.No for minimal. This recreates settings ownership in app_server.
Sandbox agent-serverThe sandbox's own agent-server /api/settings store inside that sandbox filesystem.Per sandbox / per agent-server process. Good for inspecting or changing one already-running runtime, but not durable across new sandboxes unless the sandbox filesystem is deliberately persisted/shared.Only for runtime-local overrides, not canonical user settings.

Recommendation: for an OpenHands app_server backend, the canonical settings should be per Agent Canvas backend registration, not per sandbox. A new sandbox should receive a complete conversation-start payload derived from that backend's selected Agent Canvas profile. Today Agent Canvas does not have that separate store; adding app_server support cleanly means adding it or explicitly choosing an existing stable local agent-server as the profile store. app_server should orchestrate and forward the payload to the sandbox agent-server; it should not own the user's LLM profiles, saved secrets, MCP config, or app preferences.

That means the current PR shape is not yet cleanly minimal: because Agent Canvas marks the OpenHands OSS backend as kind="cloud", today's settings code would naturally call app_server /api/v1/settings. To make “Agent Canvas + agent-server own settings” real, Agent Canvas needs to separate control-plane backend from settings/profile owner, then send the resolved agent_settings, conversation_settings, and conversation secrets in POST /api/v1/app-conversations or an equivalent start route. app_server can validate, add sandbox facts, and forward to the sandbox-hosted agent-server, but it should not persist those settings as its own user model.

What can be left out for this path

If the deliverable is “Agent Canvas talks to OpenHands app_server”, then several large OpenHands/OpenHands areas are not minimum requirements.

Old UIfrontend/*, old local GUI routes, old React event renderers, old right panel implementations.
SaaS chromeBilling, org switch UX, invitation flows, hosted-only account surfaces.
Most integrationsKeep SaaS repository/provider integrations out of the minimum. Agent Canvas + agent-server already own local settings, secrets, and profile configuration.
Runtime internalsDo not reimplement agent-server. Proxy it or orchestrate conversation start.

Endpoint responsibility map

This table merges the PR diff with the endpoint gist. The useful dividing line is: app_server owns app metadata and sandboxes; agent-server owns conversation execution and workspace inspection.

SurfaceOwnerKeep?Why
/api/v1/app-conversationsapp_serveryesCreates and lists app-level conversations; wraps sandbox creation and POST /api/conversations on the runtime.
/api/v1/app-conversations/start-tasksapp_serveryesAgent Canvas already expects asynchronous cloud-style conversation startup.
/api/v1/sandboxesapp_serveryesThis is the actual sandbox control plane: pause, resume, status, exposed URLs, session keys.
/ws/events/{id}app_server proxyyesThe centerpiece of the PR: browser connects to app_server, app_server tunnels to /sockets/events/{id}.
/api/conversations/{id}/ask_agentapp_server proxyyesAgent Canvas PR #1435 explicitly routes /btw through this when websocket_url exists.
/api/v1/conversation/{id}/events/searchhybridyesAgent Canvas REST-first history loader uses this before WebSocket. It can read app_server events or fallback to agent-server.
/api/v1/git/repositories/searchapp_serveroptionalOnly for hosted/SaaS repository browsing. Not required when Agent Canvas attaches a local workspace or sends repo information directly at conversation start.
/api/v1/settings, /api/v1/secretsapp_serverno for minimalDo not duplicate Agent Canvas + agent-server settings/secrets. app_server should not become a second settings source unless this is explicitly a SaaS identity backend.
/api/llm, /api/settings on agent-serveragent-serveryesThis is the existing implementation Agent Canvas should use for LLM profiles, secrets, and runtime-owned configuration.
frontend/buildold UInoAgent Canvas replaces this UX. Static serving can remain for compatibility, but it is not minimum.

Gaps in the two PRs for the exact target topology

The PRs move in the right direction, but “all browser traffic goes through app_server, which reaches private sandbox agent-servers” needs a few more seams closed.

Message send still bypasses app_server

Agent Canvas cloud mode still sends user messages to hostOverride=conversation_url and path /api/conversations/{id}/events. That depends on Agent Canvas' own /api/cloud-proxy reaching the sandbox runtime.

Bridge fix: when websocket_url is present, route sends through app_server, ideally POST /api/v1/app-conversations/{id}/send-message or a new /api/conversations/{id}/events proxy.

Confirmation responses still bypass app_server

respond_to_confirmation remains a direct runtime call in Agent Canvas. app_server PR #14909 does not add a matching gateway route.

Bridge fix: add and use POST /api/conversations/{id}/events/respond_to_confirmation on app_server.

Git path mismatch

OpenHands PR #14909 adds /api/v1/git/changes?conversation_id=... and /api/v1/git/diff?conversation_id=.... Current Agent Canvas cloud git service calls /api/v1/app-conversations/{id}/git/changes and /diff.

Bridge fix: either add compatibility routes in app_server or update Agent Canvas to use the new /api/v1/git/*?conversation_id= contract.

WebSocket replay params are downgraded

Agent Canvas opens the main socket with resend_mode=since&after_timestamp=... after REST history loads. The new app_server gateway only accepts resend_all and forwards resend_all to the runtime.

Bridge fix: pass through resend_mode and after_timestamp so reconnect and REST-then-WS semantics match agent-server.

Bash WebSocket is not gatewayed

Agent Canvas still derives /sockets/bash-events from conversation_url. If the sandbox agent-server is private, terminal-tab command execution needs a gateway too.

Bridge fix: add /ws/bash-events/{conversation_id} or an equivalent conversation-scoped terminal proxy.

Auth story is not yet real OAuth

The PR adds in-memory device flow and returns a bearer-looking token, while app_server route dependencies still primarily understand SESSION_API_KEY / X-Session-API-Key or SaaS access-token dependencies.

Bridge fix: decide whether self-hosted app_server uses bearer API keys, X-Access-Token, or session API keys, then make Agent Canvas and app_server dependencies agree.

Minimal route contract for Agent Canvas

If we write the contract down from Agent Canvas' point of view, it looks like this:

NeedRouteMode
Add backendPrefer pasted/preconfigured app_server key; optionally POST /oauth/device/authorize, POST /oauth/device/token if real device auth existsapp_server auth
Start conversationPOST /api/v1/app-conversations carrying resolved agent_settings, conversation_settings, and one-time secret references/values; GET /api/v1/app-conversations/start-tasksapp_server orchestration, not settings storage
Load conversation metadataGET /api/v1/app-conversations/search, GET /api/v1/app-conversations?ids=...app_server + live status
Stream eventsWS /ws/events/{id}WS /sockets/events/{id}gateway tunnel
Send user messagesPOST /api/v1/app-conversations/{id}/send-message or POST /api/conversations/{id}/eventsgateway/orchestration
History paginationGET /api/v1/conversation/{id}/events/searchapp_server history or fallback proxy
Side questionsPOST /api/conversations/{id}/ask_agentgateway proxy
Confirm actionsPOST /api/conversations/{id}/events/respond_to_confirmationgateway proxy
Git diff panelGET /api/v1/git/changes?conversation_id=..., GET /api/v1/git/diff?conversation_id=...gateway proxy
Pause/resume sandboxPOST /api/v1/sandboxes/{sandbox_id}/pause, /resumeapp_server sandbox control

Source notes

Working conclusion: the old local GUI can be removed from the critical path. The irreducible OpenHands/OpenHands value for this scenario is app_server as a stateful sandbox control plane plus a narrow gateway to each sandbox-hosted agent-server. The two PRs establish the pattern, but the next step should be making every Agent Canvas runtime call choose the app_server gateway whenever websocket_url is present, while keeping settings/secrets/profile ownership in Agent Canvas + agent-server instead of recreating it in app_server.

← Back to studies