Skip to content

Adds codex support, Adds multi account view, small perf and security fixes#92

Closed
bberenberg wants to merge 5 commits into
ankitvgupta:mainfrom
bberenberg:codex/codex-provider-unified-accounts
Closed

Adds codex support, Adds multi account view, small perf and security fixes#92
bberenberg wants to merge 5 commits into
ankitvgupta:mainfrom
bberenberg:codex/codex-provider-unified-accounts

Conversation

@bberenberg

@bberenberg bberenberg commented Apr 16, 2026

Copy link
Copy Markdown

Summary

  • Adds Codex-backed LLM/provider support using the local Codex SDK/CLI, model selection, auth checks, and provider routing for built-in AI flows.
  • Hardens email privacy by blocking remote image loads and replacing external images with placeholders.
  • Adds a unified All accounts inbox view with persisted account selection and account-aware actions/search/snooze/drafts.
  • Fixes Gmail label-state reconciliation so archived/read changes in Gmail stop appearing stale locally, and reduces background prefetch/usage pressure.

Changes

  • Introduced shared LLM abstractions, Codex provider integration, model picker/settings, and Codex auth status plumbing.
  • Generalized draft generation/refinement, analysis, archive-ready checks, sender lookup, agent drafting, and UI copy away from Anthropic-only assumptions.
  • Switched Gmail history sync to unfiltered history, added current-label reconciliation, and cleaned up failed client/reauth handling.
  • Added remote image privacy replacement, tightened renderer CSP image sources, and updated tests.
  • Added All accounts selection, persisted selectedAccountId, account labels, and account-aware inbox actions.
  • Made dev relaunch show the initial window inactive to avoid stealing focus.

Tests

  • npm run lint
  • ./node_modules/.bin/tsc --noEmit
  • HOME=/tmp/exo-test-home PYTHON=/usr/bin/python3 npm test

The code in this PR was largely written by AI but I have been using this locally for a few days now.


Open with Devin

@greptile-apps

greptile-apps Bot commented Apr 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds three major features: (1) a Codex-backed LLM provider with model selection and an MCP-based tool bridge for the interactive agent, (2) a unified "All accounts" inbox view with persisted account selection and account-aware actions, and (3) email privacy hardening via remote image blocking (regex-based replacement + tightened CSP img-src from * to 'self' data: blob:).

The abstraction of llm-service.ts cleanly wraps Anthropic and Codex paths, and the multi-account batch-action refactors correctly route undo actions per account. One P1-level concern and a few P2 concerns are noted inline.

Confidence Score: 4/5

Safe to merge after addressing the fragile Codex auth string-match (P1); remaining findings are P2 improvements.

The PR has one P1 issue: the getCodexAuthStatus auth check relies on matching the literal string 'Logged in' in CLI stdout, which will silently misreport auth state if the CLI output ever changes. The multi-account and LLM abstraction changes are well-structured and the image-privacy layering (content replacement + CSP) is correct. All other findings are P2 — none block correctness today.

src/main/services/codex-cli.ts (auth detection), src/main/agents/providers/codex-agent-provider.ts (MCP bridge auth), src/main/services/email-sync.ts (newMessageIds dedup)

Security Review

  • Remote image blocking: The combined defense of replaceRemoteImageSources (content replacement) and the tightened CSP (img-src 'self' data: blob:) effectively blocks external image loads and tracking pixels. srcset attributes bypass the replacement but are still blocked by CSP.
  • MCP tool bridge (unauthenticated localhost): codex-agent-provider.ts creates an HTTP server on 127.0.0.1:0 with no auth token. During an active agent session any local process can invoke the registered email tools. Risk is limited by the ephemeral port and session duration, but worth hardening with a per-session bearer token.
  • Env var forwarding to Codex subprocess: buildCodexEnv() copies the full process.env — including ANTHROPIC_API_KEY and any other secrets — to the Codex CLI child process. This is likely intentional for PATH/locale inheritance but is worth an explicit allowlist to avoid unnecessary credential exposure.
  • No SQL injection, XSS, or auth-bypass issues were found in the changed code.

Important Files Changed

