Skip to content

cursor/auto is a sentinel, not a provider — and it is what a Cursor session records - #4607

Merged
chelojimenez merged 4 commits into
mainfrom
fix/cursor-auto-model-identity
Sep 2, 2026
Merged

cursor/auto is a sentinel, not a provider — and it is what a Cursor session records#4607
chelojimenez merged 4 commits into
mainfrom
fix/cursor-auto-model-identity

Conversation

@chelojimenez

@chelojimenez chelojimenez commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

Two bugs that made a Cursor CLI session misrepresent itself — one of which prevented Cursor hosts from reaching the harness at all on the desktop rail.

The cursor-cli template seeds modelId: "cursor/auto", a deliberate NEUTRAL SENTINEL: the Cursor CLI has no provider routing, the adapter passes no model, and Cursor Auto picks. Backend #1224 additionally registered cursor as a ModelProviderName so the sentinel would not classify as ollama. That registration had a consequence nobody traced.

Root causes

1 — cursor/auto demanded a BYOK provider key. deriveOrgProviderKey (server/utils/org-model-config.ts:274-287) returns {ok: true, key: provider} for anything non-custom, so cursor/auto yielded key: "cursor". Three call sites then hit the org config:

  • resolveSyntheticModelSource → not local-eligible → source: "byok"resolve-turn-runtime.ts built /stream/org with providerKey: "cursor" → Convex provider_not_configured: cursor is not enabled for this project/workspace organization. (This is what we hit live on the v1 sessions path.)
  • web-chat-turn.ts:1082 for non-harness turns.
  • routes/mcp/chat-v2.ts:1732 — worse: the gate was isMcpJamProvidedModel && modelDefinition.id, so every Cursor host on the local desktop rail fell into org-BYOK and never reached the harness at all.

2 — the session recorded no model. Two independent holes, both specific to the external-account harness:

  • routes/web/chat-v2.ts:737 gated the host-model override on isScenarioSession, so a Playground host-bound turn never adopted the host's cursor/auto. The client cannot supply it either — the picker reseed only adopts a host model that resolves in availableModels, and the sentinel never does — so the body's leftover pick was recorded verbatim.
  • routes/web/chat-v2.ts:282 guarded !modelDefinition (the object), never .id, and model arrives via an unvalidated body cast. The harness rail is the only live path with no downstream id check (it skips deriveOrgProviderKey and both harness model gates, which are exempt for external-account), so an id-less body ran a full turn and ""/undefined reached ingestion, where JSON.stringify drops the key — hence modelId: undefined on the persisted row.

Changes

  • shared/model-provider.tsRUNTIME_CHOSEN_MODEL_SENTINELS (null-prototype id → label) plus isRuntimeChosenModelSentinel / runtimeChosenModelSentinelName. One shared predicate beside the classification rules, usable by server and client, instead of scattered === "cursor/auto" checks.
  • org-model-config.tsderiveOrgProviderKey refuses the sentinel at the single chokepoint covering all three call sites; resolveSyntheticModelSource returns external-account before any key derivation; buildSyntheticModelDefinition labels it "Cursor Auto" without rewriting the id.
  • resolve-turn-runtime.ts — new external-account arm → hosted /stream carrying only the harness, modelSource: "external-account", no providerKey; throws clearly when no harness is selected.
  • routes/v1/chat-session-turn.ts — refuses the sentinel before claimTurnLease, which is what creates the chatSessions row; refusing after it is what leaves a modelless orphan.
  • routes/mcp/chat-v2.ts — external-account exemption on the MCPJam-free branch (mirrors web-chat-turn) + host-model authority.
  • routes/web/chat-v2.ts — host model is authoritative for an external-account harness on every surface, not just scenarios; ingress validates model.id, not just model.
  • client/src/hooks/use-chat-session.ts — renders the sentinel as "Cursor Auto" with an honest lock reason instead of the raw id under a sign-in wall.

Tests

199 passing across the touched files (5389 in the wider server+shared sweep, 2097 client). Non-vacuity verified per file by stashing only the production change:

  • revert web/chat-v2.ts → 3 of 4 new route tests fail (expected 'anthropic/claude-haiku-4.5' to be 'cursor/auto' ×2; expected 200 to be 400 for the id-less body — the unfixed code returns 200 and persists the blank).
  • revert org-model-config.ts + resolve-turn-runtime.ts → 5 fail, incl. expected 'byok' to be 'external-account' — the exact route to provider_not_configured.
  • revert shared/model-provider.ts → 5 fail; revert chat-session-turn.ts → the sentinel refusal fails.

A deliberately passing-both-ways case ("leaves a NON-harness host's model to the body, as before") keeps the exemption scoped.

Found, not fixed (reported, not silently carried)

  • Orphan lease rows in general: claimTurnLease creates the chatSessions row before the model resolves, so any turn dying between claim and ingest leaves a modelless row. The cursor/auto case is closed by refusing earlier; the general shape remains.
  • "" is indistinguishable from absent in the trace projection (routes/v1/chat-sessions.ts:402).
  • getDefaultModel returns availableModels[0] against a declared ModelDefinition return type (documented as INSPECTOR-CLIENT-222).
  • String(model.id) laundering turns an undefined id into the string "undefined" at three client sites.
  • The Playground picker still shows a stale model on a Cursor host (the reseed skips the sentinel); the server no longer records it, and the Behavior tab already marks the selection unenforced.

🤖 Generated with Claude Code


Note

Medium Risk
Touches model routing, spend attribution (modelSource), harness preflight, and eval dispatch across web, MCP, v1, and swarm paths—high behavioral surface area but heavily tested and fail-closed for misconfiguration.

Overview
Fixes Cursor CLI hosts misrouting on cursor/auto and recording the wrong (or empty) model in sessions.

Provider and runtime handling. Introduces shared runtime-chosen sentinel helpers (cursor/auto → display Cursor Auto, id unchanged). deriveOrgProviderKey refuses the sentinel instead of yielding a cursor BYOK key; classification uses external-account without org lookup. resolveTurnRuntime rejects those turns on surfaces that cannot pass harness credentials (e.g. v1 API). The public sessions API rejects the sentinel before the turn lease creates a row.

Chat rails (web + desktop MCP). External-account harness turns follow the MCPJam-free/harness path (not org-BYOK), persist modelSource: 'external-account', mint guest bearer for anonymous desktop Cursor turns, and treat the host model as authoritative over the browser picker. Ingress requires a non-empty model.id; misconfigured hosts (ordinary id on Cursor harness) fail early via externalAccountHostModelRefusalReason using hostModelId.

Harness gates and dispatch. External-account harnesses must pin the sentinel; runAssistantTurn throws instead of silently emulating. Eval execution promotes the host sentinel over per-case models so admission and dispatch agree.

UI. Playground shows Cursor Auto locked with copy that the host runtime picks the model on the user's account, not a sign-in wall.

Reviewed by Cursor Bugbot for commit 24b7df9. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Treats cursor/auto as a sentinel rather than a provider model, so Cursor CLI turns no longer fail with provider_not_configured and sessions record the host's sentinel instead of a model that never ran.

Bug Fixes

  • Adds a shared sentinel predicate that exempts cursor/auto from provider-key derivation and the org-model-config lookup, classifies it as external-account, and labels it "Cursor Auto" on the server and client.
  • Web and local MCP rails route external-account harness turns to the harness without a provider key, tagged external-account so they don't consume MCPJam spend; anonymous local turns now get a guest bearer instead of 503-ing.
  • Refuses external-account turns on surfaces that can't deliver the customer's credential: resolveTurnRuntime before the turn is marked spent, and the public sessions API before the turn lease creates the session row.
  • Makes the host's sentinel authoritative over the browser's leftover model on every surface, including eval runs where admission and execution now agree, and rejects missing, null, or blank model.id before anything runs or persists.
  • Rejects external-account hosts that pin an ordinary model id, holding the host's own configured id to the gate so a body-supplied sentinel can't bypass it; the check runs before the org-model-config lookup, and the dispatch throws instead of falling back to the emulated engine.

Written for commit 24b7df9. Summary will update on new commits.

Review in cubic

…ession records

The `cursor-cli` host template seeds `modelId: "cursor/auto"`: a neutral
sentinel, deliberately not a real provider model, because the Cursor CLI has no
provider routing and the adapter passes no model at all. Registering `cursor` as
a `ModelProvider` (so the id classifies honestly instead of falling through the
bare-id rule to `ollama`) also made every provider-resolution path treat it as a
BYOK provider needing a configured org key, which answered
`provider_not_configured: cursor` — a setup error for a key that cannot exist.

Two bugs, one cause.

1. Provider resolution. `isRuntimeChosenModelSentinel` /
   `runtimeChosenModelSentinelName` land in `shared/model-provider.ts`, beside
   the classification rules, so server and client share one answer instead of
   comparing against `"cursor/auto"` in a dozen places.
   `deriveOrgProviderKey` refuses the sentinel at the one chokepoint all three
   derivation sites go through; `resolveSyntheticModelSource` classifies it
   `external-account` without a round trip; `resolveTurnRuntime` maps that to the
   hosted shape carrying only the harness, and refuses it outright when no
   harness is selected. On the public sessions API the refusal moved ahead of the
   turn lease, which is what CREATES the session row — failing after it left a
   session that had run on nothing. The local `/api/mcp/chat-v2` rail takes the
   external-account exemption the web rail already had, so a Cursor host reaches
   the harness instead of the org-BYOK branch, tagged `external-account` rather
   than `mcpjam`.

