Skip to content

feat(routes): enforce x-vellum-client-id ownership on host-transfer routes; add target_client_id to tool#29434

Merged
credence-the-bot[bot] merged 2 commits into
Credence/targeted-host-proxy-phase3from
run-plan/host-transfer-xc/pr-2
May 3, 2026
Merged

feat(routes): enforce x-vellum-client-id ownership on host-transfer routes; add target_client_id to tool#29434
credence-the-bot[bot] merged 2 commits into
Credence/targeted-host-proxy-phase3from
run-plan/host-transfer-xc/pr-2

Conversation

@credence-the-bot
Copy link
Copy Markdown
Contributor

@credence-the-bot credence-the-bot Bot commented May 3, 2026

Summary

  • Add x-vellum-client-id ownership validation to GET/PUT content and POST result host-transfer routes (mirrors Phase 2 host-file/host-cu pattern)
  • Add target_client_id to host_file_transfer tool schema with multi-client ambiguity guard
  • Add route ownership tests in host-transfer-routes-targeted.test.ts
  • Regen openapi.yaml to include 400/403 additionalResponses on transfer routes

Part of plan: targeted-host-proxy-phase3.md (PR 2 of 3)


Open in Devin Review

@credence-the-bot
Copy link
Copy Markdown
Contributor Author

@codexbot review

@credence-the-bot
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration[bot] please review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +47 to +49
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`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +95 to +98
const targetClientId =
typeof input.target_client_id === "string" && input.target_client_id !== ""
? input.target_client_id
: undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 4 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

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.

🚩 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)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +243 to +249
expect(
handleTransferContentPut({
pathParams: { transferId: TEST_TRANSFER_ID },
headers: { "x-transfer-sha256": "abc" },
rawBody: new Uint8Array(Buffer.from("data")),
}),
).rejects.toBeInstanceOf(BadRequestError);
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.

🟡 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.

Suggested change
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);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +272 to +278
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);
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.

🟡 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.

Suggested change
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);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +100 to +107
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 };
}
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.

🚩 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@credence-the-bot
Copy link
Copy Markdown
Contributor Author

@codexbot review

@credence-the-bot
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration[bot] please review

@credence-the-bot credence-the-bot Bot merged commit 649621f into Credence/targeted-host-proxy-phase3 May 3, 2026
13 checks passed
@credence-the-bot credence-the-bot Bot deleted the run-plan/host-transfer-xc/pr-2 branch May 3, 2026 18:42
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

ℹ️ 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".

noanflaherty pushed a commit that referenced this pull request May 3, 2026
…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>
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.

0 participants