Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
96 changes: 93 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ limboo/
│ ├── paths.ts # assetPath() resolver
│ ├── sendCommand.ts # native menu/tray → renderer command bridge
│ ├── window/ # createWindow.ts (frameless, sandbox, icon) + windowState.ts
│ ├── managers/ # Settings, Notification, AppMenu, Tray managers
│ ├── managers/ # Settings, Notification, AppMenu, Tray managers (+ cursor/ auth)
│ ├── secrets/ # SecretStore.ts — safeStorage-encrypted secrets under userData/secrets/
│ └── ipc/ # registry.ts (handle wrapper) + *Handlers + registerAllIpc()
├── preload/
│ └── index.ts # the ONLY bridge — exposes window.limboo.{window,settings,system,app,events}
Expand Down Expand Up @@ -406,7 +407,12 @@ version is just a dev/baseline placeholder. To release: `git tag vX.Y.Z && git p
internal hosts. Resolve+check the target before connecting.
- **Secrets / API keys**: encrypt with Electron `safeStorage`, never plaintext
files; **redact secrets/tokens** before they reach
[`logger.ts`](src/main/logger.ts).
[`logger.ts`](src/main/logger.ts). This is now implemented:
[`src/main/secrets/SecretStore.ts`](src/main/secrets/SecretStore.ts) is the
safeStorage-backed store (opaque files under `userData/secrets/`, decrypt
only at spawn time, names-only logging) — new secrets go through it, and
`logger.ts` + `AgentManager.redact` already strip `crsr_` / `CURSOR_API_KEY`
material.
- **Terminal / Git engines**: spawn with **no `shell: true`** — pass argv arrays
(`spawn(cmd, [args])`); validate every `cwd`/path stays inside the session
repo (path-traversal guard) before touching the filesystem.
Expand Down Expand Up @@ -656,7 +662,91 @@ boot revalidation chains after `worktrees.recover().finally(retarget)`).
gated to the active session) + `resume:state-changed`. Settings under the **Memory
& Search** category (`settings.resume`, bounds in `RESUME_LIMITS`).

**Still open / future** — Repository clone/track UI, a dedicated Permission System
### Agent Adapter Architecture (multi-agent: Claude + Cursor) — IN PROGRESS

**Build-order item (1), Authentication, is BUILT; the rest is planned.** Limboo
is evolving from "a Claude integration" into a
multi-agent orchestration platform via an **Agent Adapter Architecture**: a thin
translation layer per agent runtime, with **nothing above the adapters changing**.
The UI never knows which agent is running — it only knows "the current session has
an active coding agent". Full research/design doc:
[`docs/agents/cursor-integration.txt`](docs/agents/cursor-integration.txt).

- **The seam is already narrow.** Claude coupling is concentrated in
`AgentManager.ts`; the `AgentEvent`/`AgentState`/`PermissionRequest` types, all
agent IPC channels, the preload namespace, `useAgentStore`, and the
Composer/permission/plan/timeline UI are provider-neutral and stay frozen. The
planned `AgentAdapter` interface covers exactly: executable/auth detection
(`probeHealth`), run invocation (`run(spec) → AsyncIterable<AdapterEvent>`),
per-provider options mapping (`buildOptions`), wire-format → `AgentEvent`
translation (`handleMessage`), tool-identity/permission gating (`makeCanUseTool`
tool-name sets, plan-capture style), resume-token get/set
(`agent_session_meta`), error classification (`classifyAgentError`), and
utility one-shots (`buildUtilityOptions`).
- **BUILT — build-order item (1), Authentication.** The Cursor auth layer is
live (auth only — Cursor still cannot *run*; `AGENT_MODELS` deliberately has
no Cursor entries, so it is structurally unselectable as the running agent):
- `src/main/managers/cursor/CursorAuthManager.ts` — lazy, fully local
classification (`not-installed` / `not-authenticated` / `authenticated-cli`
/ `authenticated-api-key`) honoring `settings.agent.cursor.preferredAuth`
(`auto` = key wins; `api-key` = a stored CLI login never classifies;
`cli-login` = a stored key is kept but ignored), single-flight
`cursor-agent login` child (timeout-killed, `dispose()` on quit),
manual-browser mode (`NO_OPEN_BROWSER=1`; the captured login URL must be
https, credential-free, AND on a Cursor-owned host — `cursor.com` /
`cursor.sh` or subdomain, dot-boundary match), `logout` (also clears stale
login state), API-key set/remove, and `getSpawnEnv()` (the only sanctioned
decrypt site; Phase 2's runtime hook — returns `{}` under `cli-login`).
Broadcasts secret-free `CursorAuthState` on `agent:cursor-auth-changed`.
- `src/main/managers/cursor/exec.ts` — argv-only runner (runGit idiom):
PATH probe + Windows `where.exe` fallback + `~/.local/bin/{cursor-agent,agent}`
install-dir probe (plain `agent` is never PATH-searched — collision risk);
`.cmd` shims run via `%ComSpec%` only with static-whitelisted literal args;
bounded output; `redactCursor()`.
- `src/main/secrets/SecretStore.ts` — the safeStorage secret store (§6).
- IPC: 7 `agent:cursor*` channels (`src/main/ipc/cursorHandlers.ts`, all via
`handle()`; key validated + never echoed), preload `agent.cursor.*`
namespace, `useAgentStore.cursorAuth` + actions.
- UI: `CursorProviderCard` under Settings › Agent › **Providers**, sharing
the provider layout with the Claude row via `ProviderStatusRow`
(`panels/ProviderCard.tsx`) — status renders as a lucide **icon + label**
pill (`cursorStatusMeta`/`lifecycleMeta.icon` in `features/agent/status.ts`;
e.g. Download + "Install CLI", never bare "Not installed" text). Shared
`ActionButton` lives in `settings/controls.tsx`; a `SegmentedControl`
exposes `preferredAuth`. `CursorMark` in `ProviderIcon.tsx`, catalog search
fields. Settings: `agent.cursor` (`preferredAuth`, `manualBrowserLogin`),
`SETTINGS_VERSION` 13, bounds in `CURSOR_LIMITS`. The
**Connection & reliability** section (`agent.connection`) is provider-neutral
and shared by every provider.
- **Cursor adapter, remaining build order:** (1) ~~Authentication~~ (BUILT,
above). (2) **Runtime** —
`@cursor/sdk` local runtime preferred (typed errors, `run.stream()`/`onDelta`,
`Agent.resume`, store under `userData`; native `@cursor/sdk-<os>-<arch>`
binaries need the same asar-unpack treatment as the Claude SDK executable);
fallback: spawn `cursor-agent --print --output-format stream-json
--stream-partial-output` and translate the NDJSON events. (3) **Permissions** —
translate Limboo's posture into a session-scoped `.cursor/cli.json`
(deny-first) + hooks (`preToolUse`/`beforeShellExecution`/`beforeReadFile`,
`failClosed`) bridged over a named pipe into the existing `PermissionRequest`
flow; `--force` only for the `auto` posture; Cursor sandbox on by default.
(4) **Context injection** — memory/search/resume blocks land via a generated
session-scoped rules file (Cursor auto-loads `AGENTS.md`/`CLAUDE.md`; no
system-prompt preset-append exists). (5) **MCP reuse** — expose
`limboo_memory`/`limboo_search` to Cursor (stdio bridge or SDK `customTools`)
so both agents share the same platform services. (6) **Worktrees** — always
pass `--workspace <resolveSessionRoot(...)>`, never Cursor's `-w` (Limboo's
WorktreeManager stays the single root resolver).
- **Config surface (remaining):** `AgentProvider` is already widened and the
Cursor glyph exists; still to do when the runtime lands — add Cursor entries
to `AGENT_MODELS`/`providerForModel()` (the deliberate "unselectable" guard),
a provider selector for the running agent, and de-Claude the copy in
Composer/Plan strings.
- **Later:** Cursor Cloud Agents (SSE-streamed remote runs; SSRF-allowlisted
fetch per §6) and an **ACP adapter** (`agent acp`, JSON-RPC over stdio) as the
universal route to any ACP-speaking agent.

**Still open / future** — the Agent Adapter Architecture above (Cursor as the
second first-class agent), repository clone/track UI, a dedicated Permission System
beyond the agent's `canUseTool`, merge-conflict resolution UI, remote management, and
stash. Local vector embeddings on top of BM25 (both Memory and Search rankings are
already fusion-ready) and recording File Writer mutations into the session activity
Expand Down
24 changes: 18 additions & 6 deletions docs/architecture/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ specific, implemented defense.
5. **Content-Security-Policy** — strict `self`-only in production (no eval); relaxed
only for Vite HMR in development. ([`src/main/index.ts`](../../src/main/index.ts))

6. **No shell execution** — git and the terminal are spawned with argv arrays via
`execFile` / `node-pty`, never `shell: true`, so there is no shell string to
inject into. ([`managers/git/exec.ts`](../../src/main/managers/git/exec.ts),
6. **No shell execution** — git, the terminal, and the Cursor CLI are spawned with
argv arrays via `execFile` / `spawn` / `node-pty`, never `shell: true`, so there
is no shell string to inject into. Windows `.cmd` shims for `cursor-agent` are
bridged through `%ComSpec%` only when every argument matches a static-literal
whitelist. ([`managers/git/exec.ts`](../../src/main/managers/git/exec.ts),
[`managers/cursor/exec.ts`](../../src/main/managers/cursor/exec.ts),
[`TerminalManager.ts`](../../src/main/managers/TerminalManager.ts))

7. **Path-traversal guards** — every renderer-supplied path is validated to stay
Expand All @@ -55,9 +58,18 @@ specific, implemented defense.
key (settings, workspace config, permission decisions, and the settings
deep-merge).

10. **Secret redaction** — secrets and tokens are redacted before anything reaches
the logger; embedded-credential remote URLs are redacted from git results and
logs. ([`logger.ts`](../../src/main/logger.ts))
10. **Secret redaction** — secrets and tokens (including `crsr_` Cursor keys and
`CURSOR_API_KEY=` values) are redacted before anything reaches the logger;
embedded-credential remote URLs are redacted from git results and logs.
([`logger.ts`](../../src/main/logger.ts))

11. **Encrypted secret storage** — the only credential Limboo holds on the user's
behalf (an optional Cursor API key) lives in the safeStorage-backed
[`SecretStore`](../../src/main/secrets/SecretStore.ts): encrypted at rest under
`userData/secrets/`, gated on `safeStorage.isEncryptionAvailable()`, decrypted
only at child-spawn time into the child **environment** (never argv, never IPC,
never logs), with a stale-blob self-heal on keychain resets. The renderer sees
only a `configured` boolean + timestamp.

## Input caps

Expand Down
29 changes: 27 additions & 2 deletions docs/architecture/subsystems/agent-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,33 @@ with `managers/memory/memoryTools.ts`.

`probeHealth()` checks for an existing sign-in (the `ANTHROPIC_API_KEY`,
`ANTHROPIC_AUTH_TOKEN`, or `CLAUDE_CODE_OAUTH_TOKEN` env vars, or the Claude Code
credentials file) and reports `AgentInstall`. Limboo stores no credentials.
`retryAuth()` forces a re-probe after the user signs in again.
credentials file) and reports `AgentInstall`. Limboo stores no Anthropic
credentials. `retryAuth()` forces a re-probe after the user signs in again.

### Cursor authentication (Agent Adapter Architecture, Phase 1)

Cursor is the second provider, currently **authentication only** — it cannot run
yet (`AGENT_MODELS` has no Cursor entries, so it is structurally unselectable as
the running agent). The code lives beside, not inside, this manager:

- `src/main/managers/cursor/CursorAuthManager.ts` — lazy local classification
(`not-installed` / `not-authenticated` / `authenticated-cli` /
`authenticated-api-key`), the interactive `cursor-agent login` child (single-
flight, timeout-killed, manual-browser mode via `NO_OPEN_BROWSER=1` with a
validated https URL surfaced to the UI), `logout`, and API-key lifecycle.
State broadcasts on `agent:cursor-auth-changed` and never carries secrets.
- `src/main/managers/cursor/exec.ts` — argv-only `cursor-agent` runner (the
`runGit` idiom): PATH resolution with a Windows `where.exe` fallback, batch
shims bridged through `%ComSpec%` only with static-whitelisted literal args,
bounded output, `redactCursor()` on everything captured.
- `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.
Comment on lines +46 to +49

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

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.


