Skip to content

Fix CodeRouter trusted tenant exchange - #9607

Merged
lawrencecchen merged 5 commits into
mainfrom
fix/coderouter-trusted-exchange
Aug 5, 2026
Merged

lawrencecchen merged 5 commits into
mainfrom
fix/coderouter-trusted-exchange

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Moves the trusted hosted-tenant exchange request into the API route so the deployed function always sends the scoped capabilities and dedicated control credential. This fixes production CLI login failures caused by a stale bundled hosted client omitting the control header.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Moves the trusted hosted-tenant exchange into API routes and switches the CLI to a semantic endpoint. Always forwards scoped capabilities and the stack control token header to fix login failures from stale clients.

  • Bug Fixes
    • Added POST /api/subrouter/tenant-exchange that posts to /_subrouter/auth/stack with Authorization, x-subrouter-stack-control-token, capability flags, and team info; uses no-store caching, propagates upstream errors, and returns parsed tenant JSON.
    • Updated CLI config to set exchangeUrl to /api/subrouter/tenant-exchange and cache-control to no-store on the config response.
    • Read hosted credentials via validated env (SUBROUTER_HOSTED_URL, SUBROUTER_STACK_TENANT_DELETE_TOKEN); return 503 if missing.

Written for commit 935611b. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added tenant-aware subrouter exchange support for CLI integrations.
    • Tenant capabilities and identity are securely forwarded during exchanges.
    • Added permission checks and validation for required service configuration.
  • Bug Fixes

    • Updated CLI configuration to use the tenant-specific exchange endpoint.
    • Improved propagation of upstream errors and unexpected failures.
    • Exchange responses are now uncached to ensure current configuration is used.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The CLI config now advertises /api/subrouter/tenant-exchange with no-store caching. The new POST route authorizes tenant context, validates service configuration, forwards capabilities and identity to the hosted stack, and propagates responses and errors.

Changes

Hosted exchange flow

Layer / File(s) Summary
Exchange authorization and configuration
web/app/api/subrouter/tenant-exchange/route.ts, web/services/subrouter/hostedClient.ts
The dynamic Node.js POST route resolves request context, enforces use-or-manage access, validates the control token and hosted URL, and derives tenant capabilities. The hosted client reads the tenant deletion token from the imported configuration.
Direct hosted endpoint exchange
web/app/api/subrouter/tenant-exchange/route.ts
The route forwards tenant capabilities and identity to the hosted stack, preserves upstream errors, disables caching, and converts exceptions through the shared error handler.
CLI exchange endpoint wiring
web/app/api/cli/config/route.ts, web/tests/cli-config-route.test.ts
The CLI configuration response now uses /api/subrouter/tenant-exchange and no-store caching. Tests verify the URL and cache policy.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RequestContext
  participant TenantExchangePOST
  participant HostedStackEndpoint
  RequestContext->>TenantExchangePOST: resolve context and authorize use-or-manage
  TenantExchangePOST->>HostedStackEndpoint: POST tenant capabilities and identity
  HostedStackEndpoint-->>TenantExchangePOST: return status and response body
  TenantExchangePOST-->>RequestContext: return JSON response or propagated error
Loading

Possibly related PRs

  • manaflow-ai/cmux#9099: This PR uses the shared subrouter request-context infrastructure introduced there.
  • manaflow-ai/cmux#9261: This PR extends the hosted Subrouter migration with the tenant exchange endpoint and CLI configuration changes.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux User-Facing Error Privacy ❌ Error The production tenant-exchange route returns the raw upstream body for non-2xx responses (lines 45-50), exposing upstream messages or payloads to API users. Replace the raw body with a generic sanitized error response, and keep upstream details only in sanitized server logs or internal telemetry.
Description check ⚠️ Warning The description explains the change and reason, but it omits the required Testing, Demo Video, Review Trigger, and Checklist sections. Add the missing template sections and record testing, manual verification, review triggers, and checklist completion.
✅ Passed checks (23 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed The pull request changes only four TypeScript files; no Swift files or Swift actor-isolation changes appear in the diff.
Cmux Swift Blocking Runtime ✅ Passed The PR diff contains only web TypeScript and test files; it introduces or modifies no Swift code or blocking runtime primitives.
Cmux Browser Automation Off-Main ✅ Passed The PR changes only four web files; no Sources/TerminalController.swift or socket policy/router files changed, so it introduces no browser automation routing or worker-lane changes.
Cmux Expensive Synchronous Load ✅ Passed The commit changes four TypeScript files only; it contains no Swift production changes or Swift diff to assess for expensive synchronous loads.
Cmux Cache Substitution Correctness ✅ Passed The PR adds a transient API exchange and changes responses to no-store; it does not replace a fresh read with a cache in persistence, history, undo, or snapshot paths.
Cmux No Hacky Sleeps ✅ Passed The full PR diff adds no sleep, timer, polling, fixed delay, or wall-clock synchronization; runtime changes use direct async request handling only.
Cmux Algorithmic Complexity ✅ Passed The added production lines only change env access, a header, a URL, and cache policy; structural scans found no new loops, sorting, filtering, joins, or batch rescans.
Cmux Swift Concurrency ✅ Passed The diff changes only four TypeScript files under web/ and contains no Swift code, so it introduces no cmux-owned Swift concurrency patterns.
Cmux Swift @Concurrent ✅ Passed The commit changes only four TypeScript files and includes no Swift diff, so the Swift @concurrent check is not applicable.
Cmux Swift Package Boundaries ✅ Passed The diff contains only four web TypeScript/test files and no changed Swift files, so the Swift package boundary rule does not apply.
Cmux Swiftpm Lockfiles ✅ Passed The PR changes only four web TypeScript/test files; it contains no Package.swift, Package.resolved, .gitignore, Xcode project, workflow, or dependency changes covered by the rule.
Cmux Swift Logging ✅ Passed The patch changes only TypeScript and test files; it adds or materially changes no Swift logging, so the Swift logging rule is not applicable.
Cmux Full Internationalization ✅ Passed The PR adds only machine-facing API routes, protocol tokens, URLs, headers, and cache behavior; it introduces no localized user-facing text or message/catalog changes.
Cmux Swiftui State Layout ✅ Passed The PR changes only four TypeScript files. The cumulative diff contains no Swift or SwiftUI paths or SwiftUI state/layout constructs.
Cmux Architecture Rethink ✅ Passed The patch changes only TypeScript API, service, and test files; it contains no Swift or Swift lifecycle changes covered by this check.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR diff contains only four web TypeScript/test files and no Swift, window, or shortcut changes; the auxiliary-window rule is not applicable.
Cmux Source Artifacts ✅ Passed All four changed paths are tracked TypeScript source or test files; the diff adds no logs, caches, temp folders, build output, screenshots, or other artifact paths.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The PR changes only four web TypeScript files; the diff contains no Swift files under production Sources, so this Swift-only check is not applicable.
Cmux No Ambient Global State ✅ Passed The check applies only to production Swift changes; the patch changes four TypeScript files and contains no Swift files.
Title check ✅ Passed The title clearly identifies the trusted tenant exchange fix, which is the main change in the pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/coderouter-trusted-exchange

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/app/api/subrouter/exchange/route.ts`:
- Around line 15-47: Add a behavior-level regression test for the exchange route
before changing implementation, with the initial commit containing only the
failing test; then place the route fix in a second commit. The test should
verify the authorization and control headers, capability derivation from
resolved team permissions, no-store fetch behavior, and mapping of upstream
failures. Anchor the test around the route handler and its existing upstream
fetch path.
- Around line 34-48: Update the upstream request in the exchange route around
fetch and response-body reading to use SUBROUTER_STACK_AUTH_TIMEOUT_MS with a
cancellation-aware AbortSignal covering both operations. Detect an abort caused
by this timeout and return the existing safe exchange error, while preserving
current handling for non-timeout failures.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cdc19a1a-2dd2-4e18-8c69-8a600017e442

📥 Commits

Reviewing files that changed from the base of the PR and between 4cf7cc3 and 4528ac0.

📒 Files selected for processing (1)
  • web/app/api/subrouter/exchange/route.ts

Comment thread web/app/api/subrouter/exchange/route.ts Outdated
Comment on lines +15 to +47
// Keep the trusted exchange request in this route. Older cached server
// bundles delegated to a legacy client which omitted both the control
// credential and scoped capabilities.
const controlToken =
process.env.SUBROUTER_STACK_TENANT_DELETE_TOKEN?.trim();
const hostedUrl = process.env.SUBROUTER_HOSTED_URL?.trim().replace(
/\/+$/,
"",
);
if (!controlToken || !hostedUrl) {
return Response.json(
{ error: "service_unavailable" },
{ status: 503 },
);
}
const capabilities = [
...(resolved.value.team.manageAccounts ? ["manage_accounts"] : []),
...(resolved.value.team.use ? ["use"] : []),
];
const upstream = await fetch(`${hostedUrl}/_subrouter/auth/stack`, {
method: "POST",
headers: {
authorization: `Bearer ${resolved.value.accessToken}`,
"content-type": "application/json",
"x-subrouter-stack-control-token": controlToken,
},
body: JSON.stringify({
capabilities,
teamId: resolved.value.team.teamId,
teamName: resolved.value.team.teamName,
}),
cache: "no-store",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the hosted-exchange regression test before merging.

The supplied PR contains no behavior-level regression test for the production login failure. Add a first commit that fails when the route omits the control header or scoped capabilities. Add the route fix in the second commit. Cover the authorization header, control header, capability derivation, no-store behavior, and upstream failure mapping.

As per coding guidelines, “Regression fixes should use two commits: the first adds only the failing test, and the second adds the fix.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/api/subrouter/exchange/route.ts` around lines 15 - 47, Add a
behavior-level regression test for the exchange route before changing
implementation, with the initial commit containing only the failing test; then
place the route fix in a second commit. The test should verify the authorization
and control headers, capability derivation from resolved team permissions,
no-store fetch behavior, and mapping of upstream failures. Anchor the test
around the route handler and its existing upstream fetch path.

Source: Coding guidelines

Comment thread web/app/api/subrouter/exchange/route.ts Outdated
Comment on lines +34 to +48
const upstream = await fetch(`${hostedUrl}/_subrouter/auth/stack`, {
method: "POST",
headers: {
authorization: `Bearer ${resolved.value.accessToken}`,
"content-type": "application/json",
"x-subrouter-stack-control-token": controlToken,
},
body: JSON.stringify({
capabilities,
teamId: resolved.value.team.teamId,
teamName: resolved.value.team.teamName,
}),
cache: "no-store",
});
const body = await upstream.text();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## locate target"
fd -a 'route.ts$' . | sed 's#^\./##' | rg 'web/app/api/subrouter/exchange/route.ts|route\.ts$' | sort

echo "## target file excerpt"
cat -n web/app/api/subrouter/exchange/route.ts | sed -n '1,120p'

echo "## timeout/abort patterns in web route API"
rg -n "fetch\\(|AbortSignal|timeout|AbortController|signal|\\{[\\s\\S]*fetch\\(" web/app/api -g 'route.ts' -g '*.ts' | head -200

echo "## candidate timeout helpers"
rg -n "TIMEOUT|TIME|timeout|AbortSignal|AbortController|setTimeout\\(" web app packages lib server 2>/dev/null | head -300 || true

Repository: manaflow-ai/cmux

Length of output: 38212


Apply the existing subrouter auth timeout to this upstream request.

web/app/api/subrouter/exchange/route.ts:34 calls fetch(...) without a cancellation-aware deadline, and web/app/api/subrouter/exchange/route.ts:48 can also wait indefinitely while reading the hosted response body. Use SUBROUTER_STACK_AUTH_TIMEOUT_MS with a bounded AbortSignal and return the safe exchange error when it is aborted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/api/subrouter/exchange/route.ts` around lines 34 - 48, Update the
upstream request in the exchange route around fetch and response-body reading to
use SUBROUTER_STACK_AUTH_TIMEOUT_MS with a cancellation-aware AbortSignal
covering both operations. Detect an abort caused by this timeout and return the
existing safe exchange error, while preserving current handling for non-timeout
failures.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/app/api/subrouter/exchange-v2/route.ts`:
- Around line 17-25: Update the hostedUrl initialization in the exchange route
to fall back to defaultHostedSubrouterURL() when SUBROUTER_HOSTED_URL is unset,
then apply the same trimming and trailing-slash normalization used by the CLI
configuration route. Keep the existing controlToken/hostedUrl availability check
and 503 response unchanged.
- Around line 7-59: Add behavior-level regression coverage for the CLI login
flow before the implementation change, using the relevant v2 exchange route and
CLI login symbols. The test must verify that the CLI receives the v2 URL and
that the exchange request sends the scoped capabilities plus the control-token
header; place only this failing test in the first commit, with the
implementation fix committed separately afterward.
- Around line 31-45: The hosted Stack exchange request in the route handler must
use a cancellation-aware deadline instead of allowing fetch and upstream.text()
to run indefinitely. Update the flow around resolveSubrouterRequestContext and
the outbound _subrouter/auth/stack fetch to pass the request signal or an
equivalent bounded signal through the fetch and response-reading path, and add
coverage verifying an aborted hosted request is cancelled.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1aab6048-aa0a-4a0b-8242-494e08242fe9

📥 Commits

Reviewing files that changed from the base of the PR and between 4528ac0 and 1bfae4b.

📒 Files selected for processing (2)
  • web/app/api/cli/config/route.ts
  • web/app/api/subrouter/exchange-v2/route.ts

Comment on lines +7 to +59
export async function POST(request: Request): Promise<Response> {
const resolved = await resolveSubrouterRequestContext(request, {
permission: "use-or-manage",
allowCookie: false,
});
if (!resolved.ok) return resolved.response;

try {
const controlToken =
process.env.SUBROUTER_STACK_TENANT_DELETE_TOKEN?.trim();
const hostedUrl = process.env.SUBROUTER_HOSTED_URL?.trim().replace(
/\/+$/,
"",
);
if (!controlToken || !hostedUrl) {
return Response.json(
{ error: "service_unavailable" },
{ status: 503 },
);
}
const capabilities = [
...(resolved.value.team.manageAccounts ? ["manage_accounts"] : []),
...(resolved.value.team.use ? ["use"] : []),
];
const upstream = await fetch(`${hostedUrl}/_subrouter/auth/stack`, {
method: "POST",
headers: {
authorization: `Bearer ${resolved.value.accessToken}`,
"content-type": "application/json",
"x-subrouter-stack-control-token": controlToken,
},
body: JSON.stringify({
capabilities,
teamId: resolved.value.team.teamId,
teamName: resolved.value.team.teamName,
}),
cache: "no-store",
});
const body = await upstream.text();
if (!upstream.ok) {
return new Response(body, {
status: upstream.status,
headers: { "content-type": "text/plain; charset=utf-8" },
});
}
const tenant: unknown = JSON.parse(body);
return Response.json(tenant, {
headers: { "cache-control": "no-store" },
});
} catch (error) {
return subrouterErrorResponse(error);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add behavior-level regression coverage before the implementation commit.

This PR fixes a CLI login regression. Add a failing test that verifies the CLI receives the v2 URL and that the v2 exchange request includes the scoped capabilities and control header. Put that test in a test-only commit before the implementation commit. As per coding guidelines, “Regression fixes should use two commits: the first adds only the failing test, and the second adds the fix.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/api/subrouter/exchange-v2/route.ts` around lines 7 - 59, Add
behavior-level regression coverage for the CLI login flow before the
implementation change, using the relevant v2 exchange route and CLI login
symbols. The test must verify that the CLI receives the v2 URL and that the
exchange request sends the scoped capabilities plus the control-token header;
place only this failing test in the first commit, with the implementation fix
committed separately afterward.

Source: Coding guidelines

Comment on lines +17 to +25
const hostedUrl = process.env.SUBROUTER_HOSTED_URL?.trim().replace(
/\/+$/,
"",
);
if (!controlToken || !hostedUrl) {
return Response.json(
{ error: "service_unavailable" },
{ status: 503 },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the same hosted URL fallback as the CLI configuration route.

When SUBROUTER_STACK_TENANT_DELETE_TOKEN is set but SUBROUTER_HOSTED_URL is unset, web/app/api/cli/config/route.ts returns defaultHostedSubrouterURL(). This route returns 503 at Line 21 instead. The CLI can complete discovery but cannot call its advertised exchange endpoint.

Use the same default URL and normalization path in both routes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/api/subrouter/exchange-v2/route.ts` around lines 17 - 25, Update the
hostedUrl initialization in the exchange route to fall back to
defaultHostedSubrouterURL() when SUBROUTER_HOSTED_URL is unset, then apply the
same trimming and trailing-slash normalization used by the CLI configuration
route. Keep the existing controlToken/hostedUrl availability check and 503
response unchanged.

Comment on lines +31 to +45
const upstream = await fetch(`${hostedUrl}/_subrouter/auth/stack`, {
method: "POST",
headers: {
authorization: `Bearer ${resolved.value.accessToken}`,
"content-type": "application/json",
"x-subrouter-stack-control-token": controlToken,
},
body: JSON.stringify({
capabilities,
teamId: resolved.value.team.teamId,
teamName: resolved.value.team.teamName,
}),
cache: "no-store",
});
const body = await upstream.text();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate target and related files =="
fd -a 'route\.ts$' . | rg 'web/app/api/subrouter/exchange-v2/route\.ts|subrouter|exchange' || true

echo "== target excerpt =="
if [ -f web/app/api/subrouter/exchange-v2/route.ts ]; then
  wc -l web/app/api/subrouter/exchange-v2/route.ts
  cat -n web/app/api/subrouter/exchange-v2/route.ts
fi

echo "== reference resolveSubrouterRequestContext usages =="
rg -n "resolveSubrouterRequestContext|subrouter.*auth/stack|_subrouter/auth/stack|setTimeout|AbortSignal|with.*abort|cancellation|deadline" web/app web/lib packages apps . --glob '!node_modules' --glob '!dist' --glob '!build' | head -200

Repository: manaflow-ai/cmux

Length of output: 22236


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('web/app/api/subrouter/exchange-v2/route.ts')
text = p.read_text() if p.exists() else ''
checks = {
    'contains_fetch_call': 'fetch(`${hostedUrl}/_subrouter/auth/stack`' in text,
    'contains_upstream_text': 'const body = await upstream.text()' in text,
    'contains_no_abort_signal': '"signal"' not in text or text.find('"signal"') > text.find('fetch(') and 'AbortSignal' not in text,
    'contains_abort_controller': 'AbortController' in text,
    'imports_resolveSubrouterRequestContext': 'resolveSubrouterRequestContext' in text,
}
for k, v in checks.items():
    print(f'{k}={v}')
PY

Repository: manaflow-ai/cmux

Length of output: 316


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate subrouter requestContext files =="
fd -a 'requestContext\.(ts|tsx|js|jsx)$' web | sort

echo "== inspect requestContext =="
for f in $(fd -a 'requestContext\.(ts|tsx|js|jsx)$' web | sort); do
  echo "--- $f ---"
  wc -l "$f"
  cat -n "$f"
done

echo "== inspect tenant-exchange route =="
f="web/app/api/subrouter/tenant-exchange/route.ts"
if [ -f "$f" ]; then
  wc -l "$f"
  cat -n "$f"
fi

echo "== inspect subrouter route tests under web =="
fd -a -e ts -e tsx 'subrouter.*test|test.*subrouter' web | sort
for f in $(fd -a -e ts -e tsx 'subrouter.*test|test.*subrouter' web | sort); do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '1,240p' "$f"
done

Repository: manaflow-ai/cmux

Length of output: 43186


Bound the hosted Stack exchange request with a cancellation-aware deadline.

resolveSubrouterRequestContext finishes with an unbounded signal after this call exits, then web/app/api/subrouter/tenant-exchange/route.ts calls fetch(...) and reads upstream.text() without a signal. A stalled _subrouter/auth/stack request can hold the tenant-exchange request until the platform timeout. Pass the request signal or another cancellation-aware boundary into the outbound call, and add coverage for an aborted hosted request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/api/subrouter/exchange-v2/route.ts` around lines 31 - 45, The hosted
Stack exchange request in the route handler must use a cancellation-aware
deadline instead of allowing fetch and upstream.text() to run indefinitely.
Update the flow around resolveSubrouterRequestContext and the outbound
_subrouter/auth/stack fetch to pass the request signal or an equivalent bounded
signal through the fetch and response-reading path, and add coverage verifying
an aborted hosted request is cancelled.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/app/api/subrouter/tenant-exchange/route.ts`:
- Around line 43-50: Update the error-response paths in the tenant exchange
handler, including the !upstream.ok branch and catch path, to set Cache-Control:
no-store whenever returning unsubstituted upstream or subrouter response
content. Preserve the existing status, body, and content-type behavior, and
apply the same header to any equivalent error exits in the route.
- Around line 31-44: Update the hosted tenant exchange fetch in the route
handler using resolveSubrouterRequestContext to include an AbortSignal with a
reasonable timeout, ensuring stalled requests are cancelled independently of the
completed context resolution. Add coverage verifying the timeout/cancellation
response path and preserve existing successful exchange behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0927f206-4d23-4efc-b9c1-917e7dbe2a4c

📥 Commits

Reviewing files that changed from the base of the PR and between 1bfae4b and 0e8fa1c.

📒 Files selected for processing (2)
  • web/app/api/cli/config/route.ts
  • web/app/api/subrouter/tenant-exchange/route.ts

Comment on lines +31 to +44
const upstream = await fetch(`${hostedUrl}/_subrouter/auth/stack`, {
method: "POST",
headers: {
authorization: `Bearer ${resolved.value.accessToken}`,
"content-type": "application/json",
"x-subrouter-stack-control-token": controlToken,
},
body: JSON.stringify({
capabilities,
teamId: resolved.value.team.teamId,
teamName: resolved.value.team.teamName,
}),
cache: "no-store",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file list =="
git ls-files | rg 'web/app/api/subrouter/tenant-exchange/route\.ts|permission|authorization|deadline|abort|fetch' | head -200

echo "== target file =="
cat -n web/app/api/subrouter/tenant-exchange/route.ts | sed -n '1,120p'

echo "== search relevant terms =="
rg -n "resolve|resolved\.value|accessToken|authorization|deadline|AbortSignal|fetch\(" web/app/api/web app web | rg "tenant-exchange|subrouter|auth" -n | head -250

Repository: manaflow-ai/cmux

Length of output: 29951


🌐 Web query:

next.js NextRequest context deadline authorization header

💡 Result:

In Next.js, NextRequest extends the standard Web Request API, meaning it adheres to standard request handling patterns rather than having a built-in context or deadline object [1][2]. Handling Authorization Headers To access an Authorization header in Next.js middleware, use the standard Headers API via request.headers.get('authorization') [3]. Because Next.js middleware operates in the Edge runtime, you must use this standard Web API rather than legacy Node.js request properties [4][3]. To pass authorization or other context downstream (e.g., to API routes or Server Components), you must explicitly set them in the outgoing request headers using NextResponse.next [5][6]: // middleware.ts example import { NextResponse, NextRequest } from 'next/server'; export function middleware(request: NextRequest) { const requestHeaders = new Headers(request.headers); // Set context/headers for downstream routes requestHeaders.set('x-user-id', 'extracted-user-id'); return NextResponse.next({ request: { headers: requestHeaders, }, }); } Context and Deadlines Next.js does not provide a native "context" object or request "deadline" (timeout) mechanism within the NextRequest API [1][2]. 1. Context: There is no built-in mechanism to pass state between middleware and the application other than by modifying request headers [7]. Developers often use headers to pass verified user identities or other context metadata downstream [7][4]. 2. Deadlines: Next.js does not expose a request-level deadline property. If you need to enforce timeouts (e.g., for database queries or API calls performed within middleware), you must implement them manually using standard JavaScript approaches like AbortController [7]. Best Practices Avoid setting large headers, as this can lead to 431 Request Header Fields Too Large errors [5]. Additionally, verify authentication inside sensitive functions (like Server Functions) rather than relying solely on middleware-based authorization, as middleware can be bypassed if routes are not correctly matched [8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tracked tests around hosted subrouter =="
git ls-files 'web/tests/*subrouter*.test.*' | sort
for f in $(git ls-files 'web/tests/*subrouter*.test.*'); do
  echo "--- $f wc"
  wc -l "$f"
done

echo "== target tests references route =="
for f in $(git ls-files 'web/tests/*subrouter*.test.*'); do
  echo "--- $f route.ts refs"
  rg -n "tenant-exchange|exchangeTeam|_subrouter/auth/stack|signal:|timeout|stubFetch|AbortSignal" "$f" || true
done

echo "== requestContext =="
cat -n web/services/subrouter/requestContext.ts | sed -n '1,220p'

echo "== subrouter routeHelpers =="
cat -n web/services/subrouter/routeHelpers.ts | sed -n '1,180p'

echo "== existing bounded fetch patterns =="
cat -n web/app/lib/agent-page-canonical-fetch.ts | sed -n '1,120p'
cat -n web/scripts/subrouter/migrate-legacy-tenants.ts | sed -n '370,400p'

Repository: manaflow-ai/cmux

Length of output: 17024


Bound the hosted tenant exchange request with a cancellation-aware signal.

resolveSubrouterRequestContext() returns a value, so the stack authorization deadline has ended before this route makes the external fetch. The call also has no signal, so a stalled hosted service can hold route capacity. Use an AbortSignal with a reasonable timeout for this hosted request, and add test coverage for the timeout response path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/api/subrouter/tenant-exchange/route.ts` around lines 31 - 44, Update
the hosted tenant exchange fetch in the route handler using
resolveSubrouterRequestContext to include an AbortSignal with a reasonable
timeout, ensuring stalled requests are cancelled independently of the completed
context resolution. Add coverage verifying the timeout/cancellation response
path and preserve existing successful exchange behavior.

Comment on lines +43 to +50
cache: "no-store",
});
const body = await upstream.text();
if (!upstream.ok) {
return new Response(body, {
status: upstream.status,
headers: { "content-type": "text/plain; charset=utf-8" },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate file =="
wc -l web/app/api/subrouter/tenant-exchange/route.ts
cat -n web/app/api/subrouter/tenant-exchange/route.ts

echo "== route helpers and cache header helpers =="
rg -n "Cache-Control|cache-control|jsonResponse|serviceUnavailableResponse|unauthorized" web/services/subrouter -S
cat -n web/services/subrouter/routeHelpers.ts | sed -n '1,240p'

echo "== subrouter auth helpers for cache headers =="
rg -n "Cache-Control|cache-control|unauthorized|forbidden|serviceUnavailable" web/services/subrouter web/app/api/subrouter -S

Repository: manaflow-ai/cmux

Length of output: 13353


Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information

Reachability: External

Reachability path
● Entry
  web/app/api/cli/config/route.ts:6
  runtime
│
▼
● Sink
  web/app/api/subrouter/tenant-exchange/route.ts

Set Cache-Control: no-store on every response that includes unsubstituted upstream body content.

cache: "no-store" only affects the outbound fetch. This branch returns upstream.text() without a cache restriction, and the catch path also returns subrouter response content without cache-control: no-store. Add that header to these error responses and any equivalent subrouter error exits that include upstream body content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/api/subrouter/tenant-exchange/route.ts` around lines 43 - 50, Update
the error-response paths in the tenant exchange handler, including the
!upstream.ok branch and catch path, to set Cache-Control: no-store whenever
returning unsubstituted upstream or subrouter response content. Preserve the
existing status, body, and content-type behavior, and apply the same header to
any equivalent error exits in the route.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web/app/api/subrouter/tenant-exchange/route.ts (2)

45-50: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Information Disclosure (CWE-209): Generation of Error Message Containing Sensitive Information

Reachability: External

Do not return the upstream error body verbatim.

route.ts:45-50 copies upstream.text() directly into the API response. The hosted endpoint may include provider names, internal identifiers, headers, or raw diagnostics. Map non-2xx responses to a stable safe error code and keep detailed diagnostics in redacted server logs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/api/subrouter/tenant-exchange/route.ts` around lines 45 - 50, Update
the non-2xx handling in the tenant exchange route to avoid returning the raw
upstream body; map failures to a stable, safe error code in the client response
while preserving the upstream status as appropriate. Log the detailed upstream
diagnostics only on the server after redaction, and keep successful response
handling unchanged.

Source: Coding guidelines


31-44: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sensitive Data Exposure (CWE-200): Exposure of Sensitive Information to an Unauthorized Actor

Reachability: Internal

Disable redirects for the credentialed exchange.

fetch follows redirects by default. This request carries access tokens and the x-coderouter-control header; if SUBROUTER_HOSTED_URL or an intermediary redirects, credentials may reach an unintended target. Use redirect: "error" or handle redirects manually with explicit host/header validation before retrying.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/api/subrouter/tenant-exchange/route.ts` around lines 31 - 44, Update
the credentialed fetch in the tenant exchange flow to disable automatic
redirects by setting its redirect policy to error. Preserve the existing request
URL, headers, body, and no-store caching behavior.

Source: MCP tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@web/app/api/subrouter/tenant-exchange/route.ts`:
- Around line 45-50: Update the non-2xx handling in the tenant exchange route to
avoid returning the raw upstream body; map failures to a stable, safe error code
in the client response while preserving the upstream status as appropriate. Log
the detailed upstream diagnostics only on the server after redaction, and keep
successful response handling unchanged.
- Around line 31-44: Update the credentialed fetch in the tenant exchange flow
to disable automatic redirects by setting its redirect policy to error. Preserve
the existing request URL, headers, body, and no-store caching behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 94aaad5e-8eea-467c-9bdb-8764b6a62e64

📥 Commits

Reviewing files that changed from the base of the PR and between 0e8fa1c and b5a88c2.

📒 Files selected for processing (1)
  • web/app/api/subrouter/tenant-exchange/route.ts

@lawrencecchen
lawrencecchen merged commit 0eecd5a into main Aug 5, 2026
6 checks passed
azooz2003-bit added a commit that referenced this pull request Aug 5, 2026
0eecd5a (#9607) switched SUBROUTER_STACK_TENANT_DELETE_TOKEN to the
validated env object, but t3-env freezes values at first import, so
tenant-control configuration became unobservable after boot and the
unconfigured paths broke: the exchange route returns 200 instead of 503
and account deletion fires hosted tenant deletes for accounts that never
enabled Subrouter. web tests have been red on main since (CI paused).
env.ts still validates presence on Vercel non-preview deployments.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant