feat(routes): enforce x-vellum-client-id ownership on host-transfer routes; add target_client_id to tool#29434
Conversation
…outes; add target_client_id to tool
|
@codexbot review |
|
@devin-ai-integration[bot] please review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 738f014ebf
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const submittingClientId = (headers as Record<string, string>)["x-vellum-client-id"]?.trim() || undefined; | ||
| if (!submittingClientId) throw new BadRequestError("x-vellum-client-id header required for targeted transfer"); | ||
| if (submittingClientId !== targetClientId) throw new ForbiddenError(`Client "${submittingClientId}" is not the owner of this transfer`); |
There was a problem hiding this comment.
Coordinate targeted transfer header enforcement with clients
These new targeted checks require x-vellum-client-id for transfer GET/PUT/result, but the current macOS transfer client does not send that header on transfer calls (clients/shared/Network/HostProxyClient.swift postTransferResult, pullTransferContent, pushTransferContent), while bash/file/cu calls do. As soon as target_client_id is used, /transfers/:transferId/content and /host-transfer-result will return 400 and the transfer cannot complete. Please ship client header propagation in lockstep or gate this enforcement until transfer callers include the header.
Useful? React with 👍 / 👎.
| const targetClientId = | ||
| typeof input.target_client_id === "string" && input.target_client_id !== "" | ||
| ? input.target_client_id | ||
| : undefined; |
There was a problem hiding this comment.
Reject targeted transfer when no host-file client is connected
target_client_id is parsed here, but if that client disconnects before execution, HostTransferProxy.instance.isAvailable() can be false and the code falls through to executeLocal(...), silently doing a local filesystem copy instead of a host-client transfer. For targeted requests this is incorrect behavior and can write to daemon/container paths unexpectedly; this path should return an explicit error when a target is specified but no capable client is connected.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
🚩 Pre-existing: resolveTransferContentGetHeaders calls getTransferContent after handler already consumed the entry
The HTTP adapter (http-adapter.ts:107-124) calls the handler first, then resolves responseHeaders. For the GET transfer content route, this means handleTransferContentGet calls getTransferContent(transferId) which deletes the transfer entry (host-transfer-proxy.ts:454), then resolveTransferContentGetHeaders calls getTransferContent again — but the entry is already deleted, so it returns null and the response falls back to bare Content-Type: application/octet-stream without Content-Length or X-Transfer-SHA256 headers. This is a pre-existing issue not introduced by this PR, but the new getTargetClientIdForTransfer call added in this PR runs non-destructively before the consuming call, so it doesn't worsen the situation.
(Refers to lines 65-84)
Was this helpful? React with 👍 or 👎 to provide feedback.
| expect( | ||
| handleTransferContentPut({ | ||
| pathParams: { transferId: TEST_TRANSFER_ID }, | ||
| headers: { "x-transfer-sha256": "abc" }, | ||
| rawBody: new Uint8Array(Buffer.from("data")), | ||
| }), | ||
| ).rejects.toBeInstanceOf(BadRequestError); |
There was a problem hiding this comment.
🟡 Missing await on .rejects assertions causes PUT handler error-type tests to always pass
The handleTransferContentPut handler is async, so thrown errors become rejected promises. At lines 243-249 and 272-278, expect(promise).rejects.toBeInstanceOf(...) is called without await. In bun:test (same as Jest), .rejects returns a Promise that must be awaited — without it, the assertion result is silently discarded and the test passes regardless of whether the handler throws the expected error type. The rest of the codebase consistently uses await expect(...).rejects.toThrow(...) (e.g., assistant/src/__tests__/host-cu-proxy.test.ts:718, assistant/src/__tests__/host-proxy-base.test.ts:146). The companion side-effect tests (lines 252-264, 281-292) properly await the handler and verify guards work, so the security guards are partially covered, but the specific error-type verification is ineffective.
| expect( | |
| handleTransferContentPut({ | |
| pathParams: { transferId: TEST_TRANSFER_ID }, | |
| headers: { "x-transfer-sha256": "abc" }, | |
| rawBody: new Uint8Array(Buffer.from("data")), | |
| }), | |
| ).rejects.toBeInstanceOf(BadRequestError); | |
| await expect( | |
| handleTransferContentPut({ | |
| pathParams: { transferId: TEST_TRANSFER_ID }, | |
| headers: { "x-transfer-sha256": "abc" }, | |
| rawBody: new Uint8Array(Buffer.from("data")), | |
| }), | |
| ).rejects.toBeInstanceOf(BadRequestError); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| expect( | ||
| handleTransferContentPut({ | ||
| pathParams: { transferId: TEST_TRANSFER_ID }, | ||
| headers: { "x-vellum-client-id": "client-B", "x-transfer-sha256": "abc" }, | ||
| rawBody: new Uint8Array(Buffer.from("data")), | ||
| }), | ||
| ).rejects.toBeInstanceOf(ForbiddenError); |
There was a problem hiding this comment.
🟡 Missing await on .rejects assertion for ForbiddenError test case
Same issue as the BadRequestError test: expect(promise).rejects.toBeInstanceOf(ForbiddenError) at line 272-278 is not awaited, so the assertion is never actually verified. The test will pass even if the handler throws a different error type or doesn't throw at all.
| expect( | |
| handleTransferContentPut({ | |
| pathParams: { transferId: TEST_TRANSFER_ID }, | |
| headers: { "x-vellum-client-id": "client-B", "x-transfer-sha256": "abc" }, | |
| rawBody: new Uint8Array(Buffer.from("data")), | |
| }), | |
| ).rejects.toBeInstanceOf(ForbiddenError); | |
| await expect( | |
| handleTransferContentPut({ | |
| pathParams: { transferId: TEST_TRANSFER_ID }, | |
| headers: { "x-vellum-client-id": "client-B", "x-transfer-sha256": "abc" }, | |
| rawBody: new Uint8Array(Buffer.from("data")), | |
| }), | |
| ).rejects.toBeInstanceOf(ForbiddenError); |
Was this helpful? React with 👍 or 👎 to provide feedback.
| if ( | ||
| targetClientId == null && | ||
| context.transportInterface != null && | ||
| !supportsHostProxy(context.transportInterface) && | ||
| assistantEventHub.listClientsByCapability("host_file").length > 1 | ||
| ) { | ||
| return { content: `Error: multiple clients support host_file. Specify which client to use with \`target_client_id\`. Run \`assistant clients list --capability host_file\` to see client IDs and labels.`, isError: true }; | ||
| } |
There was a problem hiding this comment.
🚩 Ambiguity guard skipped for macOS-originated transfers with multiple host_file clients
The multi-client ambiguity guard at assistant/src/tools/host-filesystem/transfer.ts:100-107 checks !supportsHostProxy(context.transportInterface) using the no-arg overload, which returns true only for "macos". This means macOS-originated transfers skip the disambiguation error when multiple host_file clients are connected. In that scenario, the proxy's auto-resolve logic (host-transfer-proxy.ts:134-136) only sets resolvedTargetClientId when exactly one capable client exists — with multiple clients, the broadcast goes untargeted. This is likely intentional (macOS clients are themselves host-proxy capable, and typically only one macOS client would be connected per assistant), but worth confirming the design handles the two-macOS-clients edge case as expected.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 738f014ebf
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const targetClientId = match.proxy.getTargetClientIdForTransfer(transferId); | ||
| if (targetClientId != null) { | ||
| const submittingClientId = (headers as Record<string, string>)["x-vellum-client-id"]?.trim() || undefined; | ||
| if (!submittingClientId) throw new BadRequestError("x-vellum-client-id header required for targeted transfer"); |
There was a problem hiding this comment.
Keep targeted transfer routes compatible with current clients
This new header requirement breaks targeted host transfers end-to-end because the current client transfer paths do not send x-vellum-client-id: postTransferResult, pullTransferContent, and pushTransferContent call GatewayHTTPClient without that header (clients/shared/Network/HostProxyClient.swift lines 131-174), and GatewayHTTPClient.get has no extra-header parameter to add it. As soon as target_client_id is used (new tool input), the GET/PUT/result routes now return 400 for the legitimate target client, so targeted transfers fail rather than being secured.
Useful? React with 👍 / 👎.
… on targetClientId
|
@codexbot review |
|
@devin-ai-integration[bot] please review |
649621f
into
Credence/targeted-host-proxy-phase3
|
Codex Review: Didn't find any major issues. Keep it up! ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
…sfer (#29440) * feat(host-proxy): add targetClientId support to HostTransferProxy and message envelopes (#29431) * feat(host-proxy): add targetClientId support to HostTransferProxy and message envelopes * fix(tests): add listClientsByCapability and getClientById to host-transfer-proxy mock --------- Co-authored-by: credence-the-bot <credence@vellum.ai> * feat(routes): enforce x-vellum-client-id ownership on host-transfer routes; add target_client_id to tool (#29434) * feat(routes): enforce x-vellum-client-id ownership on host-transfer routes; add target_client_id to tool * fix(tests): await .rejects assertions; guard executeLocal fallthrough on targetClientId --------- Co-authored-by: credence-the-bot <credence@vellum.ai> * feat(macos): targetClientId acceptance guard and x-vellum-client-id headers for host_transfer (#29435) * feat(macos): targetClientId acceptance guard and x-vellum-client-id headers for host_transfer * fix(swift): use setValue (not addValue) for extraHeaders in GatewayHTTPClient.get --------- Co-authored-by: credence-the-bot <credence@vellum.ai> --------- Co-authored-by: credence-the-bot[bot] <277301654+credence-the-bot[bot]@users.noreply.github.com> Co-authored-by: credence-the-bot <credence@vellum.ai>
Summary
Part of plan: targeted-host-proxy-phase3.md (PR 2 of 3)