Skip to content

fix(ts-client): keep binary bytes in RemoteWorkspace downloads - #5762

Open
enyst wants to merge 3 commits into
mainfrom
claude/umbrella-bug-fixes-tgz9ai-ts-binary-download
Open

enyst wants to merge 3 commits into
mainfrom
claude/umbrella-bug-fixes-tgz9ai-ts-binary-download

Conversation

@enyst

@enyst enyst commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

HUMAN:
This PR proposes a fix for a bug uncovered during the first full agentic run of verify-agent-server.


AGENT:

Why

RemoteWorkspace treated binary downloads as UTF-8 text, changing invalid UTF-8 bytes before returning or saving them.

Summary

 fileDownload: HTTP application/octet-stream
- automatic response decoding → string → Blob (bytes may change)
+ responseType: 'blob'        → Blob (original bytes)

 bytes ff 00 80 fe 41 → fileDownload / downloadAsBlob → same 5 bytes
 text callers        → downloadAsText() or Blob.text()

Request a Blob explicitly in the workspace download path. Binary content and file size survive unchanged; text conversion remains available through the text helper.

Source: remote-workspace.ts.

Issue Number

Fixes #5650

How to Test

Previously recorded verification
  • cd clients/typescript && npm ci && npm run lint && npx tsc --noEmit && npm test && npm run format:check: lint 0 errors (7 pre-existing warnings), typecheck clean, 384 tests pass. The new test fails on main (blob holds 239, 191, 189, ...) and passes here.
  • Integration: npx vitest run --config vitest.integration.config.ts workspace.integration.test.ts -t "File Download" against a local agent-server: 4 passed.
  • Map proof (verify-agent-server, real agent server and built client):
    • main at dfd8fe8: control-agent-server map run --file F34 --only F34.workspace-binary-download --fresh --repeat 2 --checkout <main checkout> reports xfail in both attempts.
    • this branch (before dropping the marker): same command without --checkout reports xpass in both attempts.
    • after dropping the marker: control-agent-server map check is ok and map run --file F34 --only F34.workspace-binary-download --fresh reports pass.

Video/Screenshots

n/a

Type

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

Notes

The declared string | Blob return type already allows a Blob, but all downloads now return one, including text files. Callers relying on a string must convert explicitly; the release-note decision remains relevant. The alternative of returning a string only for valid UTF-8 was not chosen. FileClient’s array-buffer path is unaffected. Supersedes #5676; umbrella #5653.

Visual explanation by Codex using HumanLayer show-me.


🐳 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 python-node-runtime Link
python-minimal amd64, arm64 python-node-runtime Link
python amd64, arm64 python-node-runtime 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:67aeb7f-python

Run

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

All tags pushed for this build

ghcr.io/openhands/agent-server:67aeb7f-golang-amd64
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-golang-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-golang-amd64
ghcr.io/openhands/agent-server:67aeb7f-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:67aeb7f-golang-arm64
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-golang-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-golang-arm64
ghcr.io/openhands/agent-server:67aeb7f-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:67aeb7f-java-amd64
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-java-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-java-amd64
ghcr.io/openhands/agent-server:67aeb7f-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:67aeb7f-java-arm64
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-java-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-java-arm64
ghcr.io/openhands/agent-server:67aeb7f-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:67aeb7f-python-amd64
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-python-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-python-amd64
ghcr.io/openhands/agent-server:67aeb7f-python-node-runtime-amd64
ghcr.io/openhands/agent-server:67aeb7f-python-arm64
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-python-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-python-arm64
ghcr.io/openhands/agent-server:67aeb7f-python-node-runtime-arm64
ghcr.io/openhands/agent-server:67aeb7f-python-minimal-amd64
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-python-minimal-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-python-minimal-amd64
ghcr.io/openhands/agent-server:67aeb7f-python-node-runtime-minimal-amd64
ghcr.io/openhands/agent-server:67aeb7f-python-minimal-arm64
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-python-minimal-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-python-minimal-arm64
ghcr.io/openhands/agent-server:67aeb7f-python-node-runtime-minimal-arm64
ghcr.io/openhands/agent-server:67aeb7f-python-slim-amd64
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-python-slim-amd64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-python-slim-amd64
ghcr.io/openhands/agent-server:67aeb7f-python-node-runtime-slim-amd64
ghcr.io/openhands/agent-server:67aeb7f-python-slim-arm64
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-python-slim-arm64
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-python-slim-arm64
ghcr.io/openhands/agent-server:67aeb7f-python-node-runtime-slim-arm64
ghcr.io/openhands/agent-server:67aeb7f-golang
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-golang
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-golang
ghcr.io/openhands/agent-server:67aeb7f-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:67aeb7f-java
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-java
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-java
ghcr.io/openhands/agent-server:67aeb7f-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:67aeb7f-python-minimal
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-python-minimal
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-python-minimal
ghcr.io/openhands/agent-server:67aeb7f-python-node-runtime-minimal
ghcr.io/openhands/agent-server:67aeb7f-python-slim
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-python-slim
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-python-slim
ghcr.io/openhands/agent-server:67aeb7f-python-node-runtime-slim
ghcr.io/openhands/agent-server:67aeb7f-python
ghcr.io/openhands/agent-server:67aeb7ff3187077959780434e70ebcedbe5edb69-python
ghcr.io/openhands/agent-server:claude-umbrella-bug-fixes-tgz9ai-ts-binary-download-python
ghcr.io/openhands/agent-server:67aeb7f-python-node-runtime