Filename Overview
src/main/services/codex-cli.ts New Codex SDK integration: CLI path resolution, prompt flattening, thread invocation, and auth status check — auth detection relies on fragile string matching ("Logged in") that could silently break if CLI output changes.
src/main/services/llm-service.ts Clean provider-agnostic LLM dispatch layer; correctly initialised at startup via index.ts; normalises Anthropic and Codex responses to a shared LlmResponse type.
src/main/agents/providers/codex-agent-provider.ts Codex interactive agent provider with MCP HTTP bridge for tool execution; bridge binds to localhost-only random port but lacks an auth token — any co-located process can call email tools during an active session.
src/shared/email-image-privacy.ts Regex-based remote image replacement with data-URI placeholders; correctly handles tracking pixels and clamped dimensions, but srcset attributes are not replaced (CSP blocks the load but shows broken-image icon instead of placeholder).
src/main/services/email-sync.ts Switches to unfiltered Gmail history (fixes missed label removals) and adds startup reconciliation of visible inbox labels; newMessageIds can accumulate duplicate IDs from both messagesAdded and labelAdded(INBOX) paths causing redundant Gmail API calls.
src/renderer/store/index.ts Adds account-aware email lookup map, custom areThreadingEmailsEqual equality function to reduce re-renders, persisted account selection, and multi-account support for isSentEmail/groupByThread — well-structured changes.
src/main/services/prefetch-service.ts Adds usage-limit backoff with 30-min cooldown, caps analysis backfill to 25 newest emails per run, and removes the archive-ready auto-queuing to reduce background API pressure.
src/main/db/index.ts Extracts parseLabelIds helper, adds getVisibleInboxLabelStates for reconciliation; the LIKE '%"INBOX"%' pattern works for current data but is slightly fragile compared to json_each.

Sequence Diagram

sequenceDiagram
    participant Renderer
    participant MainIPC
    participant LlmService
    participant AnthropicSvc
    participant CodexCli
    participant CodexAgent as CodexAgentProvider
    participant McpBridge as MCP HTTP Bridge
    participant ToolExecutor

    Renderer->>MainIPC: drafts:generate / drafts:refine
    MainIPC->>LlmService: createMessage(params, options)
    alt currentProvider == anthropic
        LlmService->>AnthropicSvc: createMessage()
        AnthropicSvc-->>LlmService: Message
    else currentProvider == codex
        LlmService->>CodexCli: createCodexMessage()
        CodexCli->>CodexCli: buildCodexPrompt()
        CodexCli-->>LlmService: LlmResponse
    end
    LlmService-->>MainIPC: LlmResponse
    MainIPC-->>Renderer: draft text

    Renderer->>MainIPC: agent:run providerIds=codex
    MainIPC->>CodexAgent: run(params)
    CodexAgent->>McpBridge: startToolBridge(tools)
    McpBridge->>McpBridge: listen on 127.0.0.1:0
    CodexAgent->>CodexCli: createCodexClient(mcp_servers)
    CodexCli-->>CodexAgent: thread
    CodexAgent->>CodexCli: thread.runStreamed(prompt)
    loop streaming events
        CodexCli-->>CodexAgent: ThreadEvent
        CodexAgent->>McpBridge: mcp_tool_call
        McpBridge->>ToolExecutor: toolExecutor(name, args)
        ToolExecutor-->>McpBridge: result
        McpBridge-->>CodexCli: MCP response
        CodexAgent-->>Renderer: AgentEvent via IPC
    end
    CodexAgent->>McpBridge: close()
Loading

Reviews (1): Last reviewed commit: "Add unified account inbox view" | Re-trigger Greptile

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 potential issue.

View 10 additional findings in Devin Review.

Open in Devin Review

Comment on lines +406 to +416
if (!state.completedToolIds.has(item.id)) {
state.completedToolIds.add(item.id);
yield {
type: "tool_call_end",
toolCallId: item.id,
result: {
status: item.status,
changes: item.changes,
},
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 file_change emits tool_call_end immediately without checking completion status

In mapCodexItemEvent, the file_change case emits both tool_call_start and tool_call_end on the very first event (e.g., item.started) because the guard only checks !state.completedToolIds.has(item.id), which is always true initially. This is inconsistent with every other item type handler: mcp_tool_call checks item.status !== "in_progress" (codex-agent-provider.ts:335), web_search checks eventType === "item.completed" (codex-agent-provider.ts:357), and command_execution checks item.status !== "in_progress" (codex-agent-provider.ts:379). The result is that tool_call_end fires with potentially stale/incomplete item.status and item.changes data from the item.started event, and subsequent item.updated/item.completed events for the same item are silently ignored.

Suggested change
if (!state.completedToolIds.has(item.id)) {
state.completedToolIds.add(item.id);
yield {
type: "tool_call_end",
toolCallId: item.id,
result: {
status: item.status,
changes: item.changes,
},
};
}
if (eventType === "item.completed" && !state.completedToolIds.has(item.id)) {
state.completedToolIds.add(item.id);
yield {
type: "tool_call_end",
toolCallId: item.id,
result: {
status: item.status,
changes: item.changes,
},
};
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +253 to +257
env: buildCodexEnv(),
...(config ? { config } : {}),
});
}

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 Fragile auth detection via CLI string matching

authenticated: combined.includes("Logged in") hardcodes the CLI's exact output phrase. If the Codex CLI ever changes this string (e.g. to "Authenticated", "Signed in", or a localized variant), the check silently returns authenticated: false even when the user is properly logged in — causing CodexAgentProvider.isAvailable() to fail and blocking all Codex-backed flows without any actionable error.

A more robust approach is to rely on the exit code alone: exit 0 → authenticated, non-zero → not authenticated (the current code already returns authenticated: false for non-zero exits). The string check adds fragility without much safety benefit, since a healthy login status command shouldn't exit 0 while unauthenticated.

