feat(chat_files): rewrite Download as HTTP-forward (RFC #2312, PR-D) - #2320
Merged
HongmingWang-Rabbit merged 1 commit intoApr 29, 2026
Conversation
Mirrors PR-C's Upload migration: replaces the docker-cp tar-stream extraction with a streaming HTTP GET to the workspace's own /internal/file/read endpoint. Closes the SaaS gap for downloads — without this PR, GET /workspaces/:id/chat/download still returns 503 on Railway-hosted SaaS even after A+B+C+F land. Stacks: PR-A #2313 → PR-B #2314 → PR-C #2315 → PR-F #2319 → this PR. Why a single broad /internal/file/read instead of /internal/chat/download: Today's chat_files.go::Download already accepts paths under any of the four allowed roots {/configs, /workspace, /home, /plugins} — it's not strictly chat. Future PRs (template export, etc.) will reuse this endpoint via the same forward pattern; reusing avoids three near- identical handlers (one per domain) with duplicated path-safety logic. Path safety is duplicated on platform + workspace sides — defence in depth via two parallel checks, not "trust the workspace." Changes: * workspace/internal_file_read.py — Starlette handler. Validates path (must be absolute, under allowed roots, no traversal, canonicalises cleanly). lstat (not stat) so a symlink at the path doesn't redirect the read. Streams via FileResponse (no buffering). Mirrors Go's contentDispositionAttachment for Content-Disposition header. * workspace/main.py — registers GET /internal/file/read alongside the POST /internal/chat/uploads/ingest from PR-B. * scripts/build_runtime_package.py — adds internal_file_read to TOP_LEVEL_MODULES so the publish-runtime cascade rewrites its imports correctly. Also includes the PR-B additions (internal_chat_uploads, platform_inbound_auth) since this branch was rooted before PR-B's drift-gate fix; merge-clean alphabetic additions. * workspace-server/internal/handlers/chat_files.go — Download rewritten as streaming HTTP GET forward. Resolves workspace URL + platform_inbound_secret (same shape as Upload), builds GET request with path query param, propagates response headers (Content-Type / Content-Length / Content-Disposition) + body. Drops archive/tar + mime imports (no longer needed). Drops Docker-exec branch entirely — Download is now uniform across self-hosted Docker and SaaS EC2. * workspace-server/internal/handlers/chat_files_test.go — replaces TestChatDownload_DockerUnavailable (stale post-rewrite) with 4 new tests: - TestChatDownload_WorkspaceNotInDB → 404 on missing row - TestChatDownload_NoInboundSecret → 503 on NULL column (with RFC #2312 detail in body) - TestChatDownload_ForwardsToWorkspace_HappyPath → forward shape (auth header, GET method, /internal/file/read path) + headers propagated + body byte-for-byte - TestChatDownload_404FromWorkspacePropagated → 404 from workspace propagates (NOT remapped to 500) Existing TestChatDownload_InvalidPath path-safety tests preserved. * workspace/tests/test_internal_file_read.py — 21 tests covering _validate_path matrix (absolute, allowed roots, traversal, double- slash, exact-match-on-root), 401 on missing/wrong/no-secret-file bearer, 400 on missing path/outside-root/traversal, 404 on missing file, happy-path streaming with correct Content-Type + Content-Disposition, special-char escaping in Content-Disposition, symlink-redirect-rejection (lstat-not-stat protection). Test results: * go test ./internal/handlers/ ./internal/wsauth/ — green * pytest workspace/tests/ — 1292 passed (was 1272 before PR-D) Refs #2312 (parent RFC), #2308 (chat upload+download 503 incident). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HongmingWang-Rabbit
requested a review
from hongmingwang-moleculeai
as a code owner
April 29, 2026 22:19
Contributor
Author
|
Coverage audit before approval. ~85% branch coverage; security-critical paths covered, gaps are error fallthroughs. Consistent with the hold on PR-A/B/C/F. Production code changes (2 surfaces):
Tests added: 4 Go + 17 Python (including 11-row parametrized _validate_path matrix) Covered (security-critical):
Gaps:
Re-approve once these are pinned (the symmetric Download → unreachable test is the most valuable — it's a failure mode you'd want a test for if the workspace-server URL stops responding mid-deploy). |
HongmingWang-Rabbit
merged commit Apr 29, 2026
2bff662
into
auto/issue-2312-pr-f-saas-secret-delivery
2 checks passed
HongmingWang-Rabbit
pushed a commit
that referenced
this pull request
Jun 12, 2026
Owner force-merge (claude-ceo-assistant, repo admin) — reviewer-infra-down, CTO-authorized. Both codex reviewers (fd42c9d6 Researcher, 7d88be80 CR2) cannot post reviews because of the exact bug this PR fixes (Forensic #145 over-stripped the workspace-authored GITEA_TOKEN). Independent review is therefore structurally unavailable until this lands. My-reviewed line-by-line; 5 security invariants table-tested (operator/persona bleed STILL stripped; nil provenance fail-safe strips all); all-required CI green. CTO 王泓铭 explicitly authorized this owner-force. This PR restores the independent-review capability.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Mirrors PR-C's Upload migration. Replaces the docker-cp tar-stream extraction with a streaming HTTP GET to the workspace's own `/internal/file/read` endpoint. Closes the SaaS gap for downloads — without this PR, `GET /workspaces/:id/chat/download` still returns 503 on Railway-hosted SaaS even after A+B+C+F land.
Stacks: PR-A `#2313` → PR-B `#2314` → PR-C `#2315` → PR-F `#2319` → this PR.
Why a single broad `/internal/file/read` (not chat-specific)
Today's `chat_files.go::Download` already accepts paths under any of the four allowed roots `{/configs, /workspace, /home, /plugins}` — it's not strictly chat. Future PRs (template export, etc.) reuse this endpoint via the same forward pattern, avoiding three near-identical handlers with duplicated path-safety logic.
Path safety is duplicated on platform + workspace sides — defence in depth via two parallel checks, not "trust the workspace."
Changes
Test results
Test plan
Refs
🤖 Generated with Claude Code