Skip to content

feat(client): serve conversation workspace static files - #244

Open
VascoSch92 wants to merge 1 commit into
mainfrom
vasco/conversation-workspace-serving
Open

feat(client): serve conversation workspace static files#244
VascoSch92 wants to merge 1 commit into
mainfrom
vasco/conversation-workspace-serving

Conversation

@VascoSch92

Copy link
Copy Markdown
Member

Summary

Split from #231 (one feature per PR). Adds client coverage for the agent-server's static workspace-serving routes, which the audit reported as missing.

Endpoint Status before Change
GET /api/conversations/{id}/workspace missing new ConversationClient.getWorkspaceRoot()
GET /api/conversations/{id}/workspace/{path} missing new ConversationClient.getWorkspaceFile()

Details

  • Both methods return the raw bytes as a Blob (call .text() for HTML/text) so binary artifacts survive intact, matching FileClient.downloadTrajectory.
  • getWorkspaceRoot serves the workspace root index.html; getWorkspaceFile serves a workspace-relative file (nested paths supported). The agent-server only serves local workspaces — non-local workspaces, a missing directory/file, or the absence of index.html yield 404; path traversal is rejected with 400.

Testing

  • Unit (src/__tests__/api-clients.test.ts, mocked fetch): both routes are GET, return a Blob, and round-trip text content; asserts the exact URLs (including the nested path).
  • Integration (deterministic-api.integration.test.ts): writes a root index.html and a nested file into the mounted workspace, serves both back verbatim through the static routes, and asserts 404 for a missing file.
  • npm run build, lint (0 errors), and format:check pass; full unit suite green.

Adds client coverage for the agent-server's static workspace routes:

- GET /api/conversations/{id}/workspace          -> getWorkspaceRoot()
- GET /api/conversations/{id}/workspace/{path}   -> getWorkspaceFile()

Both return the raw bytes as a Blob (call .text() for HTML/text) so
binary artifacts survive intact, mirroring FileClient.downloadTrajectory.
Includes a mocked unit test (both routes, blob round-trip) and an
integration test that round-trips a written root index.html and a nested
file, and asserts 404 for a missing file.
@github-actions github-actions Bot added the type: feat A new feature label Jun 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Endpoint audit

❌ 8 off-contract call(s) — not on the agent-server · classifiers: cloud

Category Count
❌ Off-contract (not on agent-server) 8
  ⛔ no known backend 7
  ↗️ served by cloud 1
➕ Missing API (agent-server has, client lacks) 7
agent-server endpoints 112
client endpoints 111

❌ Not on agent-server (gated, 8)

⛔ (no known backend) — served by no backend we can see (7)

  • DELETE /api/meta-profiles/{}
  • GET /api/meta-profiles
  • GET /api/meta-profiles/{}
  • POST /api/cloud-proxy
  • POST /api/meta-profiles/{}
  • POST /api/meta-profiles/{}/activate
  • POST /api/skills/installed/{}/update

↗️ served by cloud (1)

  • GET /api/shared-events/search

➕ Missing API — agent-server exposes it, client does not implement (7)

  • GET /
  • GET /api/bash/bash_events
  • GET /api/conversations
  • GET /api/conversations/{}/events
  • GET /api/init
  • POST /api/init
  • POST /api/skills/installed/{}/refresh

@all-hands-bot

Copy link
Copy Markdown
Contributor

🤖 OpenHands is reviewing this PR.

Head commit: dfbb8338086305ef0fbaddf58e39636db9eba601
View the conversation: https://oss-agent-canvas.ngrok.dev/conversations/8ae9b279-37b7-4e71-a81e-76aba2e00c5a

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

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

Copy link
Copy Markdown
Contributor

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 change cleanly adds client coverage for the agent-server's static workspace-serving routes (getWorkspaceRoot / getWorkspaceFile), returning raw bytes as a Blob consistent with FileClient.downloadTrajectory. The unit and integration test coverage is appropriate (round-trip content, nested paths, 404 for missing files). No material bugs block merge.

Risk assessment

Low. The change is additive (two GET methods), reads-only, and scoped to existing server routes. The one correctness gap below (#/? in filePath) only affects files whose names contain those characters, which is uncommon in practice but worth fixing for robustness.

Findings

  • getWorkspaceFile does not percent-encode filePath (see inline comment). A filename containing # or ? is silently truncated by the URL constructor in HttpClient.request, so the server receives the wrong path. Encoding each path segment with encodeURIComponent and joining on / resolves this and keeps nested paths working.
  • Minor: the integration test's 404 assertion relies on getWorkspaceFile throwing. The try/catch sets missingStatus only inside catch, so if the call ever succeeded the expect(missingStatus).toBe(404) would fail with undefined - that's the intended guard, just noting it behaves as expected today.

*/
async getWorkspaceFile(conversationId: string, filePath: string): Promise<Blob> {
const response = await this.client.get<Blob>(
`/api/conversations/${conversationId}/workspace/${filePath}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

filePath is interpolated raw into the URL. The HttpClient builds the final URL with new URL(relativePath, baseUrl + '/'), so any # or ? in a filename is interpreted as a fragment/query separator and the path is silently truncated (e.g. weird#name.txt becomes weird). Consider encoding each segment and joining on /, e.g. filePath.split('/').map(encodeURIComponent).join('/'), so nested paths and unusual filenames are preserved. The rest of this client (e.g. FileClient.downloadTrajectory) uses encodeURIComponent for path components; applying the same here keeps the encoding contract consistent across the SDK.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants