Skip to content

fix(client): subagent threads open over T3 Connect again - #15813

Closed
Mnigos wants to merge 2 commits into
pingdotgg:mainfrom
Mnigos:relay-signed-url-matches-request
Closed

Mnigos wants to merge 2 commits into
pingdotgg:mainfrom
Mnigos:relay-signed-url-matches-request

Conversation

@Mnigos

@Mnigos Mnigos commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15772

Problem

Over T3 Connect, every request carries a DPoP proof that signs the request URL. For thread snapshots, bounded snapshots and older history, the client signed a URL built by pasting the raw thread id into the path. The request itself goes through Effect's HttpApiClient, which runs path params through encodeURIComponent.

For a UUID the two URLs are the same. Delegated-task ids (thread:delegated-task:command%3Amcp%3A…) contain : and %, so the signed path kept them as-is while the sent path carried thread%3A…%253A…. The server's normalizeDpopHtu comparison failed with url_mismatch, and opening a subagent thread returned 401. Other ids with :, such as provider child threads, hit the same mismatch.

Change

boundedThreadSnapshotHttp.ts, threadSnapshotHttp.ts and threadHistoryHttp.ts now build the signed URL with makeEnvironmentHttpApiUrlBuilder, the contract-derived builder that pullRequestDiffHttp.ts already uses. It encodes the path the same way the request does, so the signed and sent URLs cannot drift apart.

The history URL now includes the cursor query. The web and mobile signers and the server all strip query and fragment before comparing, so the proof does not change.

Only T3 Connect relay connections sign requests. Local, LAN/Tailscale cookie and bearer connections are unaffected. Web, desktop and mobile share this code in packages/client-runtime.

Verification

New cases in environmentHttpAuth.test.ts run each loader (snapshot, bounded, history) with a delegated-task id and a UUID. Each one sends a real HttpApiClient request to a mocked fetch, then checks that the signed URL equals the URL fetch received. It also checks the exact encoded path after normalizeDpopHtu.

Case Main This branch
Delegated-task id, all 3 loaders Signed …/threads/thread:delegated-task:command%3Amcp%3Arequest-1/bounded, sent …/threads/thread%3Adelegated-task%3Acommand%253Amcp%253Arequest-1/bounded Signed URL equals sent URL
UUID id, snapshot and bounded Equal Equal
UUID id, history Differs only by the ?cursor= query, which signers and the server strip Equal
Gate Command Result
Tests vp test run packages/client-runtime/src/state/environmentHttpAuth.test.ts packages/client-runtime/src/state/boundedThreadSnapshotHttp.test.ts 43/43 pass
Typecheck vp run typecheck in packages/client-runtime Pass
Format vp fmt --check on the 4 changed files Pass
Lint vp lint on the 4 changed files Pass
Knip vp run knip:check Pass

With the old source and the new tests, 6 cases fail: the 3 delegated-id cases and 3 history cases that differ only by the stripped query.

No screenshot or video: the bug only happens through the T3 Connect relay. The evidence is the comparison of the signed and sent URLs in the tests.

Not checked: a live relay connection, so no in-client capture. That the relay proxy keeps the encoded path as sent comes from the reporter's log. The signer is mocked in tests, so real proof signing and server acceptance are not exercised. Encoded cursors and a retry with a delegated id are not covered.

Implemented with Claude Opus 5.5, verified with GPT-6 Astra, coordinated by Claude Fable 5.1 in Claude Code.

Over T3 Connect each request carries a DPoP proof that signs the request URL.
The thread snapshot, bounded snapshot and history loaders signed a URL built by
interpolating the raw thread id, while the request itself percent-encodes the
path parameter. Ids containing `:` or `%`, such as delegated-task ids, made
the signed and requested URLs differ, and the server rejected the proof with
url_mismatch.

The loaders now build the signed URL with the same contract-derived builder
the request uses, so the two cannot diverge.
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Oct 5, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The PR is a focused fix that aligns DPoP-signed URLs with the encoded URLs actually sent for thread snapshots and history, with targeted coverage for delegated-task IDs. Because it changes authentication-bound request construction across production paths, human validation is warranted.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: pingdotgg/t3code/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4f26a49f-8508-4f7e-aa5e-dd215d97d4df
📥 Commits

Reviewing files that changed from the base of the PR and between efecd3c and a11a68d.

📒 Files selected for processing (4)
  • packages/client-runtime/src/state/boundedThreadSnapshotHttp.ts
  • packages/client-runtime/src/state/environmentHttpAuth.test.ts
  • packages/client-runtime/src/state/threadHistoryHttp.ts
  • packages/client-runtime/src/state/threadSnapshotHttp.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The thread snapshot, bounded snapshot, and history requests now build orchestration URLs with the HTTP API URL builder. Authentication tests cover URL signing for encoded thread IDs and UUIDs.

Changes

Thread request URL alignment

Layer / File(s) Summary
Build orchestration URLs and verify signing
packages/client-runtime/src/state/*ThreadSnapshotHttp.ts, packages/client-runtime/src/state/threadHistoryHttp.ts, packages/client-runtime/src/state/environmentHttpAuth.test.ts
The three request paths use builder-generated orchestration URLs, with thread IDs supplied as route parameters and the history cursor supplied as a query parameter. Tests check DPoP proof URLs against request URLs for special-character thread IDs and UUIDs.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: juliusmarminge

Merge Risk: ⚪ Minimal · up to a11a6

No actionable issue is established for this change. It is ready to merge after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to a11a6

The change aligns authentication proofs with the thread URLs actually requested. Existing authentication and read permissions remain in place, and no new bypass was identified. Production relay behavior was not verified end to end.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The effective exposure addressed is thread snapshot and history reads within the selected environment by an authenticated principal with orchestration read scope. Previously failing encoded identifiers become usable, but the inspected path does not grant additional scopes, credentials, or cross-environment authority.

Trust Boundaries and Controls

  • observed — Server verification continues to bind the incoming request to the session proof key and access token, checking signature, method, normalized URL, token hash, time, and replay. The thread handlers independently retain orchestration read-scope enforcement. Encoding alignment restores passage through these controls rather than removing them.

Resilience and Maintainability Implications

  • observed — The bounded loader retains its existing terminal outcomes and compatibility recovery: structured resource-not-found becomes missing, unsupported or invalid bounded responses can fall back to the full snapshot, and other failures become unavailable. The URL change does not introduce a persistent write, reservation, or new ownership transition.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: restoring subagent thread access over T3 Connect.
Description check ✅ Passed The description covers the problem, change, linked issue and scope, and focused verification results. It also identifies checks that were not performed.
Linked Issues check ✅ Passed Issue #15772 requires matching the signed URL to the encoded request path in the bounded snapshot, snapshot, and history loaders. All three now use makeEnvironmentHttpApiUrlBuilder with the route pa…
Out of Scope Changes check ✅ Passed The changes are limited to the three loaders named in issue #15772 and tests for their signed URL behavior. The history query update supports matching the URL built for that request. No unrelated chan…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 4…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@juliusmarminge

Copy link
Copy Markdown
Member

Note

Grok responding on behalf of Julius.

Thanks for this! The same fix (encoding the thread id in the DPoP-signed snapshot, bounded snapshot, and history URLs) just landed on main in #17599, which closed #15772, so I'm closing this one as superseded. If there's test coverage here that #17599 doesn't have, a small follow-up PR adding it is welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: T3 Connect relay returns 401 (DPoP url_mismatch) when opening subagent threads

2 participants