2. What the session records. The Playground picker cannot hold the sentinel, and
   the host-model override was gated on scenario turns — so the browser's
   leftover pick was recorded verbatim, naming a model the turn never touched;
   with no usable id in the body it was recorded blank, because the harness rail
   is the one live path with no downstream model-id check. The host's model is
   now authoritative for an external-account harness on every surface, and the
   chat route validates the model's `id`, not just that an object was sent.

Displayed as "Cursor Auto" wherever a label is rendered. The id is never
rewritten: traces and eval metadata still record `cursor/auto`, because the whole
point of the sentinel is that the model is unknown to MCPJam.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_c1ed1047-a969-4530-a031-25582e707bfd)

@chelojimenez

chelojimenez commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Internal preview

Preview URL: https://mcp-inspector-pr-4607.up.railway.app
Deployed commit: dfa8f3d
PR head commit: 6e3482f
Backend target: staging fallback.
Health: ✅ Convex reachable
Access is employee-only in non-production environments.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 13 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread mcpjam-inspector/server/utils/resolve-turn-runtime.ts
Comment thread mcpjam-inspector/server/routes/mcp/chat-v2.ts
Comment thread mcpjam-inspector/server/routes/web/chat-v2.ts
Comment thread mcpjam-inspector/server/routes/web/chat-v2.ts
Comment thread .changeset/cursor-auto-sentinel-is-not-a-provider.md Outdated
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 70739239-0b4d-4380-a1df-67d23c82d5b9

📥 Commits

Reviewing files that changed from the base of the PR and between 8de6ddd and 24b7df9.

📒 Files selected for processing (17)
  • .changeset/cursor-auto-sentinel-is-not-a-provider.md
  • mcpjam-inspector/server/routes/mcp/__tests__/chat-v2.guest-skills.test.ts
  • mcpjam-inspector/server/routes/mcp/__tests__/chat-v2.harness.test.ts
  • mcpjam-inspector/server/routes/mcp/chat-v2.ts
  • mcpjam-inspector/server/routes/web/__tests__/chat-v2.environment.test.ts
  • mcpjam-inspector/server/routes/web/__tests__/chat-v2.hosted.test.ts
  • mcpjam-inspector/server/routes/web/__tests__/chat-v2.scenario-environment.test.ts
  • mcpjam-inspector/server/routes/web/__tests__/chat-v2.scenario-sandbox.test.ts
  • mcpjam-inspector/server/routes/web/chat-v2.ts
  • mcpjam-inspector/server/services/evals-runner.ts
  • mcpjam-inspector/server/services/evals/__tests__/harness-admission.test.ts
  • mcpjam-inspector/server/services/evals/harness-admission.ts
  • mcpjam-inspector/server/services/sessionSimulation/swarm-runner.ts
  • mcpjam-inspector/server/utils/__tests__/assistant-turn.test.ts
  • mcpjam-inspector/server/utils/assistant-turn.ts
  • mcpjam-inspector/server/utils/harness/__tests__/harness-availability.test.ts
  • mcpjam-inspector/server/utils/harness/harness-availability.ts

Walkthrough

The changes define cursor/auto as a runtime-chosen model sentinel with the display name Cursor Auto. Sentinel models bypass provider-key resolution, classify as external-account, and skip organization model-config lookup. External-account harnesses require the sentinel and use chat-v2 routing, bearer minting, and modelSource: "external-account" persistence. Direct unsupported turns and requests without model IDs return validation errors. Evaluation and dispatch paths use the host model, while client lock states and tests cover the new behavior.

Merge Risk: 🔵 Low · up to 8de6d

MCP chat requests with missing or blank model IDs can still pass validation and reach downstream handling, which may create malformed turns or incorrect model records. The PR is mergeable with explicit follow-up to reject these values at ingress.


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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
mcpjam-inspector/server/routes/mcp/chat-v2.ts (1)

1041-1041: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mint a guest bearer for external-account harness turns.

If an anonymous scenario resolves to an external-account harness, isMcpJamProvidedModel is false. resolveMcpJamAuthHeader() then returns without minting a bearer, and the usesMcpjamFreePath branch returns 503.

Use usesMcpjamFreePath in both bearer conditions. Add a no-Authorization regression test.

🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

