Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export const fetchEnvironmentBoundedThreadSnapshot = Effect.fn(
group: "orchestration",
method: "GET",
url: (httpBaseUrl) =>
environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}/bounded`),
environmentEndpointUrl(
httpBaseUrl,
`/api/orchestration/threads/${encodeURIComponent(input.threadId)}/bounded`,
),
timeoutMs: input.timeoutMs ?? DEFAULT_BOUNDED_THREAD_SNAPSHOT_TIMEOUT_MS,
request: ({ client, headers }) =>
client.threadBoundedSnapshot({
Expand Down
45 changes: 45 additions & 0 deletions packages/client-runtime/src/state/environmentHttpAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ORCHESTRATION_PROTOCOL_HEADER,
ORCHESTRATION_PROTOCOL_VERSION_TEXT,
ProjectId,
ThreadId,
type AuthSessionState,
type OrchestrationV2ShellSnapshot,
OrchestrationV2ThreadDetailSnapshot,
Expand Down Expand Up @@ -307,6 +308,50 @@ describe("authenticated environment HTTP requests", () => {
}),
);

// MCP-created thread ids contain ":", which the request path percent-encodes.
// The DPoP proof must sign the URL that is actually sent, or the environment
// rejects it as a URL mismatch.
const MCP_THREAD_ID = ThreadId.make("mcp:3534bc83-1c17-4a1e-9118-601c2766d355");
const MCP_THREAD_LOADERS: ReadonlyArray<
Pick<(typeof LOADERS)[number], "name" | "response" | "load">
> = [
{
name: "thread snapshot",
response: encodeThreadSnapshot(THREAD),
load: (input: HttpInput) =>
ThreadSnapshotLoader.fetchEnvironmentThreadSnapshot({ ...input, threadId: MCP_THREAD_ID }),
},
{
name: "bounded thread snapshot",
response: encodeBoundedSnapshot(BOUNDED_THREAD),
load: (input: HttpInput) =>
fetchEnvironmentBoundedThreadSnapshot({ ...input, threadId: MCP_THREAD_ID }),
},
{
name: "older thread history",
response: THREAD_HISTORY,
load: (input: HttpInput) =>
fetchEnvironmentThreadHistoryPage({
...input,
threadId: MCP_THREAD_ID,
cursor: "older-page",
}),
},
];
it.effect.each(MCP_THREAD_LOADERS)(
"signs the sent URL for a $name of a thread id that needs encoding",
(loader) =>
Effect.gen(function* () {
const harness = makeHarness(() => Response.json(loader.response));
yield* loader.load(harness.input).pipe(Effect.provide(harness.httpLayer));

const sent = new URL(harness.calls[0]!.url);
expect(sent.pathname).toContain("/mcp%3A3534bc83-");
sent.search = "";
expect(harness.proofs.map((proof) => proof.url)).toEqual([sent.toString()]);
}),
);

it.effect("retries a rejected diff once with a new token, endpoint, and proof", () =>
Effect.gen(function* () {
const harness = makeHarness((requestNumber) =>
Expand Down
5 changes: 4 additions & 1 deletion packages/client-runtime/src/state/threadHistoryHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export const fetchEnvironmentThreadHistoryPage = Effect.fn(
group: "orchestration",
method: "GET",
url: (httpBaseUrl) =>
environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}/history`),
environmentEndpointUrl(
httpBaseUrl,
`/api/orchestration/threads/${encodeURIComponent(input.threadId)}/history`,
),
timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_HISTORY_TIMEOUT_MS,
request: ({ client, headers }) =>
client.threadHistoryPage({
Expand Down
5 changes: 4 additions & 1 deletion packages/client-runtime/src/state/threadSnapshotHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn(
group: "orchestration",
method: "GET",
url: (httpBaseUrl) =>
environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}`),
environmentEndpointUrl(
httpBaseUrl,
`/api/orchestration/threads/${encodeURIComponent(input.threadId)}`,
),
timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_SNAPSHOT_TIMEOUT_MS,
request: ({ client, headers }) =>
client.threadSnapshot({
Expand Down
Loading