| author | |
| committer | |
| log | a1db449fe4fecafcb473340771163e791b7fb3d6 |
| tree | c29ff9a3bc0276d6b5ee45ab0404c68082ffb596 |
| parent | 454b94a13aea918f27bb060d8d054d4cfb791bc2 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>4 files changed, 57 insertions(+), 3 deletions(-)
packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts+4-1| ... | @@ -40,7 +40,10 @@ export const fetchEnvironmentBoundedThreadSnapshot = Effect.fn( | ... | @@ -40,7 +40,10 @@ export const fetchEnvironmentBoundedThreadSnapshot = Effect.fn( |
| 40 | group: "orchestration", | 40 | group: "orchestration", |
| 41 | method: "GET", | 41 | method: "GET", |
| 42 | url: (httpBaseUrl) => | 42 | url: (httpBaseUrl) => |
| 43 | environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}/bounded`), | 43 | environmentEndpointUrl( |
| 44 | httpBaseUrl, | ||
| 45 | `/api/orchestration/threads/${encodeURIComponent(input.threadId)}/bounded`, | ||
| 46 | ), | ||
| 44 | timeoutMs: input.timeoutMs ?? DEFAULT_BOUNDED_THREAD_SNAPSHOT_TIMEOUT_MS, | 47 | timeoutMs: input.timeoutMs ?? DEFAULT_BOUNDED_THREAD_SNAPSHOT_TIMEOUT_MS, |
| 45 | request: ({ client, headers }) => | 48 | request: ({ client, headers }) => |
| 46 | client.threadBoundedSnapshot({ | 49 | client.threadBoundedSnapshot({ |
packages/client-runtime/src/state/environmentHttpAuth.test.ts+45| ... | @@ -4,6 +4,7 @@ import { | ... | @@ -4,6 +4,7 @@ import { |
| 4 | ORCHESTRATION_PROTOCOL_HEADER, | 4 | ORCHESTRATION_PROTOCOL_HEADER, |
| 5 | ORCHESTRATION_PROTOCOL_VERSION_TEXT, | 5 | ORCHESTRATION_PROTOCOL_VERSION_TEXT, |
| 6 | ProjectId, | 6 | ProjectId, |
| 7 | ThreadId, | ||
| 7 | type AuthSessionState, | 8 | type AuthSessionState, |
| 8 | type OrchestrationV2ShellSnapshot, | 9 | type OrchestrationV2ShellSnapshot, |
| 9 | OrchestrationV2ThreadDetailSnapshot, | 10 | OrchestrationV2ThreadDetailSnapshot, |
| ... | @@ -307,6 +308,50 @@ describe("authenticated environment HTTP requests", () => { | ... | @@ -307,6 +308,50 @@ describe("authenticated environment HTTP requests", () => { |
| 307 | }), | 308 | }), |
| 308 | ); | 309 | ); |
| 309 | 310 | ||
| 311 | // MCP-created thread ids contain ":", which the request path percent-encodes. | ||
| 312 | // The DPoP proof must sign the URL that is actually sent, or the environment | ||
| 313 | // rejects it as a URL mismatch. | ||
| 314 | const MCP_THREAD_ID = ThreadId.make("mcp:3534bc83-1c17-4a1e-9118-601c2766d355"); | ||
| 315 | const MCP_THREAD_LOADERS: ReadonlyArray< | ||
| 316 | Pick<(typeof LOADERS)[number], "name" | "response" | "load"> | ||
| 317 | > = [ | ||
| 318 | { | ||
| 319 | name: "thread snapshot", | ||
| 320 | response: encodeThreadSnapshot(THREAD), | ||
| 321 | load: (input: HttpInput) => | ||
| 322 | ThreadSnapshotLoader.fetchEnvironmentThreadSnapshot({ ...input, threadId: MCP_THREAD_ID }), | ||
| 323 | }, | ||
| 324 | { | ||
| 325 | name: "bounded thread snapshot", | ||
| 326 | response: encodeBoundedSnapshot(BOUNDED_THREAD), | ||
| 327 | load: (input: HttpInput) => | ||
| 328 | fetchEnvironmentBoundedThreadSnapshot({ ...input, threadId: MCP_THREAD_ID }), | ||
| 329 | }, | ||
| 330 | { | ||
| 331 | name: "older thread history", | ||
| 332 | response: THREAD_HISTORY, | ||
| 333 | load: (input: HttpInput) => | ||
| 334 | fetchEnvironmentThreadHistoryPage({ | ||
| 335 | ...input, | ||
| 336 | threadId: MCP_THREAD_ID, | ||
| 337 | cursor: "older-page", | ||
| 338 | }), | ||
| 339 | }, | ||
| 340 | ]; | ||
| 341 | it.effect.each(MCP_THREAD_LOADERS)( | ||
| 342 | "signs the sent URL for a $name of a thread id that needs encoding", | ||
| 343 | (loader) => | ||
| 344 | Effect.gen(function* () { | ||
| 345 | const harness = makeHarness(() => Response.json(loader.response)); | ||
| 346 | yield* loader.load(harness.input).pipe(Effect.provide(harness.httpLayer)); | ||
| 347 | |||
| 348 | const sent = new URL(harness.calls[0]!.url); | ||
| 349 | expect(sent.pathname).toContain("/mcp%3A3534bc83-"); | ||
| 350 | sent.search = ""; | ||
| 351 | expect(harness.proofs.map((proof) => proof.url)).toEqual([sent.toString()]); | ||
| 352 | }), | ||
| 353 | ); | ||
| 354 | |||
| 310 | it.effect("retries a rejected diff once with a new token, endpoint, and proof", () => | 355 | it.effect("retries a rejected diff once with a new token, endpoint, and proof", () => |
| 311 | Effect.gen(function* () { | 356 | Effect.gen(function* () { |
| 312 | const harness = makeHarness((requestNumber) => | 357 | const harness = makeHarness((requestNumber) => |
packages/client-runtime/src/state/threadHistoryHttp.ts+4-1| ... | @@ -30,7 +30,10 @@ export const fetchEnvironmentThreadHistoryPage = Effect.fn( | ... | @@ -30,7 +30,10 @@ export const fetchEnvironmentThreadHistoryPage = Effect.fn( |
| 30 | group: "orchestration", | 30 | group: "orchestration", |
| 31 | method: "GET", | 31 | method: "GET", |
| 32 | url: (httpBaseUrl) => | 32 | url: (httpBaseUrl) => |
| 33 | environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}/history`), | 33 | environmentEndpointUrl( |
| 34 | httpBaseUrl, | ||
| 35 | `/api/orchestration/threads/${encodeURIComponent(input.threadId)}/history`, | ||
| 36 | ), | ||
| 34 | timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_HISTORY_TIMEOUT_MS, | 37 | timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_HISTORY_TIMEOUT_MS, |
| 35 | request: ({ client, headers }) => | 38 | request: ({ client, headers }) => |
| 36 | client.threadHistoryPage({ | 39 | client.threadHistoryPage({ |
packages/client-runtime/src/state/threadSnapshotHttp.ts+4-1| ... | @@ -66,7 +66,10 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn( | ... | @@ -66,7 +66,10 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn( |
| 66 | group: "orchestration", | 66 | group: "orchestration", |
| 67 | method: "GET", | 67 | method: "GET", |
| 68 | url: (httpBaseUrl) => | 68 | url: (httpBaseUrl) => |
| 69 | environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}`), | 69 | environmentEndpointUrl( |
| 70 | httpBaseUrl, | ||
| 71 | `/api/orchestration/threads/${encodeURIComponent(input.threadId)}`, | ||
| 72 | ), | ||
| 70 | timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_SNAPSHOT_TIMEOUT_MS, | 73 | timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_SNAPSHOT_TIMEOUT_MS, |
| 71 | request: ({ client, headers }) => | 74 | request: ({ client, headers }) => |
| 72 | client.threadSnapshot({ | 75 | client.threadSnapshot({ |