Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
edfd951
feat(cursor): add owner-scoped OAuth pool kernel
yansigit Sep 6, 2026
fe0593a
fix(cursor): harden pool isolation and rollback
yansigit Sep 6, 2026
c42ce67
chore(cursor): remove unused pool import
yansigit Sep 6, 2026
1d0e67b
test(cursor): cover pool security invariants
yansigit Sep 6, 2026
04b7424
fix(cursor): align pool tests with flat account seam
yansigit Sep 6, 2026
8290e84
style(cursor): format pool kernel and tests
yansigit Sep 6, 2026
e9e0462
fix(cursor): make rollback and rotation CAS precise
yansigit Sep 6, 2026
debedc6
fix(cursor): scope TTL affinity cleanup by owner
yansigit Sep 6, 2026
415bb1e
test(cursor): keep TTL fixtures usable
yansigit Sep 6, 2026
20a36cb
fix(cursor): restore identity fallback and clear opaque refs
yansigit Sep 6, 2026
d3a6345
style(cursor): keep crypto import at module boundary
yansigit Sep 6, 2026
126ee93
fix(cursor): preserve pool compatibility contracts
yansigit Sep 6, 2026
d5e2ed2
fix(cursor): scope pool rollback generations
yansigit Sep 6, 2026
3db5d22
test(cursor): verify pooled-token transport wiring in adapter
yansigit Sep 6, 2026
2b58f12
fix(cursor): address review findings for pool references, versions, a…
yansigit Sep 6, 2026
23f900a
fix(cursor): clear versions on pool reset and eliminate redundant sto…
yansigit Sep 6, 2026
6e1d189
fix(cursor): prune removed refs below activation threshold
yansigit Sep 6, 2026
45fafac
fix(cursor): invalidate snapshots for removed accounts
yansigit Sep 6, 2026
29115af
fix(cursor): invalidate swept account snapshots
yansigit Sep 6, 2026
5192183
fix(cursor): prune orphaned pool versions
yansigit Sep 6, 2026
4700059
perf(cursor): bound pool cleanup scans
yansigit Sep 6, 2026
bea8f2d
merge upstream/dev into the Cursor pool kernel
cursoragent Oct 12, 2026
77b5d64
Merge remote-tracking branch 'upstream/dev' into codex/upstream-curso…
cursoragent Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `desktopExecutor?` | `DesktopExecutorConfig` | Cursor only: external computer-use and record-screen commands. |
| `unsafeAllowNativeLocalExec?` | `boolean` | Cursor legacy boolean, equivalent to `nativeLocalExec: "on"` only when the newer field is unset. |
| `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` | Cursor local-exec policy. `off` is default; `codex-sandbox` currently fails closed like `off`. |
| `cursorAccountPool?` | `{ enabled: boolean }` | Cursor only. Off unless `enabled` is exactly `true`. Selects among two or more usable stored Cursor accounts for one credential identity scope and client thread. A single account, a missing client thread, or a malformed value keeps the configured credential. |

Command Code's shipped per-model effort defaults include live API measurements. DeepSeek
v4/v4.1 Flash (including v4 Flash Vision), GLM-5.3 and GLM-5.3-Flash, Qwen3.8-Flash,
Expand Down Expand Up @@ -1312,6 +1313,12 @@ Set `nativeLocalExec` on `providers.cursor`, not at the top level. In the dashbo
`nativeLocalExec: "on"` only when `nativeLocalExec` is unset. MCP, screen recording, and computer use
are controlled separately by `mcpServers` and `desktopExecutor`.

`cursorAccountPool: { "enabled": true }` is also set on the Cursor provider. It stays off for any
other value. While on, a request with a client thread can send with another usable stored Cursor
account. The owner is the request's existing identity scope, or the hash of the configured
credential when that scope is absent. One stored account, or a request with no client thread,
leaves the configured credential in place. Conversation ids stay namespaced by that owner.

Foreground `shellArgs` and `shellStreamArgs` requests are rejected before process creation on
Windows, macOS, and Linux, including with `nativeLocalExec: "on"`. They require a kernel-backed
descendant owner that this implementation does not provide; use the client's shell tool instead.
Expand Down
34 changes: 31 additions & 3 deletions src/adapters/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { AdapterEvent, OcxProviderConfig } from "../types";
import type { ProviderAdapter } from "./base";
import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
import { cursorExecDeniedMessage, cursorRequestDeclaresFullAccess } from "./cursor/exec-policy";
import { isCursorBenignCancelError, isCursorIncompleteToolCallMessage, isCursorInvalidArgumentError, isCursorOverflowRemintCandidate, isCursorRootEnvelopeError, safeCursorErrorMessage, type CursorSizeContext } from "./cursor/cursor-errors";
import { classifyCursorError, isCursorBenignCancelError, isCursorIncompleteToolCallMessage, isCursorInvalidArgumentError, isCursorOverflowRemintCandidate, isCursorRootEnvelopeError, safeCursorErrorMessage, type CursorSizeContext } from "./cursor/cursor-errors";
import { cursorCheckpointModelAffinityId, cursorNeedsExternalToolContinuation, inferCursorContextWindow, isCursorExternalWireModel } from "./cursor/discovery";
import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
import { mapCursorServerMessage } from "./cursor/message-mapper";
Expand Down Expand Up @@ -66,6 +66,7 @@ import {
} from "./cursor/transport";
import { cursorLiveRosterScope } from "./cursor/catalog";
import { cursorUsesPlainToolWording } from "./cursor/tool-wording";
import { noteCursorPoolRateLimit, selectCursorPoolToken } from "../providers/cursor-pool";

