Skip to content

feat(agent-server): conversation-scoped runtime APIs and clients - #4966

Merged
neubig merged 28 commits into
mainfrom
feat/conversation-scoped-runtime-api
Sep 14, 2026
Merged

neubig merged 28 commits into
mainfrom
feat/conversation-scoped-runtime-api

Conversation

@neubig

@neubig neubig commented Sep 12, 2026

Copy link
Copy Markdown
Member

HUMAN:

I did a test of an automation-driven software factory that was based on PRs stacked on top of this, so I'm pretty sure it works fine.

AGENT:

Why

Canvas must address the workspace and runtime services owned by a specific conversation. This PR supplies that shared contract by reusing the agent server's existing service routers; #3403 adds Docker dispatch and lifecycle behind the same routes.

Summary

  • Reuse the existing Bash, file, Git, desktop, and VS Code routers under /api/conversations/{id}. Local requests resolve the owning conversation, keep Bash history and working directories separate, stop commands when their Bash service closes, and reject workspace paths belonging to another conversation.
  • Add an optional conversationId only to the existing TypeScript clients that perform runtime operations and to RemoteWorkspace. A RemoteConversation binds its workspace to the ID returned by create, load, or fork. Generic BaseWorkspaceOptions remains conversation-agnostic because local and standalone workspaces can exist before any conversation.
  • Reuse the existing server-info cache and a single capability marker to select scoped routes. Older servers continue through their existing cid behavior.
  • Add the common runtime lifecycle response model plus get/reprovision methods. Local mode reports its in-process runtime as available; feat(agent-server): add docker runtime mode for per-conversation containers #3403 supplies the Docker implementation.
  • Preserve legacy file-route response metadata while giving the new scoped aliases precise binary schemas. The weak-schema allowlist is unchanged from main.

The narrowed implementation deliberately excludes a second runtime facade, a connection abstraction, scoped MCP probing, duplicate runtime fields on every conversation response, a duplicate workspace-mode signal, and a compiled-client feature flag. Lost-create-response recovery is independent #5036.

Review order: #4966#5017#3403. The profile-schema foundation #4931 is merged. #4998/#5005/#5008 were consolidated under #5016.

REST API contract changes

Compared with base OpenAPI 15a9b8609c12 for public /api/** paths.

--- base public OpenAPI
+++ head public OpenAPI
@@ -5,0 +6 @@
+operation DELETE /api/conversations/{runtime_conversation_id}/bash/bash_events operationId=clear_all_bash_events_api_conversations__runtime_conversation_id__bash_bash_events_delete
@@ -30,0 +32 @@
+operation GET /api/conversations/{conversation_id}/runtime operationId=get_local_conversation_runtime_api_conversations__conversation_id__runtime_get
@@ -32,0 +35,13 @@
+operation GET /api/conversations/{runtime_conversation_id}/bash/bash_events/ operationId=batch_get_bash_events_api_conversations__runtime_conversation_id__bash_bash_events__get
+operation GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search operationId=search_bash_events_api_conversations__runtime_conversation_id__bash_bash_events_search_get
+operation GET /api/conversations/{runtime_conversation_id}/bash/bash_events/{event_id} operationId=get_bash_event_api_conversations__runtime_conversation_id__bash_bash_events__event_id__get
+operation GET /api/conversations/{runtime_conversation_id}/desktop/url operationId=get_desktop_url_api_conversations__runtime_conversation_id__desktop_url_get deprecated=true
+operation GET /api/conversations/{runtime_conversation_id}/file/archive operationId=archive_directory_api_conversations__runtime_conversation_id__file_archive_get
+operation GET /api/conversations/{runtime_conversation_id}/file/download operationId=download_file_query_api_conversations__runtime_conversation_id__file_download_get
+operation GET /api/conversations/{runtime_conversation_id}/file/download-trajectory/{conversation_id} operationId=download_trajectory_api_conversations__runtime_conversation_id__file_download_trajectory__conversation_id__get
+operation GET /api/conversations/{runtime_conversation_id}/git/changes operationId=git_changes_query_api_conversations__runtime_conversation_id__git_changes_get
+operation GET /api/conversations/{runtime_conversation_id}/git/commits operationId=git_commits_query_api_conversations__runtime_conversation_id__git_commits_get
+operation GET /api/conversations/{runtime_conversation_id}/git/commits/{sha}/changes operationId=git_commit_changes_query_api_conversations__runtime_conversation_id__git_commits__sha__changes_get
+operation GET /api/conversations/{runtime_conversation_id}/git/diff operationId=git_diff_query_api_conversations__runtime_conversation_id__git_diff_get
+operation GET /api/conversations/{runtime_conversation_id}/vscode/status operationId=get_vscode_status_api_conversations__runtime_conversation_id__vscode_status_get
+operation GET /api/conversations/{runtime_conversation_id}/vscode/url operationId=get_runtime_vscode_url_api_conversations__runtime_conversation_id__vscode_url_get
@@ -97,0 +113 @@
+operation POST /api/conversations/{conversation_id}/runtime/reprovision operationId=reprovision_local_conversation_runtime_api_conversations__conversation_id__runtime_reprovision_post
@@ -102,0 +119,4 @@
+operation POST /api/conversations/{runtime_conversation_id}/bash/execute_bash_command operationId=execute_bash_command_api_conversations__runtime_conversation_id__bash_execute_bash_command_post
+operation POST /api/conversations/{runtime_conversation_id}/bash/start_bash_command operationId=start_bash_command_api_conversations__runtime_conversation_id__bash_start_bash_command_post
+operation POST /api/conversations/{runtime_conversation_id}/file/create_directory operationId=create_directory_api_conversations__runtime_conversation_id__file_create_directory_post
+operation POST /api/conversations/{runtime_conversation_id}/file/upload operationId=upload_file_query_api_conversations__runtime_conversation_id__file_upload_post
@@ -132,0 +153 @@
+parameter DELETE /api/conversations/{runtime_conversation_id}/bash/bash_events path:runtime_conversation_id required=true schema=type="string" format="uuid"
@@ -181,0 +203 @@
+parameter GET /api/conversations/{conversation_id}/runtime path:conversation_id required=true schema=type="string" format="uuid"
@@ -184,0 +207,41 @@
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/ path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search query:command_id__eq required=false schema=anyOf=[type="string" format="uuid",type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search query:kind__eq required=false schema=anyOf=[type="string" enum=["BashCommand","BashOutput"],type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search query:limit required=false schema=type="integer" default=100 exclusiveMinimum=0
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search query:order__gt required=false schema=anyOf=[type="integer",type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search query:page_id required=false schema=anyOf=[type="string",type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search query:sort_order required=false schema=BashEventSortOrder
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search query:timestamp__gte required=false schema=anyOf=[type="string" format="date-time",type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search query:timestamp__lt required=false schema=anyOf=[type="string" format="date-time",type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/{event_id} path:event_id required=true schema=type="string"
+parameter GET /api/conversations/{runtime_conversation_id}/bash/bash_events/{event_id} path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/desktop/url path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/desktop/url query:base_url required=false schema=type="string" default="http://localhost:8002"
+parameter GET /api/conversations/{runtime_conversation_id}/file/archive path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/file/archive query:base_ref required=false schema=anyOf=[type="string",type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/file/archive query:exclude required=false schema=anyOf=[type="array" items=type="string",type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/file/archive query:format required=false schema=type="string" enum=["git-delta","tar.gz"] default="git-delta"
+parameter GET /api/conversations/{runtime_conversation_id}/file/archive query:path required=true schema=type="string"
+parameter GET /api/conversations/{runtime_conversation_id}/file/archive query:use_default_excludes required=false schema=type="boolean" default=true
+parameter GET /api/conversations/{runtime_conversation_id}/file/download path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/file/download query:path required=true schema=type="string"
+parameter GET /api/conversations/{runtime_conversation_id}/file/download-trajectory/{conversation_id} path:conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/file/download-trajectory/{conversation_id} path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/git/changes path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/git/changes query:path required=true schema=type="string"
+parameter GET /api/conversations/{runtime_conversation_id}/git/changes query:ref required=false schema=anyOf=[type="string",type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/git/commits path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/git/commits query:limit required=false schema=type="integer" default=50 minimum=1 maximum=200
+parameter GET /api/conversations/{runtime_conversation_id}/git/commits query:path required=true schema=type="string"
+parameter GET /api/conversations/{runtime_conversation_id}/git/commits/{sha}/changes path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/git/commits/{sha}/changes path:sha required=true schema=type="string" pattern="^[0-9a-fA-F]{4,64}$"
+parameter GET /api/conversations/{runtime_conversation_id}/git/commits/{sha}/changes query:path required=true schema=type="string"
+parameter GET /api/conversations/{runtime_conversation_id}/git/diff path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/git/diff query:commit required=false schema=anyOf=[type="string" pattern="^[0-9a-fA-F]{4,64}$",type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/git/diff query:path required=true schema=type="string"
+parameter GET /api/conversations/{runtime_conversation_id}/git/diff query:ref required=false schema=anyOf=[type="string",type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/vscode/status path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/vscode/url path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter GET /api/conversations/{runtime_conversation_id}/vscode/url query:base_url required=false schema=anyOf=[type="string",type="null"]
+parameter GET /api/conversations/{runtime_conversation_id}/vscode/url query:workspace_dir required=false schema=anyOf=[type="string",type="null"]
@@ -241,0 +305 @@
+parameter POST /api/conversations/{conversation_id}/runtime/reprovision path:conversation_id required=true schema=type="string" format="uuid"
@@ -246,0 +311,6 @@
+parameter POST /api/conversations/{runtime_conversation_id}/bash/execute_bash_command path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter POST /api/conversations/{runtime_conversation_id}/bash/start_bash_command path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter POST /api/conversations/{runtime_conversation_id}/file/create_directory path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter POST /api/conversations/{runtime_conversation_id}/file/create_directory query:path required=true schema=type="string"
+parameter POST /api/conversations/{runtime_conversation_id}/file/upload path:runtime_conversation_id required=true schema=type="string" format="uuid"
+parameter POST /api/conversations/{runtime_conversation_id}/file/upload query:path required=true schema=type="string"
@@ -258,0 +329 @@
+requestBody GET /api/conversations/{runtime_conversation_id}/bash/bash_events/ application/json required=true schema=type="array" items=type="string"
@@ -284,0 +356,3 @@
+requestBody POST /api/conversations/{runtime_conversation_id}/bash/execute_bash_command application/json required=true schema=ExecuteBashRequest
+requestBody POST /api/conversations/{runtime_conversation_id}/bash/start_bash_command application/json required=true schema=ExecuteBashRequest
+requestBody POST /api/conversations/{runtime_conversation_id}/file/upload multipart/form-data required=true schema=Body_upload_file_query_api_conversations__runtime_conversation_id__file_upload_post
@@ -314,0 +389,2 @@
+response DELETE /api/conversations/{runtime_conversation_id}/bash/bash_events 200 application/json schema=type="object" additionalProperties=type="integer"
+response DELETE /api/conversations/{runtime_conversation_id}/bash/bash_events 422 application/json schema=HTTPValidationError
@@ -372,0 +449,2 @@
+response GET /api/conversations/{conversation_id}/runtime 200 application/json schema=ConversationRuntimeInfo
+response GET /api/conversations/{conversation_id}/runtime 422 application/json schema=HTTPValidationError
@@ -378,0 +457,27 @@
+response GET /api/conversations/{runtime_conversation_id}/bash/bash_events/ 200 application/json schema=type="array" items=anyOf=[BashEventBase,type="null"]
+response GET /api/conversations/{runtime_conversation_id}/bash/bash_events/ 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search 200 application/json schema=BashEventPage
+response GET /api/conversations/{runtime_conversation_id}/bash/bash_events/search 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{runtime_conversation_id}/bash/bash_events/{event_id} 200 application/json schema=BashEventBase
+response GET /api/conversations/{runtime_conversation_id}/bash/bash_events/{event_id} 404 no-content
+response GET /api/conversations/{runtime_conversation_id}/bash/bash_events/{event_id} 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{runtime_conversation_id}/desktop/url 200 application/json schema=DesktopUrlResponse
+response GET /api/conversations/{runtime_conversation_id}/desktop/url 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{runtime_conversation_id}/file/archive 200 application/octet-stream schema=type="string" format="binary"
+response GET /api/conversations/{runtime_conversation_id}/file/archive 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{runtime_conversation_id}/file/download 200 application/octet-stream schema=type="string" format="binary"
+response GET /api/conversations/{runtime_conversation_id}/file/download 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{runtime_conversation_id}/file/download-trajectory/{conversation_id} 200 application/octet-stream schema=type="string" format="binary"
+response GET /api/conversations/{runtime_conversation_id}/file/download-trajectory/{conversation_id} 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{runtime_conversation_id}/git/changes 200 application/json schema=type="array" items=GitChange
+response GET /api/conversations/{runtime_conversation_id}/git/changes 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{runtime_conversation_id}/git/commits 200 application/json schema=GitCommitsPage
+response GET /api/conversations/{runtime_conversation_id}/git/commits 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{runtime_conversation_id}/git/commits/{sha}/changes 200 application/json schema=type="array" items=GitChange
+response GET /api/conversations/{runtime_conversation_id}/git/commits/{sha}/changes 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{runtime_conversation_id}/git/diff 200 application/json schema=GitDiff
+response GET /api/conversations/{runtime_conversation_id}/git/diff 422 application/json schema=HTTPValidationError
+response GET /api/conversations/{runtime_conversation_id}/vscode/status 200 application/json schema=type="object" additionalProperties=anyOf=[type="boolean",type="string"]
+response GET /api/conversations/{runtime_conversation_id}/vscode/status 422 application/json schema=HTTPValidationError
... diff truncated; 32 more line(s)

Validation

  • 328 TypeScript tests passed before the final removals; the affected 96-test client subset, lint, build, and format checks pass on the narrowed implementation.
  • 99 focused Python tests pass, including conversation lifecycle and scoped routing.
  • OpenAPI type quality passes with the same 99 allowlisted locations as main; the REST compatibility checker reports no unapproved break.
  • An isolated live Canvas ran a DeepSeek V4 Flash conversation through the scoped Bash route; evidence is committed in .pr/sdk4966-narrow-client/.
  • The current feat(agent-server): add docker runtime mode for per-conversation containers #3403 descendant passes all 110 Docker runtime tests after the same removals.

How to Test

  • Standalone local API/server-info/deprecation suite: 30 passed.
  • TypeScript: 328 passed / 20 suites.
  • Commit hooks including Ruff/Pyright passed.

Not included

Docker lifecycle, credentials, runtime recovery and demo stay in #3403. Create-response reconciliation is independent in #5036. Async masking (#4967) and title generation (#4968) are independent PRs, not stack members.

openhands+astra requested by @neubig

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

Issue Number

Part of #5016; split from #3403 at the author’s request.


🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python-slim amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:1e1a610-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-1e1a610-python \
  ghcr.io/openhands/agent-server:1e1a610-python

All tags pushed for this build

ghcr.io/openhands/agent-server:1e1a610-golang-amd64
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-golang-amd64
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-golang-amd64
ghcr.io/openhands/agent-server:1e1a610-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:1e1a610-golang-arm64
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-golang-arm64
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-golang-arm64
ghcr.io/openhands/agent-server:1e1a610-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:1e1a610-java-amd64
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-java-amd64
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-java-amd64
ghcr.io/openhands/agent-server:1e1a610-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:1e1a610-java-arm64
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-java-arm64
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-java-arm64
ghcr.io/openhands/agent-server:1e1a610-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:1e1a610-python-amd64
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-python-amd64
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-python-amd64
ghcr.io/openhands/agent-server:1e1a610-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:1e1a610-python-arm64
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-python-arm64
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-python-arm64
ghcr.io/openhands/agent-server:1e1a610-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:1e1a610-python-slim-amd64
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-python-slim-amd64
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-python-slim-amd64
ghcr.io/openhands/agent-server:1e1a610-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-slim-amd64
ghcr.io/openhands/agent-server:1e1a610-python-slim-arm64
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-python-slim-arm64
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-python-slim-arm64
ghcr.io/openhands/agent-server:1e1a610-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-slim-arm64
ghcr.io/openhands/agent-server:1e1a610-golang
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-golang
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-golang
ghcr.io/openhands/agent-server:1e1a610-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:1e1a610-java
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-java
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-java
ghcr.io/openhands/agent-server:1e1a610-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:1e1a610-python-slim
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-python-slim
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-python-slim
ghcr.io/openhands/agent-server:1e1a610-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-slim
ghcr.io/openhands/agent-server:1e1a610-python
ghcr.io/openhands/agent-server:1e1a610201456210aad897199dd1069636de46a7-python
ghcr.io/openhands/agent-server:feat-conversation-scoped-runtime-api-python
ghcr.io/openhands/agent-server:1e1a610-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

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

Live Agent Canvas evidence

Animated recording · Scenario, exact revisions and limits · Allowlisted observations. Direct integrated enhancement demonstration with a real agent and synthetic secrets; the report distinguishes the owning PR from companion SDK/Automation/Canvas changes.

Current narrowed-client recording · exact revisions and limits. A real model creates and reads a file while Canvas sends all shell operations through the selected conversation route.

Runtime stack cleanup

The current implementation and tests remove 364 net lines from the previous head: three unpublished public wrappers, alternate constructor inputs, a redundant capability cache, and path-specific response metadata handling. Independent create-response reconciliation and its tests moved to #5036. Existing unscoped clients remain supported; no deprecated shim is needed for unpublished PR-only APIs.

Includes canonical local routes, injectable runtime dispatch, query-route deprecation registration (1.48 to 1.53), deadline enforcement and stack-base CI coverage.

openhands+astra requested by @neubig; created by an AI agent (OpenHands) on behalf of @neubig.

Co-authored-by: openhands <openhands@all-hands.dev>
Explicit prerequisite CI repair for upstream #4954; unrelated removals remain rejected.

openhands+astra requested by @neubig; created by an AI agent (OpenHands) on behalf of @neubig.

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

Copy link
Copy Markdown
Collaborator

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

@neubig
neubig added this pull request to stack #4969 September 12, 2026 00:22
@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   api.py3072692%138, 140–145, 147, 149, 151, 192, 206, 212, 263, 268, 277–279, 309, 315, 319, 340–341, 593, 596, 602
   bash_router.py50688%84–87, 97–98
   bash_service.py2533088%92–94, 196, 198, 231, 234–236, 248–251, 262, 336, 341–342, 367–372, 440–441, 477–478, 482, 515–516
   conversation_router.py2571495%291, 373, 419, 479, 649–652, 664–667, 707, 745
   dependencies.py42295%56, 84
   event_service.py91712686%181–182, 304, 308, 313, 340, 347, 381, 384–385, 389–390, 401, 407, 417–421, 424–427, 498, 519–520, 594, 648, 668, 695, 719–720, 724, 732, 735, 751, 783, 794, 801, 807, 867, 870, 885–886, 974, 1004, 1007, 1045, 1085, 1103, 1117, 1260, 1380–1383, 1387, 1427, 1431, 1438, 1452, 1467, 1514–1516, 1596, 1621, 1627, 1629, 1639, 1641, 1648, 1658, 1660–1661, 1665, 1679–1684, 1686, 1713, 1719, 1728–1729, 1735–1738, 1746–1749, 1793–1795, 1850–1851, 1853–1860, 1862–1863, 1872–1873, 1875–1876, 1883–1884, 1886–1887, 1898, 1923, 1929, 1935, 1944–1945, 1967, 1977
   file_router.py4407583%108–110, 146–148, 172–174, 279, 282–283, 290–291, 329, 331–333, 382, 426–429, 435–437, 443–445, 457–458, 495, 563–565, 570, 574–576, 618–619, 628, 635, 656–658, 715–717, 744–745, 749–750, 758, 760–765, 768–770, 856–857, 861–862, 898, 1046–1048, 1052–1055
   runtime_router.py51688%54–59
   server_details_router.py59297%27–28
TOTAL429351781159% 

@all-hands-bot

all-hands-bot commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

🚦 CI is currently failing on this PR's latest commit.

Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request @all-hands-bot as a reviewer to have it reviewed regardless of CI status.)

This is an automated check - no AI was used to generate this comment.

@all-hands-bot

Copy link
Copy Markdown
Collaborator

🚦 CI is currently failing on this PR's latest commit.

Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request @all-hands-bot as a reviewer to have it reviewed regardless of CI status.)

This is an automated check - no AI was used to generate this comment.

1 similar comment
@all-hands-bot

Copy link
Copy Markdown
Collaborator

🚦 CI is currently failing on this PR's latest commit.

Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request @all-hands-bot as a reviewer to have it reviewed regardless of CI status.)

This is an automated check - no AI was used to generate this comment.

@all-hands-bot

Copy link
Copy Markdown
Collaborator

🤖 OpenHands is reviewing this PR.

Head commit: f638e0e1165a797a7fb5d721496a1d80b33c71b9
View the conversation: https://oss-agent-canvas.ngrok.dev/conversations/0efc3650-57a1-4374-baf7-50926817230d

This comment was posted by an AI agent (OpenHands).

all-hands-bot
all-hands-bot previously approved these changes Sep 12, 2026

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.

Review Summary

Solid foundation PR for conversation-scoped runtime APIs. The route-class injection pattern, per-conversation bash event service lifecycle, and TypeScript client capability detection are well-designed and well-tested.

Key findings: No material bugs, security issues, or design flaws found.

What's good

  • Bash service lifecycle: Task tracking with _tasks set, _closed flag, CancelledError handler that sends SIGKILL to the process group, and proper cleanup in close(). The test test_conversation_close_stops_runtime_terminal verifies the process is actually killed.
  • Workspace validation: require_local_runtime correctly validates path/workspace_dir params against the conversation workspace root, and trajectory conversation_id against runtime_conversation_id. The docstring honestly frames these as "routing safeguards, not a sandbox."
  • Route registration: create_runtime_router with route_class injection cleanly separates the API contract from the dispatch adapter, so Docker (#3403) can inject its own route class without touching route definitions.
  • TS client routing: The isRuntimePath + /server_info capability check + URL rewriting logic is correct. The cid query-param fallback for non-capable servers preserves backward compatibility. Error handling resets runtimeRoutes on non-404 failures so subsequent requests retry.
  • CI scripts: The check_deprecations.py AST walker for programmatic add_api_route(deprecated=True) calls and the check_sdk_api_breakage.py accepted-members dict with reason strings are both well-tested.
  • Test coverage: 221 lines of runtime router tests covering workspace isolation, terminal history isolation, cwd validation, close cleanup, OpenAPI schema preservation, deprecation enforcement, and MCP probe scope. 71 lines of TS client tests covering scoped/unscoped/capable-server routing.

Minor observations (non-blocking)

  • The MCP runtime_verified flag is set to result.ok in the conversation-scoped path, but the probe itself (_probe_mcp_server) is identical to the host-level probe with no conversation context injected. This is accurate for local mode; Docker mode (#3403) may need a context-aware probe.
  • The runtimeRoutes promise is cached per HttpClient instance, so the capability check runs once. This is fine for a single session but won't detect server upgrades mid-session.

Risk Assessment

� LOW � This PR adds a new routing layer without changing existing endpoint behavior, agent logic, prompt templates, or tool execution. Legacy routes remain untouched. The bash service changes (task cancellation on close) are cleanup improvements that don't alter command execution behavior. No version bumps or dependency changes.

VERDICT: � Worth merging

@all-hands-bot
all-hands-bot dismissed their stale review September 12, 2026 01:25

Automated review used the wrong decision (APPROVED instead of COMMENT) and is dismissed. Findings are reposted as a comment.

@all-hands-bot

Copy link
Copy Markdown
Collaborator

The review was successfully posted to GitHub with state APPROVED.

GITHUB_REVIEW_POSTED

This comment was posted by an AI agent (OpenHands).

…rship

Share server capability discovery; keep HTTP domain-neutral and global setup separate. Bind create/load/fork workspaces to scoped runtimes.

openhands+astra requested by @neubig. Created by an AI agent (OpenHands) on behalf of @neubig.

Co-authored-by: openhands <openhands@all-hands.dev>
openhands+astra requested by @neubig. AI-generated by OpenHands on behalf of @neubig.

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

Copy link
Copy Markdown
Collaborator

🤖 OpenHands is reviewing this PR.

Head commit: 29184d83e450aa421b1680cda3aebae3a1a660e0
View the conversation: https://oss-agent-canvas.ngrok.dev/conversations/10b252ed-2146-4234-b432-bd852e2dc411

This comment was posted by an AI agent (OpenHands).

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.

Summary

This PR adds conversation-scoped runtime API endpoints (/api/conversations/{id}/...) that mirror the existing global runtime endpoints, with per-conversation workspace validation and bash history isolation. It also introduces TypeScript client infrastructure (RuntimeClient, RuntimeTransport, ServerConnection) that discovers server capabilities and routes requests accordingly, with fallback to legacy ?cid= routing on older servers.

Analysis

Architecture: The approach of reusing existing router endpoints via create_runtime_router() is clean — it avoids duplicating endpoint logic while adding a require_local_runtime dependency for workspace-scoped validation. The route-class injection pattern (route_class parameter) allows the upper Docker layer (#3403) to supply its own dispatch adapter without coupling this foundation to Docker.

Security: The require_local_runtime dependency validates path and workspace_dir parameters are absolute and inside the conversation workspace via Path.resolve().is_relative_to(root). The bash router's _validate_cwd does the same for cwd. The TS RuntimeTransport.scope() adds client-side path traversal protection (rejecting ?, #, \\, ./.. segments). These are correctly characterized as routing safeguards, not a sandbox — appropriate for local runtime where the process boundary is the real isolation.

Bash lifecycle: The BashEventService now tracks in-flight tasks (_tasks set) and properly cancels them on close(), with CancelledError handling that kills the process group before re-raising. EventService.close() closes the per-conversation bash service first. The test test_conversation_close_stops_runtime_terminal verifies that child processes are actually killed. Good.

Backward compatibility: Legacy global routes remain registered alongside the new conversation-scoped routes. The add_legacy_runtime_routes helper defines deprecated Docker compatibility routes with proper deprecation metadata (v1.48.0 → v1.53.0). The deprecation checker (check_deprecations.py) is extended to inspect programmatic add_api_route calls with deprecated=True. Local routes are intentionally not deprecated.

TypeScript client: The ServerConnection.supportsRuntimeRoutes() method memoizes capability discovery with ??=, and on non-404 errors resets the promise to allow retry. The 404 path returns false for older servers. The runtimeServiceConnections() factory correctly resolves shared vs. scoped connections and throws on conflicting conversation IDs. Test coverage is comprehensive (210-line test file covering scoping, legacy fallback, discovery coalescing, retry, and immutability enforcement).

Eval-risk flag

The BashEventService changes (task tracking, CancelledError handling, _closed flag, cwd defaulting) affect the global bash service used by existing REST endpoints, not just the new runtime-scoped one. While the agent's tool execution path goes through the SDK directly (not these REST endpoints), the changes touch terminal/stdout handling. Per repo policy, flagging for a human maintainer to decide whether lightweight evals are needed before merge.

Minor observation

The VSCode status route under the runtime router is registered with only path, endpoint, and methods — omitting response_model, tags, summary, etc. that the main loop copies for other routes. This won't cause functional issues but produces a less detailed OpenAPI schema for /api/conversations/{id}/vscode/status. Not blocking.

Risk Assessment

🟢 Low risk — No critical bugs or security issues found. The PR is well-structured with good test coverage. The main consideration is the eval-risk flag above, which warrants human maintainer judgment.


Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:

  1. Add a .agents/skills/custom-codereview-guide.md file to your branch (or edit it if one already exists) with the /codereview trigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.
  2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
  3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.

Resolve with AI? Install the iterate skill in your agent and run /iterate to automatically drive this PR through CI, review, and QA until it's merge-ready.

Was this review helpful? React with 👍 or 👎 to give feedback.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.

Review Summary

This PR introduces conversation-scoped runtime APIs, per-conversation bash history, TypeScript client support with capability discovery, and runtime lifecycle models. The implementation is clean, additive, and backward-compatible.

Key findings

No material bugs or security issues found.

I reviewed the following areas in depth:

  1. Bash service lazy creation (dependencies.py:73-81): The async get_bash_event_service creates a per-conversation BashEventService on first access. Since Python asyncio is single-threaded and there's no await between the is None check and the assignment, concurrent requests share a single instance safely. The test test_concurrent_runtime_requests_share_event_loop_bash_service validates this.

  2. Path validation (runtime_router.py:32-57): require_local_runtime validates path/workspace_dir params against the conversation workspace root using Path.resolve().is_relative_to(). The docstring correctly notes these are routing safeguards, not a sandbox boundary. TOCTOU between validation and file operation is acceptable given this stated scope.

  3. Bash task lifecycle (bash_service.py:261-271, 357-360, 521-528): The _closed flag, _tasks tracking set, and CancelledError handler that kills the process group with SIGKILL are correctly implemented. close() cancels all in-flight tasks and waits for them before closing the pubsub.

  4. Create response recovery (conversation-client.ts:72-100): The recovery loop only triggers on non-HttpError (network-level failures), never replays the POST, requires a caller-supplied conversation_id, and breaks on non-404 errors. Well-designed.

  5. Backward compatibility: All new fields (runtime_status, can_resume, runtime_error, conversation_runtime, workspace_mode, capabilities) have defaults. The file_router/file_discovery_router split keeps global discovery endpoints (/api/file/home, /api/file/search_subdirs) available while scoping file operations under conversation routes. Legacy /api/file/* and /api/bash/* routes remain unchanged.

  6. TypeScript RuntimeTransport (runtime-transport.ts:27-47): Path traversal protection rejects ?, #, \\, and ./.. path segments. The cid override guard prevents conversation identity injection. Capability discovery with caching and fallback to legacy cid query param is clean.

Eval risk flag

This PR touches bash/terminal execution service lifecycle (task tracking, cancellation, cwd validation, _closed flag). While these changes are to the agent-server HTTP API layer rather than the agent's terminal tool itself, terminal/stdin/stdout handling is in the eval-risk category per the review guidelines. Flagging for a human maintainer to decide whether lightweight evals are needed before merge.

[RISK ASSESSMENT]

  • Overall PR: LOW
  • The changes are additive and well-tested (228 new test lines in test_runtime_router.py, TypeScript scope/recovery tests). The main risk is the eval-adjacent bash service changes, which are lifecycle/cleanup-focused rather than changing command execution semantics.

VERDICT: Worth merging. No blocking issues. Human maintainer should confirm eval necessity given the bash service changes.


Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:

  1. Add a .agents/skills/custom-codereview-guide.md file to your branch (or edit it if one already exists) with the /codereview trigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.
  2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
  3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.

Resolve with AI? Install the iterate skill in your agent and run /iterate to automatically drive this PR through CI, review, and QA until it's merge-ready.

Was this review helpful? React with 👍 or 👎 to give feedback.

Reuse cached server info and existing workspace clients. Move download metadata to its owning routes and split create-response recovery into an independent fix.

Co-authored-by: openhands <openhands@all-hands.dev>
Remove the three obsolete weak-schema exceptions while preserving published media types.

Co-authored-by: openhands <openhands@all-hands.dev>
Keep the existing host bash service cancellation behavior unchanged while ensuring conversation-owned processes are stopped on release.

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Use the existing weak-schema ratchet for scoped aliases and bind conversation workspaces only after reading the server response.

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

neubig commented Sep 14, 2026

Copy link
Copy Markdown
Member Author

@all-hands-bot please review the current head. If there are no blocking findings, please explicitly approve this PR rather than leaving only a comment.

@neubig
neubig marked this pull request as ready for review September 14, 2026 02:28
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
@neubig

neubig commented Sep 14, 2026

Copy link
Copy Markdown
Member Author

@all-hands-bot please review the current head. If there are no blocking findings, please explicitly approve this PR rather than leaving only a comment.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review: head 01398e7e

Verdict: APPROVE � No blocking findings.

What changed since the last approval (ae05f6e0)

The head has been significantly refactored and simplified compared to the previously approved SHA. Key changes reviewed:

  1. Runtime router renamed and simplified � require_local_runtime � bind_local_conversation_runtime. It now resolves the conversation via get_conversation_service + get_event_service, stores the event service on request.state, and validates path/workspace_dir params against the conversation workspace root using Path.resolve().is_relative_to().

  2. MCP probing removed from scoped routes � The MCPProbeScope/runtime_verified fields and the scoped MCP test endpoint were removed, keeping the runtime router focused on workspace/file/bash/git/desktop/vscode operations only. This is a correct narrowing.

  3. Runtime fields removed from ConversationInfo � runtime_status, can_resume, and runtime_error were moved off every conversation response and into the dedicated ConversationRuntimeInfo model exposed only via GET /runtime and POST /runtime/reprovision. Cleaner contract.

  4. File router split � file_discovery_router (/home, /search_subdirs) is mounted globally; file_router (download, upload, archive, download-trajectory) is reused under the conversation-scoped prefix. Binary response schemas are typed on their owning routes, with application/json stripped from scoped FileResponse routes via RuntimeRouter.add_api_route.

  5. Bash lifecycle is now intrinsic � _tasks set, _closed flag, and CancelledError � SIGKILL handling are part of BashEventService itself (not a wrapper). close() cancels all tracked tasks and awaits them. The default_cwd field enables per-conversation working-directory isolation, and _validate_cwd in bash_router.py rejects cwd values outside the workspace.

  6. TypeScript client transport � ConversationScopedHttpClient rewrites /api/... paths to /api/conversations/{id}/..., blocks cid override, rejects path traversal (../. segments, query strings in URL), and falls back to legacy cid query param for older servers. Server-info capability discovery is cached and retried on failure (503 propagates, 404 falls back).

  7. RemoteConversation workspace binding � After create/load/fork, the workspace is rebound with the conversation ID from the server response, ensuring all subsequent runtime operations are scoped correctly.

Security verification

  • Path containment: Path.resolve().is_relative_to() correctly handles symlinks and .. traversal. Relative paths are rejected (is_absolute() check). Verified across file, vscode, and bash cwd validation.
  • Conversation isolation: Bash history, working directories, and file access are scoped per conversation. Cross-conversation access returns 422. Test test_local_runtime_workspace_and_terminal_context confirms this.
  • Process cleanup: close() cancels tasks and kills process groups. Test test_conversation_close_stops_runtime_terminal verifies orphaned processes are killed.
  • Client-side guards: cid override prevention, path traversal rejection, and workspace-session identity enforcement are all tested in conversation-scope.test.ts.

Test results

All relevant tests pass:

  • test_runtime_router.py: 6/6 passed (workspace isolation, path containment, bash lifecycle, vscode defaults, route registration, concurrent requests)
  • test_conversation_router.py: 2/2 passed (runtime requires existing conversation)
  • test_server_details_router.py: 8/8 passed (capabilities, conversation_runtime field)
  • conversation-scope.test.ts: 10/10 passed (routing, scoping, override rejection, discovery retry, older-server fallback, workspace binding)
  • api-clients.test.ts: 86/86 passed (including new getRuntime/reprovisionRuntime)

The test_remote_conversation_live_server.py cross test has an environment config issue (env_parser IndexError) unrelated to this PR's changes.

Risk assessment: LOW

  • All changes are additive API surface and infrastructure.
  • Agent terminal execution uses the SDK TerminalTool, not these REST endpoints.
  • No dependency changes, no agent loop changes.
  • Backward compatibility preserved: legacy routes work, TypeScript clients fall back gracefully.

Generated by OpenHands AI on behalf of the user.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved on head commit 72a74b3. No blocking findings — see the comment for the detailed review.

@all-hands-bot

Copy link
Copy Markdown
Collaborator

Review of head commit 72a74b3

I reviewed all 30 changed files (863 additions, 72 deletions) across the Python agent-server and TypeScript client. No blocking findings. The review is approved.

Python server-side

runtime_router.py (new) — Re-registers the existing bash, file, git, desktop, and vscode routers under /api/conversations/{runtime_conversation_id}/... with a require_local_runtime dependency that:

  • Resolves the conversation workspace root and validates all path/workspace_dir parameters via Path.resolve().is_relative_to(root), correctly handling symlink escapes and .. traversal.
  • Validates that trajectory conversation_id path params match the owning conversation.
  • Stashes the resolved EventService on request.state for downstream dependency injection.

The RuntimeRouter subclass strips the stale application/json media type from FileResponse routes so scoped aliases get precise binary schemas, while legacy routes keep their existing weak schema. The route_class_override parameter makes the router extensible for future dispatch adapters (tested).

bash_service.py — Adds default_cwd, task tracking (_tasks set with add_done_callback), a _closed guard, and CancelledError handling that SIGKILLs the process group only for conversation-owned services (default_cwd is not None), preserving the shared host service's existing cancellation behavior. close() cancels all tracked tasks and awaits them before closing the pubsub. Process cleanup is verified by test_conversation_close_stops_runtime_terminal.

bash_router.py_validate_cwd rejects out-of-workspace cwd before command execution. Uses the same resolve().is_relative_to() pattern.

dependencies.pyget_bash_event_service is now async and lazily creates a conversation-scoped BashEventService (with per-conversation bash_events_dir and default_cwd) when runtime_conversation_id is in the path. Falls through to the shared host service otherwise. Concurrent requests share a single instance per conversation (verified by test_concurrent_runtime_requests_share_event_loop_bash_service).

conversation_router.pyGET /{id}/runtime and POST /{id}/runtime/reprovision return ConversationRuntimeInfo with AVAILABLE status for local mode. Both 404 on missing conversations.

file_router.py — Splits /home and /search_subdirs into a separate file_discovery_router (host-level, not included in the runtime router) and adds _FILE_DOWNLOAD_RESPONSES with a binary schema for download/archive/trajectory routes.

models.py / server_details_router.py — New ConversationRuntimeStatus/ConversationRuntimeInfo models and conversation_runtime + conversation_runtime_routes_v1 capability marker. All additive with defaults.

TypeScript client

runtime-transport.ts (new) — RuntimeTransport validates that URLs are clean API paths (no query strings, no #, no \, no ./.. segments), blocks cid parameter override, and uses cached server-info capability detection to route to either /api/conversations/{id}/... or the legacy ?cid= fallback. 404 on server-info → legacy mode; non-404 errors clear the cache and propagate. Discovery is cached and shared across concurrent requests on the same client.

Client refactors (bash-client, desktop-client, file-client, vscode-client) — All adopt RuntimeServiceOptions (adds optional conversationId) and delegate to runtimeServiceConnections. FileClient keeps a server reference for discovery routes (getHome, searchSubdirs) that are intentionally not scoped.

remote-conversation.tsworkspace becomes a getter over _workspace; bindWorkspace rebinds after create, load, and fork with the server-returned working directory and conversation ID. workspaceWorkingDir safely extracts working_dir from ConversationInfo.

remote-workspace.ts — Validates that workspace-session auth belongs to the selected conversation; encodeURIComponent on conversation IDs in URL construction.

Tests

  • test_runtime_router.py (208 lines): workspace isolation, terminal history separation, cwd/path validation (422), legacy route compatibility, 404 for missing conversations, auth (401), process kill on close, vscode default workspace, concurrent instance sharing, route-class override, OpenAPI schema preservation (binary schema on scoped routes, JSON preserved on legacy).
  • test_conversation_router.py: runtime endpoints 404 on missing conversations.
  • test_server_details_router.py: capability and conversation_runtime field.
  • conversation-scope.test.ts (170 lines): scoped routing with/without capability, discovery reuse, 503 retry without silent downgrade, older-server fallback, conversation override rejection, path traversal rejection, auth session scoping.
  • api-clients.test.ts: getRuntime/reprovisionRuntime methods.
  • test_remote_conversation_live_server.py: live runtime endpoint verification.

Assessment

  • Risk: LOW — Additive API surface and infrastructure only. No agent execution path changes (terminal tool, prompts, planning logic untouched). All new fields have defaults; legacy routes and cid behavior preserved.
  • Security: Path containment is enforced on both server (resolve().is_relative_to) and client (path segment validation). Conversation ID override is blocked. No secrets or credentials introduced.
  • Backward compatibility: TypeScript clients fall back to ?cid= for older servers; Python routes are purely additive.

Generated by OpenHands AI on behalf of the user.

@enyst

enyst commented Sep 14, 2026

Copy link
Copy Markdown
Member

@OpenHands Find the repo conventions on the temporary artefacts in .pr/ directory and html design doc repo skill and use it to create a doc for visualizing the changes. Please include the explanation / description of why so many REST API changes or what do they do.

Edit the PR description to include a htmlpreview link so we can see it.

@openhands-ai

openhands-ai Bot commented Sep 14, 2026

Copy link
Copy Markdown

I'm on it! enyst can track my progress at all-hands.dev

@neubig
neubig merged commit b5c8ab9 into main Sep 14, 2026
54 of 55 checks passed
@neubig
neubig deleted the feat/conversation-scoped-runtime-api branch September 14, 2026 03:30
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.

5 participants