resolve({
  cliAvailable: true,
  authenticated: true, // exit 0 from "login status" means authenticated
});

Comment on lines 1026 to 1035
@@ -939,10 +1033,19 @@ class EmailSyncService {
});
}
} catch (err) {

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 newMessageIds can contain the same ID twice in one history batch

After the switch to unfiltered history, an email that transitions from archived → inbox can appear in both messagesAdded (with labels.includes("INBOX")) and labelAdded(INBOX) in the same history window. Both branches push to newMessageIds, so the same email is fetched from Gmail and processed twice. saveEmail is an upsert so data stays consistent, but it doubles the Gmail API calls for those messages and may push duplicates through onNewEmails to the renderer.

A cheap fix is to deduplicate before the loop:

const uniqueNewMessageIds = [...new Set(changes.newMessageIds)];
for (const id of uniqueNewMessageIds) {

Comment on lines +272 to +280
});

await new Promise<void>((resolve, reject) => {
httpServer.once("error", reject);
httpServer.listen(0, "127.0.0.1", () => {
httpServer.removeListener("error", reject);
resolve();
});
});

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 security MCP tool bridge listens on localhost without an auth token

The bridge is bound to 127.0.0.1 which prevents remote access, but any other local process running during an active agent session can connect to http://127.0.0.1:{port}/mcp and invoke all registered email tools (read thread, send mail, archive, etc.) without any credential check. The port is ephemeral and unlisted, so real-world exploitability is very low, but it's worth noting as the tool set has meaningful side effects (email send, archive).

A low-overhead mitigation is to add a randomly-generated bearer token that the Codex client must present; the StreamableHTTPServerTransport can check it in the request handler before forwarding to MCP.

Comment on lines +54 to +71
/**
* Replace remote <img src="https://..."> loads with a local placeholder.
* Tiny tracking pixels are removed outright.
*/
export function replaceRemoteImageSources(html: string, useLightMode: boolean): string {
if (!html.includes("<img")) return html;

return html.replace(
/<img\b[^>]*\bsrc\s*=\s*(["'])(https?:\/\/[^"']+)\1[^>]*>/gi,
(fullTag: string) => {
if (isLikelyTrackingPixel(fullTag)) {
return "";
}

const width = clampDimension(
extractNumericAttribute(fullTag, "width"),
DEFAULT_PLACEHOLDER_WIDTH,
MAX_PLACEHOLDER_WIDTH,

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 srcset attribute bypasses the privacy replacement

replaceRemoteImageSources only targets the src attribute. Emails that use <img src="..." srcset="https://cdn.example.com/img@2x.jpg 2x"> will have their src replaced with a placeholder but their srcset still pointing to an external URL. The CSP change (img-src 'self' data: blob:) correctly blocks the load at the browser level, so there is no privacy leak — but the browser will show a broken placeholder image icon instead of the SVG placeholder, and the tracking pixel check won't suppress these images.

Consider also stripping or replacing srcset in the same replacement pass.

Comment thread src/main/db/index.ts
Comment on lines +942 to +950
export function getVisibleInboxLabelStates(accountId: string, limit: number): EmailLabelState[] {
const db = getDatabase();
const stmt = db.prepare(`
SELECT id, label_ids as labelIds
FROM emails
WHERE account_id = ? AND (label_ids IS NULL OR label_ids LIKE '%"INBOX"%')
ORDER BY date DESC
LIMIT ?
`);

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 LIKE-based JSON matching is fragile for label detection

label_ids LIKE '%"INBOX"%' relies on the serialized JSON containing the exact substring "INBOX". This works for standard JSON arrays today, but a hypothetical future label whose ID contains the substring INBOX could produce false positives. A more robust check uses SQLite's json_each:

WHERE account_id = ? AND (
  label_ids IS NULL OR EXISTS (
    SELECT 1 FROM json_each(label_ids) WHERE value = 'INBOX'
  )
)

SQLite has supported json_each since 3.38 (2022), and better-sqlite3 bundles its own SQLite, so this should be available.

This was referenced Apr 16, 2026
@ankitvgupta

Copy link
Copy Markdown
Owner

big change but i generally like the direction. each of the features in your title should be a separate PR. mind kicking those off?

the codex one overlaps a bunch with an ollama / open source agent PR im working on, so let me get that one in first, but the other ones seem easier to handle.

Copy link
Copy Markdown
Author

Breaking this out as requested. I split the non-Codex parts of this PR into three focused draft PRs:

I left the Codex/provider-routing work out of these follow-up PRs for now because of the overlap with the Ollama / open source agent work mentioned above.

I also folded in the directly relevant review feedback while splitting:

  • image privacy: strip remote srcset during placeholder replacement
  • Gmail sync: dedupe newMessageIds before fetch and use json_each for the new inbox label reconciliation queries

Closing this larger PR in favor of the smaller replacements above.

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.

2 participants