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.
Keep the app_server orchestration core plus proxy/gateway routers. Drop the local GUI React app for this path.
Good first bridge for app-conversation discovery, /ws/events, /ask_agent, and git proxy routes.
Agent Canvas still sends several runtime calls around app_server, so the two PRs are not yet a full gateway topology.
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.
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.
| Aspect | PR #14909 app_server flow | Standard / production expectation |
|---|---|---|
| Storage | In-memory DEVICE_STORE and USER_CODE_INDEX. | Durable store with expiry, cleanup, restart tolerance, and multi-worker safety. |
| User authentication | verify-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 issuance | Generates 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 alignment | Returns 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 behavior | Has 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.
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.
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.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-conversationsGET /api/v1/app-conversations/searchGET /api/v1/app-conversations?ids=...GET /api/v1/app-conversations/start-tasksKeep 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 shapeAGENT_SERVER exposed URL lookupKeep the shared resolver currently living in app_conversation_router._get_agent_server_context. Every proxy route depends on it.
Keep websocket_router.py and return AppConversation.websocket_url from live-status responses.
websocket_url as the gateway signal./ws/events/{id} must resolve against app_server, not the sandbox.Keep thin HTTP routes for the agent-server calls the browser cannot make directly.
POST /api/conversations/{id}/ask_agentGET /api/conversations/{id}/events/countPOST /api/conversations/{id}/pause and /runGET /api/v1/git/changes|diff?conversation_id=...Keep event_router.py enough for GET /api/v1/conversation/{id}/events/search. Agent Canvas loads history by REST before opening WebSocket.
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.
/api/settings, /api/settings/secrets, and profile APIs for runtime configuration./api/v1/settings.This is the part that must be explicit. There are three different scopes, and only one is desirable for the minimal bridge.
| Scope | Where stored | What it means | Use for minimal bridge? |
|---|---|---|---|
| Current local Agent Canvas backend | Stable 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 backend | A 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 path | app_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-server | The 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.
If the deliverable is “Agent Canvas talks to OpenHands app_server”, then several large OpenHands/OpenHands areas are not minimum requirements.
frontend/*, old local GUI routes, old React event renderers, old right panel implementations.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.
| Surface | Owner | Keep? | Why |
|---|---|---|---|
| /api/v1/app-conversations | app_server | yes | Creates and lists app-level conversations; wraps sandbox creation and POST /api/conversations on the runtime. |
| /api/v1/app-conversations/start-tasks | app_server | yes | Agent Canvas already expects asynchronous cloud-style conversation startup. |
| /api/v1/sandboxes | app_server | yes | This is the actual sandbox control plane: pause, resume, status, exposed URLs, session keys. |
| /ws/events/{id} | app_server proxy | yes | The centerpiece of the PR: browser connects to app_server, app_server tunnels to /sockets/events/{id}. |
| /api/conversations/{id}/ask_agent | app_server proxy | yes | Agent Canvas PR #1435 explicitly routes /btw through this when websocket_url exists. |
| /api/v1/conversation/{id}/events/search | hybrid | yes | Agent Canvas REST-first history loader uses this before WebSocket. It can read app_server events or fallback to agent-server. |
| /api/v1/git/repositories/search | app_server | optional | Only 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/secrets | app_server | no for minimal | Do 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-server | agent-server | yes | This is the existing implementation Agent Canvas should use for LLM profiles, secrets, and runtime-owned configuration. |
| frontend/build | old UI | no | Agent Canvas replaces this UX. Static serving can remain for compatibility, but it is not minimum. |
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.
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.
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.
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.
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.
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.
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.
If we write the contract down from Agent Canvas' point of view, it looks like this:
| Need | Route | Mode |
|---|---|---|
| Add backend | Prefer pasted/preconfigured app_server key; optionally POST /oauth/device/authorize, POST /oauth/device/token if real device auth exists | app_server auth |
| Start conversation | POST /api/v1/app-conversations carrying resolved agent_settings, conversation_settings, and one-time secret references/values; GET /api/v1/app-conversations/start-tasks | app_server orchestration, not settings storage |
| Load conversation metadata | GET /api/v1/app-conversations/search, GET /api/v1/app-conversations?ids=... | app_server + live status |
| Stream events | WS /ws/events/{id} → WS /sockets/events/{id} | gateway tunnel |
| Send user messages | POST /api/v1/app-conversations/{id}/send-message or POST /api/conversations/{id}/events | gateway/orchestration |
| History pagination | GET /api/v1/conversation/{id}/events/search | app_server history or fallback proxy |
| Side questions | POST /api/conversations/{id}/ask_agent | gateway proxy |
| Confirm actions | POST /api/conversations/{id}/events/respond_to_confirmation | gateway proxy |
| Git diff panel | GET /api/v1/git/changes?conversation_id=..., GET /api/v1/git/diff?conversation_id=... | gateway proxy |
| Pause/resume sandbox | POST /api/v1/sandboxes/{sandbox_id}/pause, /resume | app_server sandbox control |
websocket_url, OAuth device-flow stub, and the optional WebSocket gateway.websocket_url against backend host, and routes ask_agent through the gateway signal./api/conversations, events, git, settings, and /sockets/*.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.