In `@mcpjam-inspector/server/routes/mcp/chat-v2.ts` at line 1041, Update
resolveMcpJamAuthHeader’s bearer condition to use usesMcpjamFreePath alongside
mcpJamAuthHeader, so anonymous external-account harness turns mint a guest
bearer when isMcpJamProvidedModel is false. Add a regression test covering an
absent Authorization header and confirming the free-path request does not return
503.
🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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 `@mcpjam-inspector/server/routes/web/chat-v2.ts`:
- Around line 283-295: Add route-level regression tests covering missing, null,
empty, and whitespace-only model.id validation before persistence;
external-account harness model override and host-model selection on the web and
MCP routes; dispatch without hosted-model or org-BYOK classification; and
harness dispatch persistence with modelSource set to external-account. Update
the affected sites in mcpjam-inspector/server/routes/web/chat-v2.ts at lines
283-295 and 766-792, and mcpjam-inspector/server/routes/mcp/chat-v2.ts at lines
936-964, 1029-1034, and 1564-1728, using the existing route test helpers and
asserting both success and validation/error outcomes.

Apply the same fix in `@mcpjam-inspector/client/src/hooks/use-chat-session.ts`
around lines 721 - 731: Covers the client sentinel label and disabled-reason
behavior.

Apply the same fix in
`@mcpjam-inspector/server/routes/web/__tests__/chat-v2.hosted.test.ts` around
lines 1460 - 1485: Covers null and empty model-id validation edge cases.

---

Outside diff comments:
In `@mcpjam-inspector/server/routes/mcp/chat-v2.ts`:
- Line 1041: Update resolveMcpJamAuthHeader’s bearer condition to use
usesMcpjamFreePath alongside mcpJamAuthHeader, so anonymous external-account
harness turns mint a guest bearer when isMcpJamProvidedModel is false. Add a
regression test covering an absent Authorization header and confirming the
free-path request does not return 503.
🪄 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: Organization UI

Review profile: CHILL

Plan: Team

Run ID: ddfd5ac0-7891-4139-8995-3727f3d2f094

📥 Commits

Reviewing files that changed from the base of the PR and between 5ac172c and 5add8da.

📒 Files selected for processing (13)
  • .changeset/cursor-auto-sentinel-is-not-a-provider.md
  • mcpjam-inspector/client/src/hooks/use-chat-session.ts
  • mcpjam-inspector/server/routes/mcp/chat-v2.ts
  • mcpjam-inspector/server/routes/v1/__tests__/chat-sessions.test.ts
  • mcpjam-inspector/server/routes/v1/chat-session-turn.ts
  • mcpjam-inspector/server/routes/web/__tests__/chat-v2.hosted.test.ts
  • mcpjam-inspector/server/routes/web/chat-v2.ts
  • mcpjam-inspector/server/utils/__tests__/org-model-config.test.ts
  • mcpjam-inspector/server/utils/__tests__/resolve-turn-runtime.test.ts
  • mcpjam-inspector/server/utils/org-model-config.ts
  • mcpjam-inspector/server/utils/resolve-turn-runtime.ts
  • mcpjam-inspector/shared/__tests__/model-provider.test.ts
  • mcpjam-inspector/shared/model-provider.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 2 remain after this review.

Comment thread mcpjam-inspector/server/routes/web/chat-v2.ts
…t that carries it

Four findings from review on the cursor/auto sentinel change, verified against
the code and fixed at the chokepoint each belongs to.

`resolveTurnRuntime` no longer hands back a runnable runtime for an
external-account turn. Classifying the sentinel as `external-account` is right;
advertising it as runnable there was not. An external-account runtime
authenticates with the customer's own vendor credential, `runHarnessTurn` takes
that credential only from the caller's materialized project secrets, and
`runUnifiedAssistantTurn` — the facade every caller of this resolver drives —
has no `runtimeSecrets` seam at all. The synthetic/swarm path therefore reached
the harness and died inside it telling the user to add a `CURSOR_API_KEY` secret
they may already have set. Refused up front with the real reason instead;
wiring the secrets fetch AND the transcript scrubber into that runner is a
security-relevant change with its own review.

The local `/api/mcp/chat-v2` bearer mint now keys on the free-path predicate
rather than "MCPJam provides this model". An external-account harness turn takes
that branch and needs the same bearer for what the branch does with it, so an
anonymous Cursor turn was answering 503 before the harness started, on a host
the preflight had just called ready.

`resolveHostModelDefinition` skips the org-model-config round-trip for a
sentinel. No enabled provider can list `cursor/auto`, so on a live turn that
call was pure latency (a 15 s timeout) plus a failure mode, sitting between the
request and the first token.

The host-wins gate on both rails now requires the host to actually carry the
sentinel, and a host that does not is refused rather than mis-attributed. An
external-account host pinned to an ordinary id describes nothing that runs
either — Cursor ignores it — so promoting it over the body swapped one wrong
model id for another. The shared harness gate (`checkHarnessRuntimeAvailable`
and its dispatch twin `harnessModelEligibleForRuntime`, which a test asserts
agree) now rejects it, naming the id it found.

Tests: the refusals and the bypass, each proven non-vacuous by reverting the
production line; the client sentinel label with its external-account reason plus
the ordinary locked-model control; null/empty/whitespace-only `model.id` at
hosted-chat ingress asserting 400 before the engine and before any persist; and
host-model precedence for a sentinel host and a mis-configured one. Changeset
corrected: `resolveSyntheticModelSource` classifies the sentinel, it does not
refuse it — only the web and desktop derivation rails refuse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ab888051-a0f9-4009-ac68-912ae418c390)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (1)
mcpjam-inspector/server/routes/mcp/chat-v2.ts (1)

1010-1013: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject empty model IDs at ingress.

Line 1011 rejects only an absent model object. It accepts { id: "", provider: "…" } and whitespace-only IDs. Those values then reach later routing or model-construction paths instead of returning a validation error.

Match the hosted route guard with !String(modelDefinition?.id ?? "").trim(). Add MCP-route cases for missing, null, empty, and whitespace-only IDs.

🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

In `@mcpjam-inspector/server/routes/mcp/chat-v2.ts` around lines 1010 - 1013,
Update the model validation guard near modelDefinition to reject missing, null,
empty, and whitespace-only IDs by validating the trimmed string value of
modelDefinition.id. Preserve the existing 400 response for unsupported models,
and add MCP-route coverage for each invalid-ID case.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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 `@mcpjam-inspector/server/routes/mcp/chat-v2.ts`:
- Around line 1010-1013: Update the model validation guard near modelDefinition
to reject missing, null, empty, and whitespace-only IDs by validating the
trimmed string value of modelDefinition.id. Preserve the existing 400 response
for unsupported models, and add MCP-route coverage for each invalid-ID case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 1ff69c17-d59d-48d3-9db3-f193f99f71ce

📥 Commits

Reviewing files that changed from the base of the PR and between 5add8da and 8de6ddd.

📒 Files selected for processing (12)
  • .changeset/cursor-auto-sentinel-is-not-a-provider.md
  • mcpjam-inspector/client/src/hooks/__tests__/use-chat-session.minimal-mode.test.tsx
  • mcpjam-inspector/server/routes/mcp/__tests__/chat-v2.harness.test.ts
  • mcpjam-inspector/server/routes/mcp/chat-v2.ts
  • mcpjam-inspector/server/routes/web/__tests__/chat-v2.hosted.test.ts
  • mcpjam-inspector/server/routes/web/chat-v2.ts
  • mcpjam-inspector/server/utils/__tests__/org-model-config.test.ts
  • mcpjam-inspector/server/utils/__tests__/resolve-turn-runtime.test.ts
  • mcpjam-inspector/server/utils/harness/__tests__/harness-availability.test.ts
  • mcpjam-inspector/server/utils/harness/harness-availability.ts
  • mcpjam-inspector/server/utils/org-model-config.ts
  • mcpjam-inspector/server/utils/resolve-turn-runtime.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/cursor-auto-sentinel-is-not-a-provider.md

Included review availability: Your plan provides up to 8 included reviews per hour; 1 remains after this review.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 12 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mcpjam-inspector/server/utils/harness/harness-availability.ts">

<violation number="1" location="mcpjam-inspector/server/utils/harness/harness-availability.ts:71">
P2: On eval/synthetic turns, an external-account host with an ordinary model id now falls through to the emulated engine rather than being refused. `runAssistantTurn` interprets this `false` as `useHarness = false`, while `sessionSimulation/runner.ts` does not run `checkHarnessRuntimeAvailable`, so the run can complete without Cursor ever running. Propagate a hard model-unsupported result to these callers instead of using eligibility false as a fallback.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread mcpjam-inspector/server/utils/harness/harness-availability.ts Outdated
}): boolean {
if (args.adapter.modelAccess === "external-account") return true;
if (args.adapter.modelAccess === "external-account") {
return isRuntimeChosenModelSentinel(args.modelId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: On eval/synthetic turns, an external-account host with an ordinary model id now falls through to the emulated engine rather than being refused. runAssistantTurn interprets this false as useHarness = false, while sessionSimulation/runner.ts does not run checkHarnessRuntimeAvailable, so the run can complete without Cursor ever running. Propagate a hard model-unsupported result to these callers instead of using eligibility false as a fallback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcpjam-inspector/server/utils/harness/harness-availability.ts, line 71:

<comment>On eval/synthetic turns, an external-account host with an ordinary model id now falls through to the emulated engine rather than being refused. `runAssistantTurn` interprets this `false` as `useHarness = false`, while `sessionSimulation/runner.ts` does not run `checkHarnessRuntimeAvailable`, so the run can complete without Cursor ever running. Propagate a hard model-unsupported result to these callers instead of using eligibility false as a fallback.</comment>

<file context>
@@ -60,7 +67,9 @@ export function harnessModelEligibleForRuntime(args: {
 }): boolean {
-  if (args.adapter.modelAccess === "external-account") return true;
+  if (args.adapter.modelAccess === "external-account") {
+    return isRuntimeChosenModelSentinel(args.modelId);
+  }
   if (!isHostedCatalogModel(args.modelId, args.provider)) return false;
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid, and you were right that this is the serious one — fixed in 6e3482f420.

Both halves of your claim check out. assistant-turn.ts:643 is literally const useHarness = harnessRequested && modelEligible; with a warn-and-continue into runChatEngineLoop. And sessionSimulation/runner.ts contains no reference to checkHarnessRuntimeAvailable at all — it drives turns straight through runUnifiedAssistantTurn; only its swarm caller admits targets through the pre-flight.

So the previous round's false did convert a mis-configured Cursor host into a completed emulated run stamped executionEngineLabel: harness:cursor. That is worse than the mis-attributed model id it was meant to fix, because the record contains nothing wrong-looking at all.

Fix. The dispatch now throws for an external-account adapter instead of degrading, wrapped in the same sentence the chat routes build around a pre-flight refusal. The brokered fallback is deliberately untouched and still degrades — there the emulated engine genuinely runs the refused model on org BYOK. The shared condition lives in externalAccountHostModelRefusalReason, read by both the pre-flight and the dispatch, so the two cannot drift while acting on it differently.

Non-vacuity: REFUSES an ordinary model rather than falling back to the emulated engine fails when the throw is removed — and fails with promise resolved "{ messages: [...] }" instead of rejecting, which is precisely the silent-completion mode. Worth noting the proof needed the suite's logger mock completed first (event/systemEvent); without it the reverted code died on a missing mock method and the test would have passed for the wrong reason.

Coherence with last round's refusal — the two cover disjoint causes and neither can become an emulated run: a correctly configured external-account host on a synthetic surface classifies as external-account and resolveTurnRuntime throws (the credential can never arrive there), firing first inside drainAssistantTurn; a mis-configured host classifies as byok/mcpjam, never reaches that arm, and is stopped by the new dispatch throw. Interactive rails fail closed at the pre-flight in both cases.

…e instead of degrading

Both re-review findings on the sentinel validation hold. Verified, and fixed so
that neither the rule nor its enforcement can become a run that never ran Cursor.

THE GATE VALIDATED THE WRONG ID. The external-account rule asks a question about
the HOST — does it carry the runtime's sentinel? — and nothing consumes the
turn's model on that arm, so holding the rule to `model.id` let a caller satisfy
it by POSTing `cursor/auto` at a host that pinned an ordinary id: the same
mis-configured host, admitted from the other side. The pre-flight now takes the
host's configured id as its own `hostModelId` input, and every call site supplies
it (both chat rails, both eval-admission halves, the swarm target gate). With no
model pinned at all it falls back to the turn's model, which is then the only
description of what that host runs — a sentinel body is true, an ordinary one is
still refused.

The route half of the same finding: the host-wins promotion for an
external-account harness is unconditional again. Last round I narrowed it to
hosts carrying the sentinel, which looks safer and is the opposite — it leaves
the body's model standing on exactly the hosts that must be refused, and the
body's model is what the pre-flight then sees.

ELIGIBILITY-FALSE WAS BEING READ AS "USE THE EMULATED ENGINE". `runAssistantTurn`
computes `useHarness = harnessRequested && modelEligible` and warns-and-emulates
otherwise. Sound for a brokered harness, where the emulated engine runs the
refused model itself on org BYOK. Not sound here: that engine cannot run a
sentinel at all, and the id a mis-configured host carries is one the runtime
would have ignored. What the fallback produced is worse than the bug the rule was
added for — a swarm or eval turn that completes, reports success, records
`executionEngineLabel: harness:cursor`, and never ran Cursor, indistinguishable
in the transcript from a real run. The interactive rails fail closed at the
pre-flight; the dispatch now throws the same sentence for the paths that never
call it (`sessionSimulation/runner.ts` drives turns with no pre-flight of its
own; only its swarm caller admits targets through one).

The condition itself moved into `externalAccountHostModelRefusalReason` so the
two readers cannot drift while acting on it differently — one returns a typed
refusal, the other throws.

Coherent with the refusal added last round, which covers a disjoint cause: a
CORRECTLY configured host on a synthetic surface still fails at
`resolveTurnRuntime`, because that surface can never deliver the runtime's own
credential. A MIS-configured host classifies as byok/mcpjam and never reaches
that arm, so it is this dispatch throw that stops it. Neither path can become an
emulated run.

Tests: the gate holds the host's id over a body-supplied sentinel, accepts a
sentinel host whatever the body says, and falls back when nothing is pinned; the
routes hand the pre-flight the promoted host model plus `hostModelId`; the
dispatch refuses an ordinary model with no engine run at all, while the brokered
fallback still degrades as before. Each proven non-vacuous by reverting the
production line — with the suite's logger mock completed first, so the
"no engine ran" assertion fails by the turn RESOLVING rather than by a missing
mock method.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ad20d66e-8062-43eb-8f77-87ac80732b67)

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mcpjam-inspector/server/services/evals/harness-admission.ts">

<violation number="1" location="mcpjam-inspector/server/services/evals/harness-admission.ts:345">
P1: When a `cursor/auto` host runs an eval case with an ordinary `test.model`, this admission accepts the case using the host id, but eval execution still sends the case model downstream. Promote the host model in the eval execution path too, or reject mismatched case models before admitting the run.</violation>
</file>

<file name="mcpjam-inspector/server/utils/harness/harness-availability.ts">

<violation number="1" location="mcpjam-inspector/server/utils/harness/harness-availability.ts:84">
P1: When a Cursor host pins `cursor/auto` but an eval case carries an ordinary model id, this helper validates the case id instead of the host id and rejects the turn. Pass the configured host model through the dispatch eligibility check, or otherwise preserve the host sentinel when evaluating external-account harnesses.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

return (
externalAccountHostModelRefusalReason({
adapter: args.adapter,
modelId: args.modelId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When a Cursor host pins cursor/auto but an eval case carries an ordinary model id, this helper validates the case id instead of the host id and rejects the turn. Pass the configured host model through the dispatch eligibility check, or otherwise preserve the host sentinel when evaluating external-account harnesses.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcpjam-inspector/server/utils/harness/harness-availability.ts, line 84:

<comment>When a Cursor host pins `cursor/auto` but an eval case carries an ordinary model id, this helper validates the case id instead of the host id and rejects the turn. Pass the configured host model through the dispatch eligibility check, or otherwise preserve the host sentinel when evaluating external-account harnesses.</comment>

<file context>
@@ -68,14 +78,59 @@ export function harnessModelEligibleForRuntime(args: {
+    return (
+      externalAccountHostModelRefusalReason({
+        adapter: args.adapter,
+        modelId: args.modelId,
+      }) === undefined
+    );
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid — fixed in 24b7df94bd, together with the admission finding below, because they are the same seam from opposite ends.

Evidence. evals-runner.ts built the case model with buildModelDefinition(test) and no host promotion anywhere; drive-hosted-eval-turn.ts passes that straight to runAssistantTurn. My round-2 change made admission's full check read the host's hostModelId, so a Cursor host + a case pinned to anthropic/claude-haiku-4.5 was admitted — and then the dispatch eligibility check saw the case id on an external-account harness and hit my round-2 throw. Admission and execution disagreed, and the casualty was an ordinary Cursor eval suite.

Fix: promote, not refuse. resolveEvalCaseModelDefinition returns the host's sentinel definition when the host runs an external-account harness carrying one, called once in runTestCase so everything downstream agrees — canonical id, the MCPJam-vs-BYOK split, the org-BYOK runtime lookup, the engine's wire payload, and what the iteration reports.

I did not take your other suggestion of passing the host model into the dispatch eligibility check: runAssistantTurn takes exactly one model, and by the time it runs that model is the host's on every surface (chat rails promote, eval now promotes, swarm pins it). A second model input would create a place for the two to disagree, which is the bug this finding is about.

Non-vacuity: promotes the host's sentinel over the case's own model and admits that case AND leaves it dispatchable — the two ends now agree both fail when the promotion is reverted. The second asserts both ends deliberately — asserting only admission is what let this through.

// The HOST's id, not the case's: the external-account rule is about
// this host carrying the runtime's sentinel, and a case model can no
// more answer that than a request body can.
...(fullCheckHostModelId ? { hostModelId: fullCheckHostModelId } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When a cursor/auto host runs an eval case with an ordinary test.model, this admission accepts the case using the host id, but eval execution still sends the case model downstream. Promote the host model in the eval execution path too, or reject mismatched case models before admitting the run.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcpjam-inspector/server/services/evals/harness-admission.ts, line 345:

<comment>When a `cursor/auto` host runs an eval case with an ordinary `test.model`, this admission accepts the case using the host id, but eval execution still sends the case model downstream. Promote the host model in the eval execution path too, or reject mismatched case models before admitting the run.</comment>

<file context>
@@ -328,6 +339,10 @@ export function checkEvalHarnessAdmission(args: {
+        // The HOST's id, not the case's: the external-account rule is about
+        // this host carrying the runtime's sentinel, and a case model can no
+        // more answer that than a request body can.
+        ...(fullCheckHostModelId ? { hostModelId: fullCheckHostModelId } : {}),
         xaaEnterprisePolicyOn,
       });
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid — fixed with the same change in 24b7df94bd (details on the harness-availability.ts:84 thread).

Of your two options I chose promotion over refusing at admission, and the deciding fact is one this PR established: the model pickers cannot hold cursor/auto, so no eval case can name it — refusing mismatched case models would make every Cursor eval suite unrunnable. Promotion is also just this PR's own rule finally reaching its last surface: the host's model is already authoritative on an external-account harness everywhere else.

A mis-configured host promotes nothing and is still refused at admission on its own id.

Comment thread mcpjam-inspector/server/routes/web/chat-v2.ts
Comment thread mcpjam-inspector/server/routes/web/__tests__/chat-v2.hosted.test.ts Outdated
Comment thread mcpjam-inspector/server/utils/__tests__/assistant-turn.test.ts Outdated
…al comes before the resolve

Both P1s hold and are the same seam from its two ends: admission accepted a case
on the HOST's id while execution carried the CASE's model, and the dispatch's
eligibility check then refused a run that had already been admitted. A per-case
model is normal and legitimate in evals — a suite names one per case and is then
pointed at a host — so this hit ordinary Cursor suites, and my round-2 change is
what put it there.

Fixed by making the host's model authoritative in eval execution too, the way it
already is on the chat rails, rather than by refusing the case. Refusing would be
self-consistent and would also make every Cursor eval suite unrunnable: the model
pickers cannot hold `cursor/auto`, so no case can name it. Resolved once in
`runTestCase`, so the canonical id, the MCPJam-vs-BYOK split, the org-BYOK
lookup, the engine's wire payload and what the iteration reports all agree — and
so the dispatch sees the sentinel and runs the harness.

A mis-configured host (external-account harness, ordinary pinned model) still
promotes nothing and is still refused at admission: that is a broken
configuration, not a case-level choice.

P2, the 15 s dependency reappearing on the mis-configured path: both chat rails
now answer the host-sentinel question BEFORE resolving the host's model
definition, so such a host fails immediately instead of first waiting on an
org-model-config lookup for an id no provider can list. Same shared sentence, in
front of the pre-flight rather than instead of it; the promotion below it stays
unconditional because by then the host is known to carry the sentinel.

BOTH TEST FINDINGS ALSO HOLD, and I cited both of these tests as proof last
round:

  - the hosted-route "a body-supplied sentinel cannot stand in" test asserted
    only `hostModelId`, which is passed unconditionally from
    `resolvedExecution.modelId` and is independent of the promotion block — so it
    could not have caught the promotion being removed. It proved the
    `hostModelId` wiring and nothing more. Rewritten around the early refusal: it
    now asserts the route 503s, names the host's id, and never reaches the
    pre-flight.
  - the assistant-turn "still runs the sentinel" test called
    `harnessModelEligibleForRuntime` directly and never invoked
    `runAssistantTurn`, so it duplicated a harness-availability unit test instead
    of covering the dispatch. Rewritten to stub `runHarnessTurn` and assert a
    `cursor/auto` turn reaches it while the emulated engine is never dialed —
    which is also what makes the refusal test meaningful, since a rule that
    refused every Cursor turn would satisfy "no silent emulation" too.

Five suites stubbed `harness-availability` wholesale, so the routes' new import
resolved to `undefined(...)` — a 500 that reads as a routing bug. They now spread
`actual`, with the note this repo already learned once.

Non-vacuity, each by reverting the production line: the eval promotion (2 tests),
the route early refusal (2), the sentinel reaching the harness (1), and the
round-2 dispatch throw re-proven against its rewritten assertions (1, failing by
the turn RESOLVING).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_0679dcdd-4d08-40d2-a076-1e4b69739022)

@chelojimenez
chelojimenez merged commit 92e7f1b into main Sep 2, 2026
18 of 19 checks passed
@chelojimenez
chelojimenez deleted the fix/cursor-auto-model-identity branch September 2, 2026 07:25
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