`status --format json` output is parsed defensively (whitelisted scalars only);
`crsr_` tokens and `CURSOR_API_KEY=` values are redacted centrally in
`logger.ts` and in this manager's `redact()`.

## Lifecycle and recovery

Expand Down
5 changes: 5 additions & 0 deletions docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ terminal/git drawer widths. These persist (debounced) as you resize.

The largest category. Highlights:

- **Providers** — connection status per coding agent. Claude Code reuses its own
local login. **Cursor** (authentication only for now) connects via
`cursor-agent login` — with an optional manual-browser mode that prints the
login URL — or a Cursor API key stored encrypted via the OS keychain
(`safeStorage`); the key is never written to settings files or shown again.
- **Model** — the Claude model the agent uses (Opus 4.8, Sonnet 4.6, Haiku 4.5).
- **Permission mode** — how aggressively tool calls are auto-approved.
- **Web search**, **auto-approve reads**, **max turns**.
Expand Down
18 changes: 18 additions & 0 deletions docs/guides/using-the-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ If none is found, the agent reports an `auth-required` state. Sign in to Claude
as usual, then use "retry auth" so Limboo re-probes. Questions about provider
pricing or model behavior belong with the agent / provider, not Limboo.

### 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.
Comment on lines +26 to +42

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

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.


## Plan vs implement

- **Plan mode** — the agent produces a review-first plan and stops. You approve,
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/ipc-channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This page mirrors that file. When in doubt, the source file is authoritative.
| Worktree | `worktree:list`, `worktree:prune`, `worktree:recreate`, `worktree:detach`, `worktree:getRepoConfig`, `worktree:ackConfig`, `worktree:runSetup` |
| Services | `service:list`, `service:start`, `service:stop`, `service:restart`, `script:run` |
| Agent | `agent:getInstall`, `agent:getState`, `agent:send`, `agent:stop`, `agent:getSnapshot`, `agent:permissionRespond`, `agent:clarificationRespond`, `agent:clearSession`, `agent:getDiagnostics`, `agent:clearRateLimit`, `agent:retryAuth` |
| Cursor auth | `agent:cursorGetAuthState`, `agent:cursorRefreshAuth`, `agent:cursorLoginStart`, `agent:cursorLoginCancel`, `agent:cursorLogout`, `agent:cursorSetApiKey`, `agent:cursorRemoveApiKey` |
| Plan mode | `agent:getPlan`, `agent:approvePlan`, `agent:rejectPlan`, `agent:regeneratePlan`, `agent:setPlanPinned`, `agent:listPlanRevisions`, `agent:restorePlanRevision` |
| File system | `fs:index`, `fs:getTree`, `fs:readFile`, `fs:getHistory`, `fs:reveal` |
| Terminal | `terminal:create`, `terminal:list`, `terminal:write`, `terminal:resize`, `terminal:kill`, `terminal:rename`, `terminal:clear` |
Expand Down Expand Up @@ -48,6 +49,7 @@ There is also one fire-and-forget renderer -> main channel (`IpcSends`, via
| `agent:event` | A structured agent event (message delta, tool call, file change, ...). |
| `agent:permission-request` | The agent needs the user to approve or deny a tool. |
| `agent:clarification-request` | The agent (AskUserQuestion) needs clarifying answers. |
| `agent:cursor-auth-changed` | The Cursor provider's auth state changed (probe / login / key set). |
| `fs:index-progress` | Progress of an in-flight workspace index pass. |
| `fs:tree-changed` | The active workspace's directory tree changed. |
| `terminal:data` | A chunk of PTY output (raw VT bytes). |
Expand Down
8 changes: 7 additions & 1 deletion docs/reference/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Settings are stored at `settings.json` under the OS user-data directory, deep-me
with defaults on load, clamped, and migrated when `SETTINGS_VERSION` changes. See
[the Settings subsystem](../architecture/subsystems/settings.md).

The current `SETTINGS_VERSION` is **11**.
The current `SETTINGS_VERSION` is **13**.

## appearance

Expand Down Expand Up @@ -64,6 +64,12 @@ display toggles.
`cursorBlink` `true`, `scrollback` `5000`, `copyOnSelect` `false`, `confirmKill`
`true`, `mirrorAgentCommands` `true`.

`agent.cursor` (Cursor provider — authentication only): `preferredAuth` `auto`
(one of `auto` / `api-key` / `cli-login`), `manualBrowserLogin` `false` (print the
login URL instead of auto-opening a browser). **No secrets live here** — the
optional Cursor API key is safeStorage-encrypted in a main-only file under
`userData/secrets/`, never in `settings.json`. Bounds in `CURSOR_LIMITS`.

## git

| Field | Default | Notes |
Expand Down
7 changes: 7 additions & 0 deletions docs/reference/window-limboo-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ Coding-agent orchestration and the structured event stream.
`retryAuth()`, `respondPermission(decision)`
- `onStateChanged(cb)`, `onEvent(cb)`, `onPermissionRequest(cb)` — subscriptions.
`onEvent` is the unified streaming timeline; see [Data flow](../architecture/data-flow.md).
- `cursor.*` — Cursor provider authentication (auth only; no run capability yet).
Capability-based: no method ever returns a credential; the API key crosses IPC
exactly once via `setApiKey` and is safeStorage-encrypted in the main process.
- `cursor.getAuthState() -> CursorAuthState`, `cursor.refreshAuth() -> CursorAuthState`
- `cursor.loginStart(manual?)`, `cursor.loginCancel()`, `cursor.logout()`
- `cursor.setApiKey(key)`, `cursor.removeApiKey()`
- `cursor.onAuthChanged(cb)` — subscription.

## fs

Expand Down
Loading
Loading