About Multi-Architecture Support

  • Each variant tag (e.g., 67aeb7f-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., 67aeb7f-python-amd64) are also available if needed

RemoteWorkspace.fileDownload let the HTTP client parse the
application/octet-stream body in 'auto' mode, which decodes it as UTF-8
text, so fileDownload, downloadAsBlob and downloadAndSave replaced every
invalid byte with U+FFFD. Request a Blob instead, add a regression test
with a non-UTF-8 payload, and drop the known-bug marker from
F34.workspace-binary-download.

Fixes #5650

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MKndVj7XWvRQmvW9w2bDg
@enyst enyst self-assigned this Oct 13, 2026
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

…ests

fileDownload now always returns the raw bytes as a Blob, so the
integration tests read the text back with Blob.text().

Fixes #5650

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MKndVj7XWvRQmvW9w2bDg

@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 posted by an AI agent (OpenHands).

Scope

clients/typescript/ owns the browser-compatible TypeScript client for the Agent Server API, and this change stays inside it (plus the verify-agent-server map). The fix targets the TS client, so the scope gate passes and no repository move is needed.

Verified on head 07b46e0

  • The behavior change is correct and minimal: fileDownload now requests responseType: 'blob', so the existing instanceof Blob branch sets both content and file_size from the raw bytes. Both supported transports preserve the bytes — the browser path (HttpClient.parseResponse -> response.blob()) and the Node fetch path (parseNodeResponse -> new Blob([new Uint8Array(buffer)])) — so the fix holds beyond the mocked test.
  • The regression test asserts the exact non-UTF-8 bytes and file_size 5 through both downloadAsBlob and fileDownload.
  • Locally: remote-workspace-download.test.ts 5/5, full npm test 384/384, npm run lint 0 errors (7 pre-existing warnings), npx tsc --noEmit clean, npm run format:check clean.
  • Current-head CI for 07b46e0 is fully green.
  • Linked issue #5650 is open with bug + ready-for-dev and describes exactly this symptom; the head addresses its acceptance criterion.

Material finding

The public contract changed without updating the docs that describe it. fileDownload().content now returns a Blob for every file (text included), where callers previously received a string. This is the change the PR itself flags as a "Decision needed" item, and it alters the runtime return shape of a public method on a published client (@openhands/typescript-client). The consumer-facing docs were not updated and still imply a string / a two-argument file-writing call:

  • clients/typescript/README.md (~line 168) shows fileDownload('/remote/path/file.txt', './downloaded-file.txt') — a destinationPath argument the method does not accept (fileDownload(sourcePath)).
  • clients/typescript/README.md (~line 341) lists fileDownload(sourcePath, destinationPath).

A caller who follows the README and reads result.content as a string gets [object Blob] — the same silent-corruption class this PR fixes. Please either update those README entries to the Blob-always contract (pointing string callers at downloadAsText() / await content.text()) and record whether a release note is warranted, or confirm the accepted decision in the PR so a maintainer can settle the doc and release-note follow-up.

Note (not blocking)

Per the repository review guide, a fix whose symptom is only observable through the running service should carry authentic before-and-after runtime evidence. The PR body supplies that (the control-agent-server map run ... F34.workspace-binary-download run reporting xfail on main and pass on this head), but it lives only in the PR description, so I could not re-run it here. The unit and integration tests are sound regression protection regardless.

No other correctness, security, or compatibility issue found in the changed lines.

🔄 CHANGES REQUESTED

params: { path: sourcePath },
timeout: 60000,
// Keep the raw bytes: 'auto' would decode the octet-stream body as UTF-8 text.
responseType: 'blob',

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 changes fileDownload().content to a Blob for every file, text included — a public contract change the PR flags as "Decision needed". The code path is correct on both transports, but the consumer-facing docs still describe the old shape: clients/typescript/README.md (~line 168) calls fileDownload('/remote/path/file.txt', './downloaded-file.txt') with a destinationPath argument this method does not accept, and the method list (~line 341) documents fileDownload(sourcePath, destinationPath). A caller who reads result.content as a string per the README now gets [object Blob]. Please update those README entries to the Blob-always contract and point string callers at downloadAsText() / await content.text(), and record whether the contract change needs a release note.

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

Copy link
Copy Markdown
Contributor

📁 PR Artifacts Notice

This PR contains a .pr/ directory with temporary PR-specific documents. The directory will be automatically removed after this PR merges to main through a cleanup PR.

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.

[Bug]: TS RemoteWorkspace.downloadAsBlob/fileDownload return 11 mangled bytes for a 5-byte binary file

3 participants