feat(agent): Cursor provider authentication — Agent Adapter Architecture step 1#56
Conversation
…ure step 1
Adds Cursor as a second, authentication-only provider beside Claude Code
(build-order item 1 of the Agent Adapter Architecture; no run capability
yet — AGENT_MODELS deliberately has no Cursor entries).
Main process:
- CursorAuthManager: lazy, fully local auth classification (not-installed /
not-authenticated / authenticated-cli / authenticated-api-key) honoring
settings.agent.cursor.preferredAuth; single-flight cursor-agent login
child with manual-browser mode (NO_OPEN_BROWSER=1) and a Cursor-host
allowlist on the captured login URL; logout; API key set/remove;
getSpawnEnv() decrypt-at-spawn seam for the future runtime adapter.
- exec.ts: argv-only cursor-agent runner (never shell:true), Windows
where.exe + ComSpec-bridged .cmd shims gated by a static-literal arg
whitelist, ~/.local/bin/{cursor-agent,agent} install-dir fallback,
bounded output, redactCursor().
- SecretStore: safeStorage-encrypted secrets under userData/secrets/
(atomic 0o600 writes, names-only logging, presence-only metadata).
- 7 validated agent:cursor* IPC channels via the handle() wrapper; the API
key crosses IPC exactly once and is never returned, logged, or echoed.
- Logger + AgentManager redaction for crsr_ tokens and CURSOR_API_KEY.
Renderer:
- CursorProviderCard in Settings › Agent › Providers: CLI sign-in
(incl. manual URL copy/open), encrypted API key, preferredAuth
segmented control, per-state actions.
- Shared ProviderStatusRow + icon-and-label status pills (lucide icons via
lifecycleMeta/cursorStatusMeta) used by both Claude and Cursor rows;
shared ActionButton in settings controls; provider-neutral
Connection & reliability copy; CursorMark glyph; catalog search fields.
- useAgentStore cursorAuth state + actions over window.limboo.agent.cursor.
Settings: agent.cursor { preferredAuth, manualBrowserLogin },
SETTINGS_VERSION 13, CURSOR_LIMITS bounds. Docs updated (CLAUDE.md §3/§6/§8,
reference + architecture + guides).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Security Review Summary
Reviewed the Cursor authentication integration focusing on security-critical code paths. The implementation demonstrates strong security practices including proper secret redaction, input validation, command injection prevention, and encrypted storage.
Critical Issue Found (1)
Race condition in secret file creation - The SecretStore creates temporary files with default permissions before applying 0o600, leaving encrypted secrets briefly readable by other processes. This must be fixed before merge.
Security Strengths
The PR demonstrates excellent security design:
- ✅ Secrets encrypted with Electron safeStorage, never plaintext
- ✅ API keys validated at IPC boundaries with strict regex (printable ASCII only, length-capped)
- ✅ Comprehensive redaction across logger, AgentManager, and CLI output
- ✅ Command execution uses argv arrays, never
shell: true - ✅ Windows batch shim arguments whitelisted via
SAFE_ARG_RE - ✅ Login URL domain allowlist prevents CLI spoofing attacks
- ✅ Secrets injected via child environment, never argv
- ✅ Defensive JSON parsing with whitelisted fields (prototype pollution discipline)
- ✅ All file operations bounded by timeouts and output limits
The architecture is well-designed with proper separation between authentication and runtime, and the decrypt-at-spawn pattern is exactly right. Once the file permission race condition is addressed, this will be a secure implementation.
You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.
| }; | ||
| const tmp = `${target}.tmp`; | ||
| fs.mkdirSync(this.dir(), { recursive: true }); | ||
| fs.writeFileSync(tmp, JSON.stringify(payload), { encoding: 'utf8', mode: 0o600 }); |
There was a problem hiding this comment.
🛑 Security Vulnerability: Race condition between tmp file creation and permission setting creates a window where secrets are readable by other processes. The mode 0o600 is applied AFTER the file content is written, leaving encrypted secrets temporarily exposed with default permissions (typically 0o644).
Call fs.openSync with mode 0o600, write to that file descriptor, then close and rename. This ensures the restrictive permissions are atomic with file creation.
| fs.writeFileSync(tmp, JSON.stringify(payload), { encoding: 'utf8', mode: 0o600 }); | |
| const fd = fs.openSync(tmp, 'w', 0o600); | |
| try { | |
| fs.writeSync(fd, JSON.stringify(payload), 0, 'utf8'); | |
| } finally { | |
| fs.closeSync(fd); | |
| } |
📝 WalkthroughWalkthroughThis PR adds a Cursor authentication-only provider adapter: encrypted secret storage (SecretStore), a safe CLI exec wrapper, a CursorAuthManager for login/logout/API-key/probe flows, IPC and preload bridging, renderer store/UI (CursorProviderCard), expanded redaction, shared types/constants, and documentation updates. ChangesCursor Authentication Feature
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant CursorProviderCard
participant useAgentStore
participant preload as window.limboo.agent.cursor
participant CursorAuthManager
participant SecretStore
participant CursorCLI as cursor-agent CLI
CursorProviderCard->>useAgentStore: cursorLoginStart(manual)
useAgentStore->>preload: loginStart(manual)
preload->>CursorAuthManager: loginStart(manual)
CursorAuthManager->>CursorCLI: spawn cursor-agent login
CursorCLI-->>CursorAuthManager: stdout/stderr (login URL / exit)
CursorAuthManager->>CursorAuthManager: probe() re-verify auth
CursorAuthManager-->>preload: broadcast agentCursorAuthChanged
preload-->>useAgentStore: onAuthChanged(state)
useAgentStore-->>CursorProviderCard: cursorAuth updated
CursorProviderCard->>useAgentStore: cursorSetApiKey(key)
useAgentStore->>preload: setApiKey(key)
preload->>CursorAuthManager: setApiKey(key)
CursorAuthManager->>SecretStore: set(CURSOR_API_KEY_SECRET, key)
SecretStore-->>CursorAuthManager: stored (encrypted)
CursorAuthManager->>CursorAuthManager: probe()
CursorAuthManager-->>preload: broadcast agentCursorAuthChanged
preload-->>useAgentStore: onAuthChanged(state)
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 `@docs/architecture/subsystems/agent-manager.md`:
- Around line 46-49: Update the Cursor API-key transport wording in the
agent-manager docs so it does not say the key is “never IPC’d”; instead,
describe that the key is sent once via cursor.setApiKey() to persist it, and
after that it is never echoed back, logged, or placed on argv. Keep the wording
aligned with the existing SecretStore and getSpawnEnv() behavior.
In `@docs/guides/using-the-agent.md`:
- Around line 26-42: The docs claim “never stores credentials” too broadly, but
the new Cursor section persists API keys encrypted in SecretStore, so narrow
that statement to Anthropic sign-in only or explicitly note Cursor as an
exception. Update the wording in the agent guide so the earlier
credential-storage claim stays accurate when describing the sign-in flow and the
Cursor API key flow, using the “Connecting Cursor (preview)” section as the
reference point.
In `@src/renderer/features/settings/panels/CursorProviderCard.tsx`:
- Around line 77-83: The CursorProviderCard save flow should not allow an empty
draft to be submitted, since saveKey still calls setApiKey(keyDraft.trim()) even
when the input is blank. Add disabled support to ActionButton in controls.tsx,
then update both “Save key” buttons in CursorProviderCard to use
disabled={!keyDraft.trim()} so saveKey is never invoked with an empty string and
the UI gives immediate feedback.
🪄 Autofix (Beta)
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: defaults
Review profile: CHILL
Plan: Pro
Run ID: ead9959e-dc8e-4eb2-b59c-33d7962da24b
📒 Files selected for processing (29)
CLAUDE.mddocs/architecture/security-model.mddocs/architecture/subsystems/agent-manager.mddocs/getting-started/configuration.mddocs/guides/using-the-agent.mddocs/reference/ipc-channels.mddocs/reference/settings.mddocs/reference/window-limboo-api.mdsrc/main/index.tssrc/main/ipc/cursorHandlers.tssrc/main/ipc/index.tssrc/main/logger.tssrc/main/managers/AgentManager.tssrc/main/managers/SettingsManager.tssrc/main/managers/cursor/CursorAuthManager.tssrc/main/managers/cursor/exec.tssrc/main/secrets/SecretStore.tssrc/preload/index.tssrc/renderer/components/brand/ProviderIcon.tsxsrc/renderer/features/agent/status.tssrc/renderer/features/settings/catalog.tsxsrc/renderer/features/settings/controls.tsxsrc/renderer/features/settings/panels/AgentPanel.tsxsrc/renderer/features/settings/panels/CursorProviderCard.tsxsrc/renderer/features/settings/panels/ProviderCard.tsxsrc/renderer/stores/useAgentStore.tssrc/shared/constants.tssrc/shared/ipc-channels.tssrc/shared/types.ts
| - `src/main/secrets/SecretStore.ts` — the app's safeStorage-backed secret store | ||
| (first consumer). The Cursor API key is encrypted at rest under | ||
| `userData/secrets/`, decrypted only at child-spawn time (`getSpawnEnv()`), | ||
| and never IPC'd, logged, or placed on argv. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Clarify the API-key transport wording.
The new Cursor section says the API key is "never IPC'd", but cursor.setApiKey() still has to cross the renderer→main boundary once to persist it. Rephrase this to say the key is never echoed back, logged, or placed on argv after that initial setter call.
🤖 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 `@docs/architecture/subsystems/agent-manager.md` around lines 46 - 49, Update
the Cursor API-key transport wording in the agent-manager docs so it does not
say the key is “never IPC’d”; instead, describe that the key is sent once via
cursor.setApiKey() to persist it, and after that it is never echoed back,
logged, or placed on argv. Keep the wording aligned with the existing
SecretStore and getSpawnEnv() behavior.
| ### Connecting Cursor (preview) | ||
|
|
||
| Settings › Agent › Providers has a **Cursor** card. Running Cursor agents arrives | ||
| in a later update — connecting your account now just gets it ready. Two paths: | ||
|
|
||
| - **Sign in** — Limboo runs `cursor-agent login`; the CLI authenticates in your | ||
| browser and keeps its own credentials (Limboo never reads or copies them). | ||
| Enable **Manual browser login** on remote/headless machines: the login URL is | ||
| printed instead, with Copy URL / Open Browser actions. | ||
| - **API key** — paste a key from the Cursor Dashboard. It is encrypted with the | ||
| OS keychain (Electron `safeStorage`), stored outside the settings file, never | ||
| shown again, and injected only into Cursor child processes as `CURSOR_API_KEY`. | ||
| A `CURSOR_API_KEY` already present in your environment is detected and wins. | ||
|
|
||
| The card classifies the state locally (no network): Not installed → Sign in | ||
| required → Connected (via CLI login or API key). **Refresh** re-probes after you | ||
| install the CLI or sign in elsewhere. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Scope the "never stores credentials" claim.
This new Cursor section makes the page's earlier blanket statement inaccurate as written, because Cursor API keys are now persisted encrypted in SecretStore. Narrow the wording to Anthropic sign-in only, or explicitly call out Cursor as the exception.
🤖 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 `@docs/guides/using-the-agent.md` around lines 26 - 42, The docs claim “never
stores credentials” too broadly, but the new Cursor section persists API keys
encrypted in SecretStore, so narrow that statement to Anthropic sign-in only or
explicitly note Cursor as an exception. Update the wording in the agent guide so
the earlier credential-storage claim stays accurate when describing the sign-in
flow and the Cursor API key flow, using the “Connecting Cursor (preview)”
section as the reference point.
| const saveKey = async () => { | ||
| const ok = await setApiKey(keyDraft.trim()); | ||
| if (ok) { | ||
| setKeyDraft(''); | ||
| setShowKeyInput(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Disable "Save key" when the draft is empty.
saveKey calls setApiKey(keyDraft.trim()) without guarding against an empty string, so both "Save key" buttons fire an IPC round-trip that the main process must reject. Adding a disabled prop to ActionButton and gating the buttons on !keyDraft.trim() prevents the call and gives immediate visual feedback.
Proposed fix
Add disabled support to ActionButton in controls.tsx:
export function ActionButton({
label,
onClick,
danger,
primary,
}: {
label: string;
onClick: () => void;
danger?: boolean;
primary?: boolean;
+ disabled?: boolean;
}) {
return (
<button
type="button"
onClick={onClick}
+ disabled={disabled}
className={cn(
'rounded-md border px-2 py-1 text-[11px] transition-colors',
primary
? 'border-accent/50 bg-elevated text-fg hover:border-accent'
: danger
? 'border-line bg-surface-2 text-danger hover:border-danger'
: 'border-line bg-surface-2 text-muted hover:text-fg',
)}
>
{label}
</button>
);
}Then gate both save buttons in CursorProviderCard.tsx:
- <ActionButton label="Save key" primary onClick={() => void saveKey()} />
+ <ActionButton label="Save key" primary disabled={!keyDraft.trim()} onClick={() => void saveKey()} />Apply the same disabled={!keyDraft.trim()} to the second "Save key" button at line 242.
Also applies to: 217-217, 242-242
🤖 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 `@src/renderer/features/settings/panels/CursorProviderCard.tsx` around lines 77
- 83, The CursorProviderCard save flow should not allow an empty draft to be
submitted, since saveKey still calls setApiKey(keyDraft.trim()) even when the
input is blank. Add disabled support to ActionButton in controls.tsx, then
update both “Save key” buttons in CursorProviderCard to use
disabled={!keyDraft.trim()} so saveKey is never invoked with an empty string and
the UI gives immediate feedback.
Summary
Adds Cursor as a second, authentication-only agent provider beside Claude Code — build-order item (1) of the Agent Adapter Architecture. Users can connect their Cursor account today (interactive CLI sign-in or an encrypted API key) so the account is ready when the runtime adapter lands; Cursor cannot run yet —
AGENT_MODELSdeliberately has no Cursor entries, so it is structurally unselectable as the running agent.Everything above the adapter seam is untouched: sessions, worktrees, git, terminal, memory, search, resume, and the whole agent UI stay provider-neutral.
What's new
🔐 Authentication (main process)
CursorAuthManager— lazy, fully local classification (not-installed→not-authenticated→authenticated-cli/authenticated-api-key), no network probes, nothing spawns at boot. Two deliberately distinct paths:cursor-agent login(argv-only, single-flight, timeout-killed, verified afterwards viastatus --format json). The CLI keeps its own credentials; Limboo never reads, copies, or exports them. Manual-browser mode (NO_OPEN_BROWSER=1) surfaces the printed login URL for Copy/Open.SecretStore(userData/secrets/, atomic0o600writes, names-only logging). Classification checks presence only; decryption is confined togetSpawnEnv()— the decrypt-at-spawn seam reserved for the Phase-2 runtime.exec.ts— safecursor-agentrunner:execFile/spawnwith argv arrays (nevershell: true), secrets ride only in the child env (never argv), bounded output, Windowswhere.exeresolution with.cmdshims bridged through%ComSpec%only for static-literal whitelisted args, and a~/.local/bin/{cursor-agent,agent}install-dir fallback for PATH-less GUI launches.preferredAuthhonored end-to-end —auto(key wins),api-key(a stored CLI login never classifies; the status probe is skipped),cli-login(a stored key is kept but ignored, andgetSpawnEnv()returns{}).🛡️ Security hardening
CursorAuthState) is secret-free.agent:cursor*channels all go through the trusted-senderhandle()wrapper.cursor.com/cursor.shor subdomain, dot-boundary match) before the UI may open it.crsr_…tokens andCURSOR_API_KEY=…are masked in the logger,AgentManager.redact(), and all captured CLI output before anything reaches disk or the renderer.status --format jsonparsed defensively: whitelisted scalar fields only, length-capped, no unknown-key iteration (prototype-pollution discipline).🎨 UI (same pure-black theme, shared components)
CursorProviderCardin Settings › Agent › Providers: per-state actions (sign in / cancel / retry, manual-URL copy+open, logout, re-authenticate, API key save/replace/remove, refresh, install guide, dashboard), keychain-unavailable warning, and a Preferred authentication segmented control.ProviderStatusRow— Claude and Cursor render the identical provider row; status pills are now icon + label (lucideCheckCircle2Connected ·KeyRoundSign in required ·DownloadInstall CLI · spinningLoader2Checking) instead of bare text like "Not installed".ActionButtonin settings controls; provider-neutral Connection & reliability copy (agent.connectionis shared by every provider);CursorMarkglyph inProviderIcon; settings deep-search entries for every new field.cursor.com/dashboard/api).⚙️ Config & docs
settings.agent.cursor(preferredAuth,manualBrowserLogin),SETTINGS_VERSION→ 13,CURSOR_LIMITSbounds,AgentProviderwidened to'anthropic' | 'cursor'.docs/reference/*(IPC channels, settings, window.limboo API), architecture security model + agent-manager subsystem, user guide.Verified against the official Cursor docs
NO_OPEN_BROWSER=1(print login URL),status/whoami --format json,CURSOR_API_KEYenv /--api-keyflag, Dashboard → API Keys URL, and theagentbinary naming — cross-checked with cursor.com/docs (CLI authentication + parameters references).Testing
npx vite build --config vite.renderer.config.mts✅npm run lint✅preferredAuthre-classifies live.Out of scope (next build-order items)
Runtime adapter (
@cursor/sdk/--output-format stream-json), permission bridging (.cursor/cli.json+ hooks), context injection via rules files, MCP reuse, and worktree wiring — tracked in CLAUDE.md §8.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes