mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 23:22:09 +03:00
Requests AgentUrlConfig from Cursor with each selected account token and selects the account's actual server-assigned Agent endpoint (agentUrl/agentnUrl) instead of a fixed global/us host, which fails for teams pinned to a different region. Caches validated endpoints by connection+token. Closes #10802. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 4 files): - 18/18 focused tests pass (cursor-agent-host, cursor-apikey-provider). - provider-translate-path-golden.test.ts initially failed — traced to a pre-existing base-red (stale golden snapshot left by an earlier freebuff merge, #10531, unrelated to this PR) and confirmed it reproduces on the pure release tip without this PR's changes. Fixed directly on release/v3.8.50 (mechanical key-ordering regen, values unchanged) rather than folding it into this PR's scope; green after merging that fix in. - check-file-size, check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. - Author additionally validated live: a real Cursor request selected agentn.us.api5.cursor.sh and returned HTTP 200/PING. Co-authored-by: tuandinh0801 <tuandinh0801@users.noreply.github.com>
122 lines
4.6 KiB
TypeScript
122 lines
4.6 KiB
TypeScript
import { createHmac } from "node:crypto";
|
|
|
|
import { mergeAbortSignals, type ProviderCredentials } from "../base.ts";
|
|
import { stripCursorOAuthTokenPrefix } from "../../services/cursorApiKeyAuth.ts";
|
|
import {
|
|
formatCursorAgentClientVersion,
|
|
getCursorAgentCliVersion,
|
|
} from "../../utils/cursorAgentCliVersion.ts";
|
|
import { decodeFields } from "../../utils/cursorAgentProtobuf/wire.ts";
|
|
|
|
const CURSOR_API_URL = "https://api2.cursor.sh";
|
|
const CURSOR_SERVER_CONFIG_PATH = "/aiserver.v1.ServerConfigService/GetServerConfig";
|
|
const CURSOR_AGENT_PATH = "/agent.v1.AgentService/Run";
|
|
const CURSOR_SERVER_CONFIG_TIMEOUT_MS = 10_000;
|
|
const CURSOR_AGENT_URL_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
const CURSOR_AGENT_URL_CACHE_LIMIT = 1_000;
|
|
|
|
type CursorAgentUrls = { agentUrl: string; agentnUrl: string };
|
|
type CursorAgentUrlCacheEntry = CursorAgentUrls & { expiresAt: number };
|
|
const cursorAgentUrlCache = new Map<string, CursorAgentUrlCacheEntry>();
|
|
|
|
/** Reports an HTTP error from Cursor server-config discovery. */
|
|
export class CursorServerConfigError extends Error {
|
|
constructor(
|
|
message: string,
|
|
readonly status: number
|
|
) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
function validateCursorAgentUrl(value: string): string {
|
|
const url = new URL(value);
|
|
const isCursorAgentHost =
|
|
url.hostname === "api5.cursor.sh" || url.hostname.endsWith(".api5.cursor.sh");
|
|
if (
|
|
url.protocol !== "https:" ||
|
|
!isCursorAgentHost ||
|
|
url.username ||
|
|
url.password ||
|
|
url.search ||
|
|
url.hash
|
|
) {
|
|
throw new Error("Cursor server config included an invalid Agent URL");
|
|
}
|
|
return url.origin;
|
|
}
|
|
|
|
function parseCursorAgentUrls(payload: Buffer): CursorAgentUrls {
|
|
const agentUrlConfig = decodeFields(payload).find(
|
|
(field) => field.fieldNumber === 27 && field.wireType === 2
|
|
);
|
|
if (!agentUrlConfig || agentUrlConfig.wireType !== 2) {
|
|
throw new Error("Cursor server config did not include Agent URLs");
|
|
}
|
|
const fields = decodeFields(agentUrlConfig.bytes);
|
|
const agentUrl = fields.find((field) => field.fieldNumber === 1 && field.wireType === 2);
|
|
const agentnUrl = fields.find((field) => field.fieldNumber === 2 && field.wireType === 2);
|
|
if (!agentUrl || agentUrl.wireType !== 2 || !agentnUrl || agentnUrl.wireType !== 2) {
|
|
throw new Error("Cursor server config included incomplete Agent URLs");
|
|
}
|
|
return {
|
|
agentUrl: validateCursorAgentUrl(agentUrl.bytes.toString("utf8")),
|
|
agentnUrl: validateCursorAgentUrl(agentnUrl.bytes.toString("utf8")),
|
|
};
|
|
}
|
|
|
|
async function fetchCursorAgentUrls(
|
|
accessToken: string,
|
|
signal?: AbortSignal | null
|
|
): Promise<CursorAgentUrls> {
|
|
const timeoutSignal = AbortSignal.timeout(CURSOR_SERVER_CONFIG_TIMEOUT_MS);
|
|
const response = await fetch(`${CURSOR_API_URL}${CURSOR_SERVER_CONFIG_PATH}`, {
|
|
method: "POST",
|
|
headers: {
|
|
authorization: `Bearer ${accessToken}`,
|
|
"connect-protocol-version": "1",
|
|
"content-type": "application/proto",
|
|
"user-agent": "connect-es/1.6.1",
|
|
"x-cursor-client-type": "cli",
|
|
"x-cursor-client-version": formatCursorAgentClientVersion(getCursorAgentCliVersion()),
|
|
},
|
|
body: Buffer.alloc(0),
|
|
signal: signal ? mergeAbortSignals(signal, timeoutSignal) : timeoutSignal,
|
|
});
|
|
if (!response.ok) {
|
|
throw new CursorServerConfigError(
|
|
`Cursor server config request failed with status ${response.status}`,
|
|
response.status
|
|
);
|
|
}
|
|
return parseCursorAgentUrls(Buffer.from(await response.arrayBuffer()));
|
|
}
|
|
|
|
/** Resolve the Agent RPC URL that Cursor assigned to this connection. */
|
|
export async function resolveCursorAgentUrl(
|
|
credentials: ProviderCredentials,
|
|
signal?: AbortSignal | null
|
|
): Promise<string> {
|
|
const accessToken = stripCursorOAuthTokenPrefix(credentials.accessToken || "");
|
|
if (!accessToken) throw new Error("Cursor access token is required");
|
|
const cacheKey =
|
|
`${credentials.connectionId || "anonymous"}:` +
|
|
createHmac("sha256", "omniroute-cursor-agent-url-cache-v1").update(accessToken).digest("hex");
|
|
const now = Date.now();
|
|
let urls = cursorAgentUrlCache.get(cacheKey);
|
|
if (!urls || urls.expiresAt <= now) {
|
|
const fetched = await fetchCursorAgentUrls(accessToken, signal);
|
|
urls = { ...fetched, expiresAt: now + CURSOR_AGENT_URL_CACHE_TTL_MS };
|
|
if (
|
|
!cursorAgentUrlCache.has(cacheKey) &&
|
|
cursorAgentUrlCache.size >= CURSOR_AGENT_URL_CACHE_LIMIT
|
|
) {
|
|
const oldestKey = cursorAgentUrlCache.keys().next().value as string | undefined;
|
|
if (oldestKey !== undefined) cursorAgentUrlCache.delete(oldestKey);
|
|
}
|
|
cursorAgentUrlCache.set(cacheKey, urls);
|
|
}
|
|
const ghostMode = credentials.providerSpecificData?.ghostMode !== false;
|
|
return `${ghostMode ? urls.agentUrl : urls.agentnUrl}${CURSOR_AGENT_PATH}`;
|
|
}
|