Before submitting
Area
apps/mobile (root cause in packages/client-runtime)
Steps to reproduce
- Pair the mobile app with an environment so its HTTP requests use DPoP authorization.
- Create a thread whose id contains a character that
encodeURIComponent escapes. Threads created through the MCP t3_thread_launch tool get ids like mcp:7dbb5e0c-….
- Open that thread on mobile, and if it has enough history, tap Load earlier activity.
Expected behavior
Earlier activity loads, the same as it does for threads with UUID ids.
Actual behavior
The request fails and the feed shows The environment rejected this client's credentials (invalid_credential). The client refreshes the credential and retries once, and the retry also gets a 401. The bounded snapshot request (/bounded) for the same thread fails the same way.
Server trace, mobile requests only, grouped by route, by whether the thread id needed escaping, and by status:
5 /bounded escaped-id=false dpop=true 200
10 /bounded escaped-id=true dpop=true 401
14 /history escaped-id=true dpop=true 401
Cause: the DPoP htu signed by the client doesn't match the URL the server checks.
- The client builds the URL it signs with the raw id:
environmentEndpointUrl(httpBaseUrl, /api/orchestration/threads/${input.threadId}/history). WHATWG URL leaves : unescaped in a path, so the signed htu is …/threads/mcp:7dbb…/history.
- The HttpApi client sends the request with the path param percent-encoded:
…/threads/mcp%3A7dbb…/history.
- The server verifies the proof against the real request URL (
apps/server/src/auth/dpop.ts:73, url: url.value.href). normalizeDpopHtu only strips the query and fragment, so the two never match.
new URL("https://h/api/orchestration/threads/mcp:7dbb/history").toString()
// https://h/api/orchestration/threads/mcp:7dbb/history
new URL("https://h/api/orchestration/threads/" + encodeURIComponent("mcp:7dbb") + "/history").toString()
// https://h/api/orchestration/threads/mcp%3A7dbb/history
The same pattern appears in all three thread URL builders:
packages/client-runtime/src/state/threadHistoryHttp.ts:31
packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts:38
packages/client-runtime/src/state/threadSnapshotHttp.ts:69
Suggested fix: use encodeURIComponent(input.threadId) in those builders so the signed URL matches the wire URL. Alternatively, have the client sign the exact URL it sends.
Impact
Major degradation or frequent failure. Every thread with such an id can't page history or load a bounded snapshot on mobile. Desktop and threads with UUID ids are unaffected.
Version or commit
main @ 5afe18e (code paths confirmed there); mobile app build T3Code/109
Environment
iOS (CFNetwork/3896, Darwin 27.0.0), environment reached directly over Tailscale HTTPS with DPoP auth; server on Linux (WSL2)
Workaround
Open the affected threads on desktop.
Before submitting
Area
apps/mobile (root cause in
packages/client-runtime)Steps to reproduce
encodeURIComponentescapes. Threads created through the MCPt3_thread_launchtool get ids likemcp:7dbb5e0c-….Expected behavior
Earlier activity loads, the same as it does for threads with UUID ids.
Actual behavior
The request fails and the feed shows
The environment rejected this client's credentials (invalid_credential).The client refreshes the credential and retries once, and the retry also gets a 401. The bounded snapshot request (/bounded) for the same thread fails the same way.Server trace, mobile requests only, grouped by route, by whether the thread id needed escaping, and by status:
Cause: the DPoP
htusigned by the client doesn't match the URL the server checks.environmentEndpointUrl(httpBaseUrl,/api/orchestration/threads/${input.threadId}/history). WHATWGURLleaves:unescaped in a path, so the signedhtuis…/threads/mcp:7dbb…/history.…/threads/mcp%3A7dbb…/history.apps/server/src/auth/dpop.ts:73,url: url.value.href).normalizeDpopHtuonly strips the query and fragment, so the two never match.The same pattern appears in all three thread URL builders:
packages/client-runtime/src/state/threadHistoryHttp.ts:31packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts:38packages/client-runtime/src/state/threadSnapshotHttp.ts:69Suggested fix: use
encodeURIComponent(input.threadId)in those builders so the signed URL matches the wire URL. Alternatively, have the client sign the exact URL it sends.Impact
Major degradation or frequent failure. Every thread with such an id can't page history or load a bounded snapshot on mobile. Desktop and threads with UUID ids are unaffected.
Version or commit
main @ 5afe18e (code paths confirmed there); mobile app build T3Code/109
Environment
iOS (CFNetwork/3896, Darwin 27.0.0), environment reached directly over Tailscale HTTPS with DPoP auth; server on Linux (WSL2)
Workaround
Open the affected threads on desktop.