molecule-core/workspace-server/internal/handlers
Hongming Wang e955597a98 feat(chat_files): rewrite Download as HTTP-forward (RFC #2312, PR-D)
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>
2026-04-29 15:19:02 -07:00
..
a2a_proxy_helpers.go feat(runtime): native_session skips a2a_queue enqueue — primitive #5 of 6 2026-04-26 23:34:04 -07:00
a2a_proxy_test.go merge: resolve staging conflicts (a2a_proxy + workspace_crud) 2026-04-26 10:43:22 -07:00
a2a_proxy.go feat(runtime): adapter-declared idle_timeout_override end-to-end 2026-04-26 22:38:01 -07:00
a2a_queue_test.go fix(platform/delegation): classify queued response + stitch drain result back 2026-04-26 10:14:19 -07:00
a2a_queue.go fix(platform/delegation): classify queued response + stitch drain result back 2026-04-26 10:14:19 -07:00
activity_since_secs_test.go feat(activity): accept ?since_secs= for time-window filtering (#2268) 2026-04-29 05:53:52 -07:00
activity_test.go fix(activity): include request/response bodies in ACTIVITY_LOGGED broadcast 2026-04-27 13:38:23 -07:00
activity.go feat(activity): accept ?since_secs= for time-window filtering (#2268) 2026-04-29 05:53:52 -07:00
admin_memories_test.go fix(handlers): unblock Platform (Go) CI — sqlmock budget-check + test loopback 2026-04-22 19:40:06 -07:00
admin_memories.go fix(org-api-tokens): add org_id column, close requireCallerOwnsOrg regression 2026-04-21 01:34:05 +00:00
admin_queue_test.go fix(handlers/admin_queue_test): wire sqlmock to make DropStale tests pass 2026-04-24 04:40:19 +00:00
admin_queue.go fix(handlers/admin_queue): remove unused db import 2026-04-24 02:22:16 +00:00
admin_schedules_health_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
admin_schedules_health.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
admin_test_token_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
admin_test_token.go fix(security): close IDOR gaps on /admin/test-token and /orgs/:id/allowlist 2026-04-20 23:29:27 +00:00
admin_workspace_images_test.go feat(platform/admin): /admin/workspace-images/refresh + Docker SDK + GHCR auth 2026-04-26 10:17:21 -07:00
admin_workspace_images.go feat(workspace-server): GHCR digest watcher closes runtime CD chain (#2114) 2026-04-26 13:36:26 -07:00
agent_git_identity_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
agent_git_identity.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
agent_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
agent.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
approvals_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
approvals.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
artifacts_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
artifacts.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
audit_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
audit.go fix: guard HMAC slice truncation in audit chain verification (fixes #1332) (#1339) 2026-04-21 07:52:11 +00:00
budget_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
budget.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
bundle.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
channels_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
channels.go chore: sync staging to main — 1188 commits, 5 conflicts resolved (#1743) 2026-04-23 18:30:18 +00:00
chat_files_test.go feat(chat_files): rewrite Download as HTTP-forward (RFC #2312, PR-D) 2026-04-29 15:19:02 -07:00
chat_files.go feat(chat_files): rewrite Download as HTTP-forward (RFC #2312, PR-D) 2026-04-29 15:19:02 -07:00
checkpoints_integration_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
checkpoints_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
checkpoints.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
config_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
config.go fix(security): cap webhook + config PATCH bodies (H3/H4) 2026-04-19 01:23:03 -07:00
container_files_delete_test.go chore: sync staging to main — 1188 commits, 5 conflicts resolved (#1743) 2026-04-23 18:30:18 +00:00
container_files_test.go fix(handlers): add empty/dot-only path guard to validateRelPath 2026-04-24 07:17:26 +00:00
container_files.go fix(tests): path validation before docker check + a2a queue mock in tests 2026-04-24 11:07:43 +00:00
delegation_test.go fix(platform/delegation): classify queued response + stitch drain result back 2026-04-26 10:14:19 -07:00
delegation.go fix(platform/delegation): classify queued response + stitch drain result back 2026-04-26 10:14:19 -07:00
discovery_test.go fix(discovery): isSafeURL guard on registered URLs (closes #1484) 2026-04-26 06:50:36 -07:00
discovery.go fix(discovery): isSafeURL guard on registered URLs (closes #1484) 2026-04-26 06:50:36 -07:00
events_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
events.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
external_connection.go feat(external-runtime): first-class BYO-compute workspaces + manifest-driven registry 2026-04-24 15:34:10 -07:00
github_token_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
github_token.go fix(go): replace $1 literal with resp.Body.Close() in 7 files (#1247) 2026-04-21 03:18:21 +00:00
handlers_additional_test.go chore(simplify): drop verbose comments + introduce DefaultMaxConcurrentTasks const 2026-04-26 11:07:00 -07:00
handlers_extended_test.go Fix TestExtended_WorkspaceDelete missing sqlmock expectations 2026-04-20 01:13:52 -07:00
handlers_test.go fix(a2a-proxy): log when A2A_IDLE_TIMEOUT_SECONDS is invalid 2026-04-26 15:57:00 -07:00
hermes_messages_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
hermes_messages.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
hibernation_test.go feat(platform): 409 guard on /hibernate when active_tasks > 0 (closes #822) 2026-04-18 12:09:52 -07:00
instructions.go fix(review): address code review blockers on tool-trace + instructions 2026-04-22 16:18:06 -07:00
mcp_test.go fix(security): backport SSRF defence (CWE-918) to main — isSafeURL in a2a_proxy.go (#1292) (#1302) 2026-04-21 07:06:42 +00:00
mcp_tools.go fix(restart): support SaaS control-plane provisioner (unblocks Platform Go build too) (#1512) 2026-04-21 22:56:01 +00:00
mcp.go fix: CWE-78 rm scope, go vet failures, delegation idempotency 2026-04-21 18:22:30 +00:00
memories_test.go test: GLOBAL memory delimiter spoofing escape + LOCAL scope untouched 2026-04-18 11:54:52 -07:00
memories.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
memory_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
memory.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
native_session_test.go feat(runtime): native_session skips a2a_queue enqueue — primitive #5 of 6 2026-04-26 23:34:04 -07:00
native_status_mgmt_test.go feat(runtime): native_status_mgmt skip — primitive #4 of 6 2026-04-26 23:13:13 -07:00
org_helpers.go fix: CWE-78 rm scope, go vet failures, delegation idempotency 2026-04-21 18:22:30 +00:00
org_import_force_removed_test.go fix(org-import): remove force=true bypass of required-env preflight 2026-04-29 03:23:23 -07:00
org_import.go merge: sync staging into feat/wire-max-concurrent-from-template-1408 2026-04-26 11:11:30 -07:00
org_include_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
org_include.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
org_path_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
org_plugin_allowlist_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
org_plugin_allowlist.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
org_prompt_ref_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
org_test.go feat(org-templates): add ux-ab-lab + manifest entry + schema smoke test 2026-04-24 16:22:14 -07:00
org_tokens_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
org_tokens.go fix(platform): unblock SaaS workspace registration end-to-end 2026-04-21 03:06:46 -07:00
org.go fix(org-import): remove force=true bypass of required-env preflight 2026-04-29 03:23:23 -07:00
plugins_install_pipeline_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
plugins_install_pipeline.go test(plugins): unblock TestResolveAndStage_NoInternalErrorsInHTTPErr (#1814) 2026-04-27 04:00:39 -07:00
plugins_install.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
plugins_listing.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
plugins_sources.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
plugins_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
plugins.go test(plugins): unblock TestResolveAndStage_NoInternalErrorsInHTTPErr (#1814) 2026-04-27 04:00:39 -07:00
registry_test.go feat(saas): deliver platform_inbound_secret via /registry/register (RFC #2312, PR-F) 2026-04-29 15:12:34 -07:00
registry.go feat(saas): deliver platform_inbound_secret via /registry/register (RFC #2312, PR-F) 2026-04-29 15:12:34 -07:00
restart_context_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
restart_context.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
restart_template_test.go fix(handlers): apply sanitizeRuntime allowlist before Tier 4 filepath.Join (CWE-22) 2026-04-24 11:37:19 +00:00
restart_template.go fix(handlers): apply sanitizeRuntime allowlist before Tier 4 filepath.Join (CWE-22) 2026-04-24 11:37:19 +00:00
runtime_overrides_test.go feat(runtime): native_scheduler skip — primitive #3 of 6 2026-04-26 22:47:00 -07:00
runtime_overrides.go feat(runtime): native_scheduler skip — primitive #3 of 6 2026-04-26 22:47:00 -07:00
runtime_provision_timeouts_test.go feat(workspace-server): surface provision_timeout_ms in workspace API (#2054 phase 2) 2026-04-26 06:37:45 -07:00
runtime_provision_timeouts.go refactor(handlers): apply simplify findings on PR #2094 2026-04-26 06:40:15 -07:00
runtime_registry_test.go feat(external-runtime): first-class BYO-compute workspaces + manifest-driven registry 2026-04-24 15:34:10 -07:00
runtime_registry.go feat(external-runtime): first-class BYO-compute workspaces + manifest-driven registry 2026-04-24 15:34:10 -07:00
schedules_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
schedules.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
secrets_test.go feat(canvas+platform): chat attachments, model selection, deploy/delete UX 2026-04-24 13:27:51 -07:00
secrets.go feat(canvas+platform): chat attachments, model selection, deploy/delete UX 2026-04-24 13:27:51 -07:00
security_regression_685_686_687_688_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
socket.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
sse_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
sse.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
ssrf_test.go test(ssrf): pin dev-mode RFC-1918 allow contract (follow-up to #2103) 2026-04-26 10:32:33 -07:00
ssrf.go fix(platform/ssrf): allow RFC-1918 in MOLECULE_ENV=development 2026-04-26 10:14:47 -07:00
team_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
team.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
template_files_eic_test.go feat(files-api): SSH-backed write for SaaS workspaces (fixes 500 docker not available) 2026-04-22 18:27:12 -07:00
template_files_eic.go feat(files-api): SSH-backed write for SaaS workspaces (fixes 500 docker not available) 2026-04-22 18:27:12 -07:00
template_import_test.go feat(quickstart): default new agents to T3 (Privileged) 2026-04-23 15:34:22 -07:00
template_import.go feat(quickstart): default new agents to T3 (Privileged) 2026-04-23 15:34:22 -07:00
templates_test.go fix(test): TestDeleteFile_WorkspaceNotFound uses relative path "old-file.txt" 2026-04-24 12:45:29 +00:00
templates.go refactor(handlers): apply simplify findings on PR #2094 2026-04-26 06:40:15 -07:00
terminal_test.go fix(terminal): check org_token_id context to allow org-token A2A routing (KI-005 followup) 2026-04-24 16:17:50 +00:00
terminal.go fix(terminal): check org_token_id context to allow org-token A2A routing (KI-005 followup) 2026-04-24 16:17:50 +00:00
tokens_sqlmock_test.go test(handlers): sqlmock coverage for tokens.go (closes #1819) 2026-04-26 02:50:42 -07:00
tokens_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
tokens.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
traces_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
traces.go fix(go): replace $1 literal with resp.Body.Close() in 7 files (#1247) 2026-04-21 03:18:21 +00:00
transcript_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
transcript.go fix(go): replace $1 literal with resp.Body.Close() in 7 files (#1247) 2026-04-21 03:18:21 +00:00
viewport_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
viewport.go fix(security): replace err.Error() with generic messages in handler responses (#1193) 2026-04-21 00:56:03 +00:00
webhooks_test.go fix(platform-go-ci): align test mocks with schema drift + org_id context contract (#1755) 2026-04-23 07:14:33 +00:00
webhooks_workflow_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
webhooks.go fix: multiple platform handler bug fixes 2026-04-20 05:01:01 +00:00
workspace_bootstrap_test.go feat(platform): bootstrap-failed + console endpoints for CP watcher 2026-04-20 17:11:34 -07:00
workspace_bootstrap.go fix(security): sanitize error details in BootstrapFailed, provision, and plugin install (#1219) 2026-04-21 02:11:38 +00:00
workspace_budget_test.go test(handlers): add 11th INSERT arg (max_concurrent_tasks) to remaining Create-handler mocks 2026-04-26 11:14:02 -07:00
workspace_crud.go fix(workspace_crud): drop restartStates entries on workspace delete (#2269) 2026-04-29 05:53:34 -07:00
workspace_metrics_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
workspace_metrics.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
workspace_preflight_test.go chore: code-review cleanup on today's shipped PRs 2026-04-20 16:04:57 -07:00
workspace_preflight.go chore: code-review cleanup on today's shipped PRs 2026-04-20 16:04:57 -07:00
workspace_provision_test.go Merge pull request #2166 from Molecule-AI/test/unblock-resolveandstage-test 2026-04-27 11:36:15 +00:00
workspace_provision.go feat(wsauth): platform→workspace inbound secret (RFC #2312, PR-A) 2026-04-29 14:09:33 -07:00
workspace_restart_coalesce_test.go fix(restart): clear running flag on panic in cycle() 2026-04-29 00:00:12 -07:00
workspace_restart_test.go chore: open-source restructure — rename dirs, remove internal files, scrub secrets 2026-04-18 00:24:44 -07:00
workspace_restart.go fix(restart): clear running flag on panic in cycle() 2026-04-29 00:00:12 -07:00
workspace_test.go test(handlers): add 11th INSERT arg (max_concurrent_tasks) to remaining Create-handler mocks 2026-04-26 11:14:02 -07:00
workspace.go test(provisioner): unblock TestProvisionWorkspaceCP_NoInternalErrorsInBroadcast (#1814) 2026-04-27 03:28:25 -07:00