export const CURSOR_API_URL = "https://api2.cursor.sh";

Expand All @@ -85,6 +86,8 @@ export interface CursorAdapterDeps {
kv?: CursorKvStore;
/** Test seam: observe/replace context-usage rekeying on conversation-id rotation. */
rekeyContextUsage?: (fromConversationId: string, toConversationId: string) => void;
/** Optional internal pool seam. Owner is supplied by trusted route parsing, never request headers. */
selectPoolToken?: (owner: string, thread: string) => string | undefined;
}

function safeCursorTransportError(err: unknown, sizeContext?: CursorSizeContext): string {
Expand Down Expand Up @@ -170,13 +173,19 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
// Captured after createCursorRequest so the catch block can apply the bare-RE
// size prior (devlog 260) even though `request` is scoped inside the try.
let requestSizeContext: CursorSizeContext | undefined;
let usingSharedPool = false;
let pooledToken: string | undefined;
let poolOwner = "";
let poolThread = "";
try {
const makeTransport = deps.createTransport ?? createLiveCursorTransport;
const kv = deps.kv ?? createCursorKvStore({}, incoming.translatorBudget);
const rekeyContextUsage = deps.rekeyContextUsage ?? rekeyCursorContextUsage;
// Namespace thread→conversation derivation by the authenticated Cursor credential so
// shared-proxy tenants with different Cursor accounts cannot collide on a parent thread id.
// Prefer an already-set auth scope (e.g. Codex pool account) when present.
// Opt-in pool selection uses that scope as the owner and does not replace it with the
// rotated account, so a pooled token cannot collapse two tenants onto one conversation.
let liveRosterScope: string | undefined;
if (!_parsed._cursorIdentityScope) {
try {
Expand All @@ -188,7 +197,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
.digest("hex")
.slice(0, 16);
} catch {
/* Missing credential is handled by the live transport path below. */
// Missing credentials fail closed in the live transport path.
}
} else {
try {
Expand All @@ -197,6 +206,15 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
/* Missing credential is handled by the live transport path below. */
}
}
usingSharedPool = provider.cursorAccountPool?.enabled === true && deps.selectPoolToken === undefined;
const poolTokenSelector = deps.selectPoolToken ?? (usingSharedPool ? selectCursorPoolToken : undefined);
poolOwner = _parsed._cursorIdentityScope ?? "";
poolThread = _parsed._clientThreadId ?? "";
pooledToken = poolTokenSelector?.(poolOwner, poolThread);
const activeProvider = pooledToken ? { ...provider, apiKey: pooledToken } : provider;
// Roster evidence follows the credential that will actually be sent. The identity
// scope above stays on the pre-pool credential so conversation isolation is unchanged.
if (pooledToken) liveRosterScope = cursorLiveRosterScope(provider.baseUrl, pooledToken);
const inheritedCheckpointRef = _parsed._providerContinuation?.cursor?.checkpointRef;
const previousConversationId = _parsed._cursorConversationId;
let request = {
Expand Down Expand Up @@ -392,7 +410,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
await runCursorTurnWithRetry(
makeTransport,
{
provider,
provider: activeProvider,
headers: incoming.headers,
translatorBudget: incoming.translatorBudget,
requestDeclaresFullAccess: cursorRequestDeclaresFullAccess(activeRequest),
Expand Down Expand Up @@ -710,6 +728,16 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
}
}
} catch (err) {
if (
usingSharedPool
&& pooledToken
&& classifyCursorError(
err instanceof Error ? err.message : typeof err === "string" ? err : "",
requestSizeContext,
) === "Cursor rate limit exceeded"
) {
noteCursorPoolRateLimit(poolOwner, poolThread);
}
if (isCursorBenignCancelError(err)) return;
const partialUsage = (err as { partialUsage?: import("../types").OcxUsage }).partialUsage;
emit({
Expand Down
13 changes: 13 additions & 0 deletions src/config/schema/leaf-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,10 +411,23 @@ export const providerConfigSchema = z.object({
xaiResponsesXSearch: z.boolean().optional(),
xaiResponsesDefaultVersion: z.number().int().positive().optional().catch(undefined),
zaiResponsesDefaultVersion: z.number().int().positive().optional().catch(undefined),
// Opt-in and off by default. A malformed value degrades to absent so a hand edit cannot
// take the process through invalid-config recovery, and it cannot enable the pool either:
// only an exact `{ enabled: true }` object activates selection.
cursorAccountPool: z.object({
enabled: z.boolean(),
}).strict().optional().catch(undefined),
}).passthrough().superRefine((provider, ctx) => {
if (provider.projectContext !== undefined && provider.adapter !== "command-code") {
ctx.addIssue({ code: "custom", path: ["projectContext"], message: "projectContext is supported only by the command-code adapter" });
}
if (provider.cursorAccountPool !== undefined && provider.adapter !== "cursor") {
ctx.addIssue({
code: "custom",
path: ["cursorAccountPool"],
message: "cursorAccountPool is supported only by the Cursor adapter",
});
}
});


Expand Down
Loading
Loading