| 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 | 40 | group: "orchestration", |
| 41 | 41 | method: "GET", |
| 42 | 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 | 47 | timeoutMs: input.timeoutMs ?? DEFAULT_BOUNDED_THREAD_SNAPSHOT_TIMEOUT_MS, |
| 45 | 48 | request: ({ client, headers }) => |
| 46 | 49 | client.threadBoundedSnapshot({ |
packages/client-runtime/src/state/environmentHttpAuth.test.ts+45| ... | ... | @@ -4,6 +4,7 @@ import { |
| 4 | 4 | ORCHESTRATION_PROTOCOL_HEADER, |
| 5 | 5 | ORCHESTRATION_PROTOCOL_VERSION_TEXT, |
| 6 | 6 | ProjectId, |
| 7 | ThreadId, | |
| 7 | 8 | type AuthSessionState, |
| 8 | 9 | type OrchestrationV2ShellSnapshot, |
| 9 | 10 | OrchestrationV2ThreadDetailSnapshot, |
| ... | ... | @@ -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 | 355 | it.effect("retries a rejected diff once with a new token, endpoint, and proof", () => |
| 311 | 356 | Effect.gen(function* () { |
| 312 | 357 | const harness = makeHarness((requestNumber) => |
packages/client-runtime/src/state/threadHistoryHttp.ts+4-1| ... | ... | @@ -30,7 +30,10 @@ export const fetchEnvironmentThreadHistoryPage = Effect.fn( |
| 30 | 30 | group: "orchestration", |
| 31 | 31 | method: "GET", |
| 32 | 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 | 37 | timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_HISTORY_TIMEOUT_MS, |
| 35 | 38 | request: ({ client, headers }) => |
| 36 | 39 | client.threadHistoryPage({ |
packages/client-runtime/src/state/threadSnapshotHttp.ts+4-1| ... | ... | @@ -66,7 +66,10 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn( |
| 66 | 66 | group: "orchestration", |
| 67 | 67 | method: "GET", |
| 68 | 68 | url: (httpBaseUrl) => |
| 69 | environmentEndpointUrl(httpBaseUrl, `/api/orchestration/threads/${input.threadId}`), | |
| 69 | environmentEndpointUrl( | |
| 70 | httpBaseUrl, | |
| 71 | `/api/orchestration/threads/${encodeURIComponent(input.threadId)}`, | |
| 72 | ), | |
| 70 | 73 | timeoutMs: input.timeoutMs ?? DEFAULT_THREAD_SNAPSHOT_TIMEOUT_MS, |
| 71 | 74 | request: ({ client, headers }) => |
| 72 | 75 | client.threadSnapshot({ |