Add AWS Bedrock as an alternative API provider#112
Conversation
…nkitvgupta#24) Root cause: the blocklist regex /[;&|`$><\n\r]/ missed several shell bypass vectors — subshells via (), brace expansion via {}, line continuation via \, history expansion via !, and command substitution via $() — making it possible to chain commands past the filter. Fix: replace the blocklist with SAFE_COMMAND_PATTERN, an allowlist that only permits characters safe in shell arguments (letters, digits, spaces, common path/option chars). Anything not explicitly permitted is denied, which closes the entire class of bypass rather than playing whack-a-mole with metacharacters. The hook logic is extracted into bash-hook.ts so it can be imported and tested without triggering the Electron-specific require.resolve IIFE in the provider. 25 unit tests are added covering all bypass vectors from the issue plus legitimate CLI tool invocations.
Users without a direct Anthropic API key can now use AWS Bedrock to power all AI features (analysis, drafts, refinement, archive-ready, memory learning). The provider is selectable in Settings → Agent → Authentication with full credential management UI. Key changes: - New `src/main/lib/anthropic-client.ts` factory that returns an `AnthropicClient` (structural type satisfying both `Anthropic` and `AnthropicBedrock`) based on stored config - `BedrockConfigSchema` + `BEDROCK_MODEL_TIER_IDS` added to shared types - `apiProvider` and `bedrock` fields added to `ConfigSchema` - `getModelIdForFeature` is now provider-aware, returning Bedrock cross- region inference profile IDs when Bedrock is active - All service classes (EmailAnalyzer, DraftGenerator, CalendaringAgent, ArchiveReadyAnalyzer) accept an injected `AnthropicClient` instead of constructing one internally - All inline `new Anthropic()` calls in IPC handlers and learner services replaced with `createAnthropicClientFromConfig(getConfig())` - `settings:validate-bedrock-credentials` IPC handler for live credential validation before saving - SettingsPanel UI: provider toggle (Anthropic / AWS Bedrock) with region, access key, secret key, optional session token, and validate-and-save flow - Note in UI that the AI agent feature requires a direct Anthropic API key and is not available via Bedrock (Managed Agents is first-party only)
Greptile SummaryThis PR adds AWS Bedrock as an optional API provider alongside the existing direct Anthropic API key path. All AI services now accept an injected
Confidence Score: 4/5Safe to merge after fixing the provider-toggle persistence bug in SettingsPanel. One P1 bug: users on Bedrock who click the Anthropic API toggle are left with no path to complete the provider switch via the Save button since handleSaveApiKey does not write apiProvider. All other changes are well-structured. src/renderer/components/SettingsPanel.tsx — handleSaveApiKey must also persist apiProvider when the local selection differs from the saved config.
|
| Filename | Overview |
|---|---|
| src/renderer/components/SettingsPanel.tsx | New Bedrock credential UI with provider toggle; P1 bug: clicking "Anthropic API" tab hides the "Switch to Anthropic" button and the "Save" button for the API key does not update apiProvider, leaving the user stuck on Bedrock. |
| src/main/lib/anthropic-client.ts | New central factory; correctly uses structural AnthropicClient type to satisfy both SDK clients. Falls back to Anthropic when bedrock config is absent. |
| src/main/ipc/settings.ipc.ts | getModelIdForFeature dispatches to Bedrock model IDs when provider is bedrock; validate-bedrock-credentials handler uses HTTP status codes for error bucketing. |
| src/shared/types.ts | Adds BedrockConfig, resolveBedrockModelId with region-prefix derivation, and apiProvider field to ConfigSchema. Region prefix logic handles eu/ap/us but silently falls back to "us" for unsupported regions. |
| src/extensions/mail-ext-web-search/src/web-search-provider.ts | canEnrich now returns false when Bedrock is active, correctly guarding the web_search_20250305 Anthropic-only tool. Client is now injected via callback. |
| src/main/agents/providers/bash-hook.ts | Extracted and upgraded from claude-agent-provider.ts; switches from blocklist to allowlist pattern closing bypass vectors like subshells, brace expansion, and backslash continuation. Tests added. |
| src/main/services/draft-edit-learner.ts | Hardcoded Anthropic model IDs replaced with getModelIdForFeature; client injection via createAnthropicClientFromConfig. Extended thinking stream call should work on Bedrock for supported models. |
Sequence Diagram
sequenceDiagram
participant UI as SettingsPanel (renderer)
participant Preload as preload/index.ts
participant IPC as settings.ipc.ts
participant Factory as anthropic-client.ts
participant Bedrock as AnthropicBedrock SDK
participant Anthropic as Anthropic SDK
UI->>Preload: validateBedrockCredentials(region, keys)
Preload->>IPC: settings:validate-bedrock-credentials
IPC->>Bedrock: new AnthropicBedrock + messages.create(haiku, max_tokens:1)
Bedrock-->>IPC: success / HTTP error
IPC-->>UI: { success, error? }
UI->>Preload: settings.set({ apiProvider, bedrock })
Preload->>IPC: settings:set
IPC->>IPC: resetAnalyzer / resetArchiveReady / prefetchService.reset()
Note over IPC,Factory: On next AI call
IPC->>Factory: createAnthropicClientFromConfig(config)
alt apiProvider === bedrock and config.bedrock exists
Factory->>Bedrock: new AnthropicBedrock(region, keys)
Bedrock-->>Factory: AnthropicClient
else apiProvider === anthropic or no bedrock config
Factory->>Anthropic: new Anthropic()
Anthropic-->>Factory: AnthropicClient
end
Factory-->>IPC: AnthropicClient (structural type)
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
src/renderer/components/SettingsPanel.tsx:2079
**Provider toggle doesn't persist when switching from Bedrock → Anthropic**
Clicking "Anthropic API" only updates local React state (`setApiProvider("anthropic")`). When Bedrock is the currently-saved provider, this hides the Bedrock section — including the "Switch to Anthropic" button. The Anthropic API key form is revealed with its "Save" button, but `handleSaveApiKey` at line 480 only persists `anthropicApiKey`; it never writes `apiProvider: "anthropic"` to config. After clicking "Save", `generalConfig.apiProvider` stays `"bedrock"` and all AI calls keep going to Bedrock.
The simplest fix: have `handleSaveApiKey` also include the provider when the user's local selection differs from the saved one. Or, make the "Anthropic API" toggle button invoke `handleSwitchToAnthropic` directly (which already calls `window.api.settings.set({ apiProvider: "anthropic" })`) rather than only updating local state.
### Issue 2 of 2
src/renderer/components/SettingsPanel.tsx:530-534
**`handleSwitchToAnthropic` has no error handling**
If `window.api.settings.set` rejects, the error is swallowed silently — local state shows "Anthropic" as active but the saved config hasn't changed. A try/catch keeps UI state consistent with what was actually persisted.
Reviews (2): Last reviewed commit: "Address Greptile review: Bedrock web sea..." | Re-trigger Greptile
… IDs, typed error handling - Skip sender lookup enrichment when Bedrock is active (web_search_20250305 is Anthropic-only) - Replace static BEDROCK_MODEL_TIER_IDS with resolveBedrockModelId(tier, region) that derives the cross-region inference profile prefix (us/eu/ap) from the configured AWS region - Use HTTP status codes (401/403 → bad creds, 404 → model unavailable) in the Bedrock credential validator instead of fragile substring matching on error messages
|
Addressed all three Greptile review findings in commit be603e6: Fixed (P1) — Sender lookup broken on Bedrock Fixed (P2) — US-only cross-region inference profile IDs Fixed (P3) — Fragile substring matching on error messages |
| </h5> | ||
| <div className="flex gap-3"> | ||
| <button | ||
| onClick={() => setApiProvider("anthropic")} |
There was a problem hiding this comment.
Provider toggle doesn't persist when switching from Bedrock → Anthropic
Clicking "Anthropic API" only updates local React state (setApiProvider("anthropic")). When Bedrock is the currently-saved provider, this hides the Bedrock section — including the "Switch to Anthropic" button. The Anthropic API key form is revealed with its "Save" button, but handleSaveApiKey at line 480 only persists anthropicApiKey; it never writes apiProvider: "anthropic" to config. After clicking "Save", generalConfig.apiProvider stays "bedrock" and all AI calls keep going to Bedrock.
The simplest fix: have handleSaveApiKey also include the provider when the user's local selection differs from the saved one. Or, make the "Anthropic API" toggle button invoke handleSwitchToAnthropic directly (which already calls window.api.settings.set({ apiProvider: "anthropic" })) rather than only updating local state.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/renderer/components/SettingsPanel.tsx
Line: 2079
Comment:
**Provider toggle doesn't persist when switching from Bedrock → Anthropic**
Clicking "Anthropic API" only updates local React state (`setApiProvider("anthropic")`). When Bedrock is the currently-saved provider, this hides the Bedrock section — including the "Switch to Anthropic" button. The Anthropic API key form is revealed with its "Save" button, but `handleSaveApiKey` at line 480 only persists `anthropicApiKey`; it never writes `apiProvider: "anthropic"` to config. After clicking "Save", `generalConfig.apiProvider` stays `"bedrock"` and all AI calls keep going to Bedrock.
The simplest fix: have `handleSaveApiKey` also include the provider when the user's local selection differs from the saved one. Or, make the "Anthropic API" toggle button invoke `handleSwitchToAnthropic` directly (which already calls `window.api.settings.set({ apiProvider: "anthropic" })`) rather than only updating local state.
How can I resolve this? If you propose a fix, please make it concise.|
let's hold off on my ollama PR first bc that cleans up a lot of the code to have multiple backends |
|
Sounds good, @ankitvgupta — I'll hold this and rebase on top of the Ollama PR once it lands so the backend abstraction is aligned. Ping me whenever you'd like me to pick it up again. |
Summary
Adds AWS Bedrock as an optional API provider, allowing users who have AWS Bedrock access (but no direct Anthropic API key) to use all AI features in Exo.
Changes
New Infrastructure
src/main/lib/anthropic-client.ts(new): Central factory that returns eitherAnthropicorAnthropicBedrockbased on config. Uses a structural type{ messages: Pick<Anthropic["messages"], "create" | "stream"> }to satisfy both clients (they are siblings extendingBaseAnthropic, not parent-child).package.json: Added@anthropic-ai/bedrock-sdk@^0.29.1dependencyShared Types (
src/shared/types.ts)BEDROCK_MODEL_TIER_IDSmapping tiers to Bedrock cross-region inference profile IDs (us.anthropic.claude-*-v1:0)BedrockConfigSchema/BedrockConfig(region, accessKeyId, secretAccessKey, sessionToken)apiProviderandbedrockfields toConfigSchemaSettings & IPC
src/main/ipc/settings.ipc.ts:getModelIdForFeature()now returns Bedrock model IDs when Bedrock is active; addedsettings:validate-bedrock-credentialsIPC handler; resets services on provider changesrc/preload/index.ts: ExposesvalidateBedrockCredentialsto rendererServices (constructor injection)
All services now accept an injected
AnthropicClientinstead of callingnew Anthropic()internally:src/main/services/email-analyzer.tssrc/main/services/draft-generator.tssrc/main/services/calendaring-agent.tssrc/main/services/archive-ready-analyzer.tsIPC Handlers
Updated to create clients via
createAnthropicClientFromConfig:src/main/ipc/analysis.ipc.tssrc/main/ipc/archive-ready.ipc.tssrc/main/ipc/drafts.ipc.tssrc/main/ipc/memory.ipc.tssrc/main/services/draft-pipeline.tssrc/main/services/draft-edit-learner.tssrc/main/services/analysis-edit-learner.tsExtensions
src/extensions/mail-ext-web-search/src/web-search-provider.ts: AcceptsgetClient: () => AnthropicClientcallbacksrc/extensions/mail-ext-web-search/src/index.ts: Passes factory callbackUI (
src/renderer/components/SettingsPanel.tsx)Notes
AnthropicBedrockandAnthropicare sibling classes (both extendBaseAnthropic) — not a subclass relationship. Using a structural type avoids union-type inference issues in TypeScript.archive-ready.spec.tsandemail-analyzer.spec.ts(Playwright/Vitest runner conflict) are unrelated to these changes and exist onmain.