feat(sdk): extend existing conversation and workspace APIs for automation - #5010
Conversation
|
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
Coverage Report •
|
||||||||||||||||||||||||||||||||||||||||
|
🤖 OpenHands is reviewing this PR. Head commit: This comment was posted by an AI agent (OpenHands). |
|
🤖 OpenHands is reviewing this PR. Head commit: This comment was posted by an AI agent (OpenHands). |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
Verdict: APPROVE — 🟢 Low risk
Clean, well-structured additive client. The shared _requests.py module eliminates sync/async duplication, UUID validation prevents path injection on all conversation-scoped operations, the credential accessor fails closed on empty/non-string values, and release correctly treats 404 as idempotent while propagating other failures. Tests cover the full wire contract, scope validation, release idempotency, legacy runtime restrictions, and missing-credential edge cases.
One observation (non-blocking): The sync AgentServerClient is missing runtime_for_api_prefix() — only the async AsyncAgentServerClient exposes it (line 223). If sync consumers ever need to migrate from a stored API prefix, they would have no public method. This may be intentional if all migration consumers are async, but worth confirming.
Server endpoint coordination note: The client targets conversation-scoped bash/file/runtime endpoints that do not exist on the current main server. The PR description acknowledges this — the server-side changes are in #4966/#3403, #4998/#5005, and #5008. The client is correctly additive and the MockTransport tests validate wire shape without needing live endpoints. No issue with the client code itself; just flagging the cross-PR dependency.
Automated review used the wrong decision (APPROVED instead of COMMENT) and is dismissed. Findings are reposted as a comment.
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
🟡 Acceptable - Works well; two actionable findings around merge-gate completeness and error handling.
Summary
This PR adds a repository-scoped GitHub gateway that exposes role-specific operations (triage, developer, reviewer, watchdog) to Docker automation workers while keeping upstream GitHub credentials in the control plane. The authorization model is well-designed: permitted() uses strict re.fullmatch patterns, path sanitization blocks traversal characters, secrets.compare_digest prevents timing attacks on role tokens, and the merge gate is fail-closed across head SHA, base ancestry, mergeability, CI statuses, and check-runs completeness. Repository placement is correct - this is a skill reference script, not SDK or agent-server behavior.
Findings
1. Statuses pagination not checked for completeness (merge gate asymmetry)
The merge gate carefully verifies check-runs pagination completeness via check_page.get("total_count", len(checks)) == len(checks), but latest_statuses() fetches only the first 100 entries from /commits/{sha}/statuses?per_page=100 with no equivalent completeness check. If a commit has >100 status entries, a failed third-party CI status beyond page 1 would be silently absent from statuses.values(), and the all(s["state"] == "success" for s in statuses.values()) check would pass without seeing it. The required factory contexts would likely be in the first page, but the broader "all statuses must pass" invariant could be violated. Consider adding a total_count comparison for statuses as well, or documenting why only the first page is sufficient.
2. URLError not handled in request handler
The exception handling in do_POST catches HTTPError and (ValueError, KeyError, TypeError) but not urllib.error.URLError. Non-HTTP network failures (DNS resolution failure, connection refused, socket timeout exceeding the 60s/90s urlopen timeout) would propagate as unhandled exceptions in the handler thread. While ThreadingHTTPServer keeps the server alive, the client receives a connection reset with no JSON error response. For a merge service where the watchdog may retry on ambiguous failures, returning a structured 502/504 error would make failure semantics cleaner.
Risk Assessment
- [Overall PR]
⚠️ Risk Assessment: 🟡 MEDIUM
The gateway implements a security-critical permission boundary with a well-structured authorization model and thorough fail-closed merge gate. The two findings above are completeness gaps rather than privilege escalation paths - the core security properties (no main writes, no force push, no admin operations, SHA-guarded merge) hold. Tests are real and exercise actual code paths rather than mock-only assertions.
VERDICT: ✅ Worth merging - core logic is sound, address the two findings as follow-ups.
KEY INSIGHT: The authorization and merge-gate design is solid; the gaps are in edge-case completeness (statuses pagination) and error-path robustness (URLError), not in the permission model itself.
Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:
- Add a
.agents/skills/custom-codereview-guide.mdfile to your branch (or edit it if one already exists) with the/codereviewtrigger 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.- Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
- 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
/iterateto automatically drive this PR through CI, review, and QA until it's merge-ready.Was this review helpful? React with 👍 or 👎 to give feedback.
|
🤖 OpenHands is reviewing this PR. Head commit: This comment was posted by an AI agent (OpenHands). |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
Summary
Clean, well-structured additive client module. The shared _requests.py operation layer eliminates wire-contract duplication between sync/async, UUID validation on conversation IDs prevents path traversal, and the test suite covers the key contracts (scoped wire paths, invalid scope rejection, idempotent release, missing credential fail-closed, caller-owned transport lifecycle).
The existing server endpoints (/server_info, /api/conversations, /api/conversations/{id}/events, /api/conversations/{id}/interrupt, /api/conversations/{id}/bash/*, /api/conversations/{id}/file/upload) all match the current agent-server router registrations. The /runtime and /runtime/credentials endpoints are not yet in the server — the PR description acknowledges this and references pending PRs (#4966/#3403, #4998/#5005, #5008). Since this is a draft, that coordination is expected.
No material code issues found. One minor observation below.
Minor Observation
Async lazy client init race (agent_server.py:167-168): In AsyncAgentServerClient._send, if self._http is None, a new httpx.AsyncClient is created inline. Two concurrent coroutines hitting _send before the first assignment completes could each create a client, leaking one. In practice this only happens on first use when no http_client is injected, and the leaked client is GC'd eventually — low impact. Consumers following the recommended pattern (injecting an http_client) are unaffected. Worth a note for awareness but not blocking.
Risk Assessment
🟢 LOW — Purely additive new module with no changes to existing SDK or server code. No agent behavior, prompt, or tool changes. The module is exported via openhands.sdk.client.__all__ but not yet via the top-level openhands.sdk.__all__, so it doesn't enter the SDK's tracked public API surface yet. The server-side runtime endpoints it targets are pending in separate PRs, which is appropriate for a draft.
Verdict
Code is clean and well-tested. No material findings. Leaving COMMENT (not APPROVE) because the PR is draft and the target server endpoints (/runtime, /runtime/credentials) haven't landed yet — the author's plan to validate against those PRs first is the right call.
|
The review was successfully posted to GitHub as an APPROVE review (ID: 5192223095) against commit GITHUB_REVIEW_POSTED Summary: Reviewed PR #5010 which extends
No material issues found. The code is clean, well-tested (287 tests passing), and doesn't touch agent behavior or eval-sensitive paths. Risk: 🟢 LOW. Approved. This comment was posted by an AI agent (OpenHands). |
Reuse StartConversationRequest and a shared connection initializer instead of selecting behavior from agent=None. Co-authored-by: openhands <openhands@all-hands.dev>
|
🤖 OpenHands is reviewing this PR. Head commit: This comment was posted by an AI agent (OpenHands). |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
Summary
This PR extends the SDK's remote conversation and workspace APIs to support automation use cases: explicit create()/attach() classmethods, set_title(), scoped runtime operations (start_command/get_command_output/get_runtime_session_key/release_runtime), in-memory bytes upload, async context manager support, and a runtime_conversation_id field that scopes file/command/git operations to a conversation's isolated runtime.
Assessment
Good taste — The refactoring is clean and well-motivated. Extracting _initialize_connection() eliminates duplicated setup across the constructor, create(), and attach(). The generator extraction in remote_workspace_mixin.py (splitting _start_command_generator, _search_command_output_generator, etc. out of _execute_command_generator) reduces duplication without adding abstraction layers. The api_prefix property is a simple, elegant way to route operations to either the host workspace or a conversation-scoped runtime.
Findings
No material issues found. A few things I verified:
create()vs__init__()payload parity:create()sendsStartConversationRequest.model_dump()which includes all fields the server needs (agent, workspace, max_iterations, hook_config, plugins, client_tools, tags, observability, etc.). The constructor's manual payload construction is the older path;create()correctly uses the canonical request model. The one field only on the constructor path (stuck_detection_thresholds) is not onStartConversationRequest, which is by design — the request model is the canonical surface._from_info()bypasses__init__viacls.__new__: This skips Pydantic validation but_initialize_connection()explicitly sets every required attribute (agent, workspace, _id, _state, _ws_client, _callbacks, etc.), anddelete_on_closedefaults toFalseas a class attribute. This is safe.- URL inconsistency in
_file_download_generator: The download/git URLs usef"{self.api_prefix}/..."(relative, resolved against the httpx client'sbase_url), while upload usesf"{self.host}{self.api_prefix}/..."(absolute). Both work correctly, and this is a pre-existing pattern — the PR only changed the hardcoded/apitoself.api_prefix. get_command_outputreturns only the latest event (limit=1, sort DESC): This is by design for polling completion status, as documented in the docstring.- No version bumps, no dependency changes, no event type modifications.
Risk Assessment
LOW — This PR adds new API surface without changing existing agent behavior, prompt templates, or tool execution logic. It is not in the eval-risk category. The refactoring preserves existing constructor behavior. Tests cover create/attach/no-fallback scenarios and scoped workspace operations across sync/async paths.
Validate the outgoing profile request with the canonical schema to prevent the live null-agent discriminator failure. Co-authored-by: openhands <openhands@all-hands.dev>
|
🤖 OpenHands is reviewing this PR. Head commit: This comment was posted by an AI agent (OpenHands). |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
Summary
This PR extends the SDK'''s conversation and workspace APIs for automation use cases: explicit create/attach classmethods on RemoteConversation, scoped runtime operations on remote workspaces, detached command lifecycle, and in-memory file upload. The refactoring of __init__ into a shared _initialize_connection is clean — all three entry points (constructor, create, attach) share the same connection setup, WebSocket lifecycle, and callback composition.
Findings
No material issues found. The design is sound:
create()/attach()separation:createsubmits aStartConversationRequest(with profile or agent),attachis read-only and raises on missing conversations. Neither falls back to the other — verified bytest_failed_explicit_operation_does_not_try_the_other_operation.exclude_none=Trueon the create payload correctly omits a nullagentwhen usingagent_profile_id, avoiding the server-side discriminator failure described in the PR.runtime_conversation_idfield is frozen (immutable) and defaults toNone, preserving existing/apibehavior when unset. Theapi_prefixproperty cleanly routes to/api/conversations/{id}when scoped.- Runtime lifecycle guards:
get_runtime_session_key/release_runtimeraiseValueErrorwhen no conversation scope is set. Release handles 404 idempotently. start_command/get_command_outputextract the generators already used byexecute_command, eliminating duplication.file_uploadacceptingbytesis backward compatible — existingstr | Pathcallers are unaffected.- Tests cover profile requests, agent type preservation on attach, no-fallback on failure, scoped vs unscoped operations across sync/async, runtime credential validation, and async context manager cleanup.
Risk Assessment
🟢 LOW — This is purely API surface extension for automation. No changes to agent behavior, prompt templates, tool execution, planning logic, memory/condenser behavior, or terminal handling. No eval-risk. No version bumps or dependency changes.
Automated review used the wrong decision (APPROVED instead of COMMENT) and is dismissed. Findings are reposted as a comment.
|
Code review for PR #5010 has been posted to GitHub as an APPROVED review (ID: 5192427387). Summary of findings: No material issues found. The PR cleanly extends the SDK's conversation and workspace APIs for automation:
Risk: 🟢 LOW — purely API surface extension, no changes to agent behavior or eval-relevant logic. This comment was posted by an AI agent (OpenHands). |
Co-authored-by: openhands <openhands@all-hands.dev>
|
🤖 OpenHands is reviewing this PR. Head commit: This comment was posted by an AI agent (OpenHands). |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
Summary
Clean refactor that extracts shared connection initialization into _initialize_connection, adds explicit RemoteConversation.create() / attach() classmethods for automation, scopes workspace operations via an immutable runtime_conversation_id, and extracts reusable command generators. No agent behavior, prompt, or tool-logic changes — purely API surface extension.
Assessment
No material findings. The implementation is sound:
_initialize_connectioncorrectly centralizes all shared setup (callbacks, WebSocket lifecycle, event sync, visualizer) used by both the legacy constructor and the newcreate/attachpaths.createserializes withexclude_none=True+expose_secrets: True— the right pattern for the request payload.attachis read-only: a single GET, no fallback to create, raises on missing/inaccessible conversations.runtime_conversation_idisfrozen=True(immutable after construction) — correct for a scope identifier._runtime_lifecycle_generatorgracefully handles 404 on release (already-gone runtime is safe).- The
LEGACY_CONVERSATIONS_PATH→CONVERSATIONS_PATHrename and removal of redundant private fields are internal-only with no public API impact. - Tests cover profile-based creation, read-only attachment, failed operations with no fallback, scoped/unscoped workspace operations across sync/async, and edge cases (empty credential, immutability).
Not in eval-risk category — no prompt templates, tool calling, planning loop, or condenser changes.
[RISK ASSESSMENT]
- Overall PR: 🟢 LOW — pure API/interface extension with no changes to agent behavior or execution paths.
VERDICT: ✅ Worth merging.
Automated review used the wrong decision (APPROVED instead of COMMENT) and is dismissed. Findings are reposted as a comment.
|
Review posted successfully to PR #5010 with APPROVE event. The PR extends existing conversation and workspace APIs for automation by:
No material findings — the implementation is clean, well-tested, and not in the eval-risk category. Risk assessment: 🟢 LOW. This comment was posted by an AI agent (OpenHands). |
Co-authored-by: openhands <openhands@all-hands.dev>
|
🤖 OpenHands is reviewing this PR. Head commit: This comment was posted by an AI agent (OpenHands). |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
Summary
This PR extends the SDK conversation and workspace APIs to support automation use cases: explicit create()/attach() class methods on RemoteConversation, runtime-scoped workspace operations via runtime_conversation_id, detached command lifecycle (start_command/get_command_output), runtime credential/release methods, in-memory byte uploads, and async workspace context cleanup.
The refactoring is clean: _initialize_connection() extracts the shared connection setup (agent, callbacks, WebSocket lifecycle, event sync) used by all three entry points (init, create, attach). The create() method correctly uses exclude_none=True to avoid serializing an unset agent field as null when agent_profile_id is used instead. The attach() method is read-only -- it GETs the conversation and never creates or updates -- and missing conversations raise rather than silently creating. Both failed operations are verified to not fall back to the other.
Runtime scoping is implemented via an immutable runtime_conversation_id field (frozen, UUID | None) and an api_prefix property that returns /api or /api/conversations/{id}. File, command, and Git request generators consistently use self.api_prefix, preserving existing host behavior when the scope is None.
The set_title method adds a simple PATCH endpoint. The AsyncRemoteWorkspace.aexit closes the HTTP client without releasing the runtime, which is the right default -- callers can explicitly call release_runtime() if needed.
Tests cover: profile-based creation with request validation, read-only attachment for both Agent and ACPAgent kinds, failed create/attach with no fallback, scoped vs unscoped file/command/git/runtime operations (sync and async), byte uploads, async context cleanup, frozen field validation, and missing runtime credential rejection.
Risk Assessment
LOW -- This PR extends API surface without changing agent behavior, prompt templates, tool calling, or planning logic. No eval-risk concern. No security issues (secrets are serialized with context expose_secrets True and exclude_none True). No breaking changes to existing constructor behavior. No dependency changes.
[RISK ASSESSMENT]
- Overall PR: LOW
- No material bugs, security problems, or design flaws found.
- The pre-existing URL inconsistency (download/git generators omit self.host while upload/command/runtime include it) is not introduced by this PR and works correctly because httpx joins relative URLs with base_url.
VERDICT: Worth merging.
Automated review used the wrong decision (APPROVED instead of COMMENT) and is dismissed. Findings are reposted as a comment.
|
Successfully reviewed and posted an APPROVE review for PR #5010 (feat(sdk): extend existing conversation and workspace APIs for automation). Review SummaryI examined all 6 changed files in detail:
Key Findings
The review was posted as APPROVE at commit This comment was posted by an AI agent (OpenHands). |
HUMAN:
This stack of PRs is working in my "software factory" prototype, so I have tested that it's working properly.
AGENT:
Why
This PR enables scheduled Automation workers to use conversations created from saved agent profiles and to run in conversation-owned local or Docker workspaces. A trusted Automation controller creates the conversation, while the isolated worker receives only the resulting conversation ID and runtime-scoped credential. The worker must then attach to the server-resolved agent without reconstructing its profile or receiving the controller's broader credentials. Creation and attachment therefore need distinct operations: creation submits the existing
StartConversationRequest, while attachment reads an existing conversation and must fail rather than silently create one when its ID is missing or inaccessible.The Agent Server already had host-level Bash, file, and Git APIs, and Python
RemoteWorkspacealready used them. Merged #4966 added conversation-scoped versions of those routes and taught the TypeScript clients to select them, but it did not complete the equivalent PythonRemoteWorkspacerouting. This PR supplies that missing Python client half: a controller addressing one runtime among many binds the workspace to an immutable conversation ID, while standalone servers and workers already running inside a single isolated runtime retain the existing unscoped behavior. This is route selection in the existing workspace abstraction, not a second runtime client. Automated behavioral-parity enforcement would have caught this omission in #4966; #5043 tracks adding that preferable repository-wide guard.An Automation run also has to outlive an individual scheduler request. The controller starts a worker command, persists the returned command ID, and polls that exact command on later passes; the existing blocking
execute_command()contains these operations internally but cannot provide that durable handoff. For Docker execution, the controller must obtain the conversation runtime's restricted session credential and release the container after verification without deleting its retained conversation history. #3403 supplies those Docker lifecycle endpoints; this PR exposes them through the same Python workspace interface. In-memory upload and async cleanup support the existing Automation execution path without adding another transport.The result is one Python SDK path for scheduled Automation in local and Docker environments: explicit profile-based creation, read-only attachment, conversation-scoped controller operations, durable command polling, scoped credential delivery, and runtime cleanup. Existing unscoped workspace operations remain valid for standalone Agent Servers and code already isolated inside a runtime, so they are not deprecated.
Summary
RemoteConversation.create(workspace, request)andRemoteConversation.attach(workspace, conversation_id). Creation submits the existing SDKStartConversationRequest, including a concrete agent or server profile. Attachment only reads an existing conversation; a missing or inaccessible conversation raises instead of creating one. Neither method falls back to the other.AgentBaseparameter. All entry points share the extracted connection initializer, callbacks, event synchronization, and WebSocket lifecycle. Use one canonical conversation route, removing the obsolete legacy name and duplicate private route state. Profile resolution and request validation remain in existing server/request machinery. Reuse messaging,run(blocking=False), state/events, and interruption; addset_titlefor persisted display names.runtime_conversation_idto existing remote workspaces. File, command, and Git requests use that scope; omitted scope preserves existing host behavior.start_commandandget_command_outputby extracting the request generators already used byexecute_command. Add scoped runtime credential/release methods, in-memory upload support, and async workspace context cleanup.AgentServerClient/RuntimeClientclasses, their sync/async variants, and the separate request module. Existing released constructor behavior remains compatible; no compatibility shim is needed for unpublished draft APIs.The server capabilities are already implemented or supplied by the runtime PRs. This PR fills Python interface gaps; it does not introduce another execution layer.
REST API contract changes
Compared with base OpenAPI
b5c8ab950401for public/api/**paths.Issue Number
Closes #5009
How to Test
After merging main’s #4966 at
e9392e7cf, 71 scoped-runtime, RemoteConversation, and RemoteWorkspace tests passed; this refresh changes no #5010 feature code. On sourcede5bc17e6: 283 conversation/workspace regressions passed after the internal route cleanup. The preceding functional change also passed 288 conversation/workspace/profile regressions. All changed-file hooks passed, including Ruff, Pyright, dynamic-attribute checks, and import boundaries.Coverage verifies profile request options, use of the returned agent, read-only attachment, failed creation/attachment with no fallback, and the unchanged scoped workspace operations. Automation and extension workers are migrated to the explicit methods.
Fresh live Canvas evidence (30-second GIF) uses SDK source
4bbab2dd0plus the recorded runtime prerequisites. The identical bundle completed real DeepSeek tasks locally and in Docker. Missing attachment made one GET, returned 404, and created no persisted conversation. Scoped file/Git operations, byte upload, detached commands, exact Docs #793 examples, and automatic runtime release passed. All private test services and workers stopped.The recording preserves the initial explicit-create failure: serializing an unset agent as null caused HTTP 500. Canonical
exclude_none=Trueserialization fixed it; the same fixture and docs then passed, and a regression validates the emitted request. Earlier constructor-based recordings are historical. The recording predatesde5bc17e6, which only renames the canonical route constant and removes redundant private route fields; endpoint URLs and public behavior are unchanged. Cleanup headde5bc17e6has 47 passing CI checks and an all-hands-bot verdict on that head of Worth merging, with no material findings. Its review is recorded as DISMISSED, so this is not claimed as a formal approval. The required HUMAN note and runtime release prerequisites remain. This validation does not replace the separate secret-scope/queue matrices or upstream release gates.Dependencies and review order
This PR targets main independently. Conversation-scoped operations use SDK #4966, now merged into main; Docker credential/release endpoints require SDK #3403. These are runtime/release prerequisites rather than native stack parents.
Release the SDK with these capabilities before merging the migrated Automation #449/#453 and extensions #570–#573. Documentation: OpenHands/docs #793.
Type
🐳 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
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimnikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:e9392e7-pythonRun
All tags pushed for this build
About Multi-Architecture Support
e9392e7-python) is a multi-arch manifest supporting both amd64 and arm64e9392e7-python-amd64) are also available if needed