Skip to content

feat: multi-provider support via OpenClaude (env var injection)#3

Merged
DavidsonGomes merged 12 commits intoEvolutionAPI:developfrom
NeritonDias:feat/multi-provider-support
Apr 11, 2026
Merged

feat: multi-provider support via OpenClaude (env var injection)#3
DavidsonGomes merged 12 commits intoEvolutionAPI:developfrom
NeritonDias:feat/multi-provider-support

Conversation

@NeritonDias
Copy link
Copy Markdown
Contributor

@NeritonDias NeritonDias commented Apr 11, 2026

EvoNexus now supports multiple AI providers beyond Anthropic. When the user selects a non-Anthropic provider (OpenRouter, OpenAI, Gemini, Codex Auth, AWS Bedrock, Vertex AI), the system uses the openclaude binary (npm install -g @gitlawb/openclaude) and injects provider-specific environment variables (CLAUDE_CODE_USE_OPENAI, OPENAI_BASE_URL, etc.) into the spawned CLI process. Anthropic continues using the native claude binary with no extra config.

How it works

Provider config is stored in config/providers.json with:

  • active_provider: which provider is active ("anthropic", "openrouter", etc.)
  • providers.<id>.cli_command: which binary to spawn ("claude" or "openclaude")
  • providers.<id>.env_vars: env vars to inject into the CLI process

When a terminal session or ADW routine starts, the system:

  1. Reads config/providers.json
  2. Resolves the CLI binary (claude or openclaude)
  3. Merges provider env vars into the process environment
  4. Spawns the CLI with the correct binary + env vars

Changes

New files

  • config/providers.json — Provider registry with 7 providers (Anthropic, OpenRouter, OpenAI, Gemini, Codex Auth, Bedrock, Vertex)
  • dashboard/backend/routes/providers.py — REST API for provider management: list, get/set active, get/update config, test connection. API keys are masked in responses for security.
  • dashboard/frontend/src/pages/Providers.tsx — Dashboard page with provider cards, configuration modal (env var forms), test connection button, and activate/deactivate flow. Uses Evolution dark theme.

Modified files

  • dashboard/terminal-server/src/claude-bridge.js — Added _loadProviderConfig() to read active provider from config/providers.json. findClaudeCommand() now resolves either claude or openclaude based on provider config. startSession() merges provider env vars into the PTY spawn environment.
  • ADWs/runner.py — Added _get_provider_config() to read CLI command and env vars from config/providers.json. run_claude() now uses the active provider's binary and injects env vars into subprocess.Popen().
  • setup.py — Added choose_provider() step to the setup wizard. Lets user pick from 7 providers, checks if openclaude is installed, prompts for provider-specific env vars (API key, base URL, model), and saves to config/providers.json.
  • dashboard/backend/app.py — Registered providers blueprint.
  • dashboard/frontend/src/App.tsx — Added /providers route.
  • dashboard/frontend/src/components/Sidebar.tsx — Added Providers nav item under System section.
  • .env.example — Added AI Provider Configuration section with examples for OpenRouter, OpenAI, and Gemini.

Supported providers

Provider Binary Key env vars
Anthropic claude (native auth)
OpenRouter openclaude CLAUDE_CODE_USE_OPENAI, OPENAI_BASE_URL, KEY, MODEL
OpenAI openclaude CLAUDE_CODE_USE_OPENAI, OPENAI_API_KEY, MODEL
Gemini openclaude CLAUDE_CODE_USE_GEMINI, GEMINI_API_KEY, MODEL
Codex Auth openclaude CLAUDE_CODE_USE_OPENAI, OPENAI_API_KEY
AWS Bedrock openclaude CLAUDE_CODE_USE_BEDROCK, AWS_REGION, TOKEN
Vertex AI openclaude CLAUDE_CODE_USE_VERTEX, PROJECT_ID, REGION

Summary by Sourcery

Add multi-provider AI support configurable via providers.json, wiring the chosen provider’s CLI binary and environment variables into both the ADW runner and terminal server, and exposing provider selection and status in the dashboard and setup flow.

New Features:

  • Introduce setup-time AI provider selection with persisted configuration in config/providers.json.
  • Expose a dashboard Providers page to view, configure, activate, and test AI providers with masked secrets.
  • Add backend REST endpoints to list providers, manage the active provider, update provider configuration, and test provider connectivity.

Enhancements:

  • Update the terminal Claude bridge and ADW runner to resolve the active provider’s CLI (claude or openclaude) and inject provider-specific environment variables into spawned processes.
  • Extend the dashboard routing and sidebar navigation to include the new Providers management page.

Documentation:

  • Extend the example environment configuration to cover AI provider-related settings.

DavidsonGomes and others added 2 commits April 10, 2026 22:48
EvoNexus now supports multiple AI providers beyond Anthropic. When the
user selects a non-Anthropic provider (OpenRouter, OpenAI, Gemini, Codex
Auth, AWS Bedrock, Vertex AI), the system uses the `openclaude` binary
(npm install -g @gitlawb/openclaude) and injects provider-specific
environment variables (CLAUDE_CODE_USE_OPENAI, OPENAI_BASE_URL, etc.)
into the spawned CLI process. Anthropic continues using the native
`claude` binary with no extra config.

## How it works

Provider config is stored in `config/providers.json` with:
- `active_provider`: which provider is active ("anthropic", "openrouter", etc.)
- `providers.<id>.cli_command`: which binary to spawn ("claude" or "openclaude")
- `providers.<id>.env_vars`: env vars to inject into the CLI process

When a terminal session or ADW routine starts, the system:
1. Reads `config/providers.json`
2. Resolves the CLI binary (claude or openclaude)
3. Merges provider env vars into the process environment
4. Spawns the CLI with the correct binary + env vars

## Changes

### New files
- `config/providers.json` — Provider registry with 7 providers
  (Anthropic, OpenRouter, OpenAI, Gemini, Codex Auth, Bedrock, Vertex)
- `dashboard/backend/routes/providers.py` — REST API for provider
  management: list, get/set active, get/update config, test connection.
  API keys are masked in responses for security.
- `dashboard/frontend/src/pages/Providers.tsx` — Dashboard page with
  provider cards, configuration modal (env var forms), test connection
  button, and activate/deactivate flow. Uses Evolution dark theme.

### Modified files
- `dashboard/terminal-server/src/claude-bridge.js` — Added
  `_loadProviderConfig()` to read active provider from
  config/providers.json. `findClaudeCommand()` now resolves either
  `claude` or `openclaude` based on provider config. `startSession()`
  merges provider env vars into the PTY spawn environment.
- `ADWs/runner.py` — Added `_get_provider_config()` to read CLI command
  and env vars from config/providers.json. `run_claude()` now uses the
  active provider's binary and injects env vars into subprocess.Popen().
- `setup.py` — Added `choose_provider()` step to the setup wizard.
  Lets user pick from 7 providers, checks if openclaude is installed,
  prompts for provider-specific env vars (API key, base URL, model),
  and saves to config/providers.json.
- `dashboard/backend/app.py` — Registered providers blueprint.
- `dashboard/frontend/src/App.tsx` — Added /providers route.
- `dashboard/frontend/src/components/Sidebar.tsx` — Added Providers
  nav item under System section.
- `.env.example` — Added AI Provider Configuration section with
  examples for OpenRouter, OpenAI, and Gemini.

## Supported providers

| Provider     | Binary      | Key env vars                                    |
|-------------|-------------|------------------------------------------------|
| Anthropic   | claude      | (native auth)                                   |
| OpenRouter  | openclaude  | CLAUDE_CODE_USE_OPENAI, OPENAI_BASE_URL, KEY, MODEL |
| OpenAI      | openclaude  | CLAUDE_CODE_USE_OPENAI, OPENAI_API_KEY, MODEL   |
| Gemini      | openclaude  | CLAUDE_CODE_USE_GEMINI, GEMINI_API_KEY, MODEL   |
| Codex Auth  | openclaude  | CLAUDE_CODE_USE_OPENAI, OPENAI_API_KEY          |
| AWS Bedrock | openclaude  | CLAUDE_CODE_USE_BEDROCK, AWS_REGION, TOKEN       |
| Vertex AI   | openclaude  | CLAUDE_CODE_USE_VERTEX, PROJECT_ID, REGION       |

https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 11, 2026

Reviewer's Guide

Implements multi-provider AI support by introducing a providers registry, backend API, and frontend management UI, and wiring provider-specific CLI commands and environment variables into both the terminal bridge and ADW runner while extending setup to configure providers interactively.

Sequence diagram for terminal session startup with provider-specific CLI and env vars

sequenceDiagram
  participant TerminalClient
  participant ClaudeBridge as ClaudeBridge_js
  participant ProvidersJSON as providers_json
  participant CLI as AI_CLI_binary

  TerminalClient->>ClaudeBridge: startSession(workingDir, agent, ...)
  activate ClaudeBridge
  ClaudeBridge->>ClaudeBridge: _loadProviderConfig()
  ClaudeBridge->>ProvidersJSON: read config/providers.json
  ProvidersJSON-->>ClaudeBridge: {active_provider, providers[active]}
  ClaudeBridge->>ClaudeBridge: findClaudeCommand()
  ClaudeBridge-->>TerminalClient: sessionId
  ClaudeBridge->>CLI: "spawn(claudeCommand, args, [ env = process.env + provider.env_vars + TERM,FORCE_COLOR,COLORTERM ])"
  deactivate ClaudeBridge
Loading

Sequence diagram for ADW run_claude using active provider

sequenceDiagram
  participant ADWTask as ADW_task
  participant Runner as runner_py
  participant ProvidersJSON as providers_json
  participant CLI as AI_CLI_binary

  ADWTask->>Runner: run_claude(prompt, log_name, timeout, agent)
  activate Runner
  Runner->>Runner: _get_provider_config()
  Runner->>ProvidersJSON: read config/providers.json
  ProvidersJSON-->>Runner: cli_command, env_vars
  Runner->>Runner: build cmd = [cli_command, --print, --dangerously-skip-permissions, --output-format json, optional --agent]
  Runner->>CLI: subprocess.Popen(cmd, cwd=WORKSPACE, env=os.environ + provider_env + TERM=dumb)
  CLI-->>Runner: streaming stdout/stderr
  Runner->>Runner: _log_to_file(...)
  Runner-->>ADWTask: result dict
  deactivate Runner
Loading

ER diagram for providers.json registry structure

erDiagram
  PROVIDER_REGISTRY {
    string active_provider
  }

  PROVIDER {
    string id
    string name
    string description
    string cli_command
    boolean requires_logout
    string setup_hint
    string default_model
    string default_base_url
    string default_region
  }

  ENV_VAR {
    string key
    string value
  }

  PROVIDER_REGISTRY ||--o{ PROVIDER : providers
  PROVIDER ||--o{ ENV_VAR : env_vars
Loading

File-Level Changes

Change Details Files
Add configurable multi-provider registry and setup-time provider selection.
  • Introduce config/providers.json schema with active_provider, provider definitions, CLI command, and env vars per provider.
  • Extend setup.py with choose_provider() to select a provider, ensure openclaude is installed when required, prompt for provider-specific env vars, and persist them to providers.json.
  • Adjust setup main() flow to run the provider selection step before collecting user info.
config/providers.json
setup.py
Make terminal server sessions respect active provider CLI and env vars.
  • Load active provider configuration from config/providers.json at ClaudeBridge construction time, with safe defaults and logging.
  • Resolve claude vs openclaude binary in findClaudeCommand() based on providerConfig.cli_command and update error messages accordingly.
  • Inject provider-specific env vars into the spawned PTY process environment and support runtime reload of provider config when changed via API.
dashboard/terminal-server/src/claude-bridge.js
Make ADW runner use active provider binary and injected env vars.
  • Add _get_provider_config() helper to read CLI command and env vars from config/providers.json with error handling.
  • Update run_claude() to construct the command using the configured CLI, add provider env vars to the subprocess environment, and label output with the provider CLI when not using native claude.
  • Keep existing logging, timeout, and streaming behavior otherwise intact.
ADWs/runner.py
Expose provider management over the backend API, including masking of secrets and CLI status/testing.
  • Create a Flask blueprint with endpoints to list providers, get/set the active provider, get/update a provider’s env var config, and test CLI connectivity.
  • Implement helpers to read/write providers.json, check CLI installation and version, mask secrets in env vars, and compute whether a provider is considered configured.
  • Return overall claude/openclaude installation status alongside provider metadata for frontend consumption.
dashboard/backend/routes/providers.py
Add dashboard UI for viewing, configuring, testing, and activating providers.
  • Introduce Providers React page that fetches providers, shows install/config/active status, and displays per-provider cards with actions to configure, activate, and test.
  • Implement configuration modal that edits env vars (with labels, masking, defaults) and posts updates, plus inline and modal test results using the backend test endpoint.
  • Wire the page into the app by adding the /providers route and a Providers nav item under the System section.
dashboard/frontend/src/pages/Providers.tsx
dashboard/frontend/src/App.tsx
dashboard/frontend/src/components/Sidebar.tsx
Document provider-related environment configuration.
  • Extend .env.example with AI provider configuration examples for OpenRouter, OpenAI, Gemini, and related env vars to guide users.
.env.example
dashboard/backend/app.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 5 security issues, 1 other issue, and left some high level feedback:

Security issues:

  • Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
  • Detected user input entering a subprocess call unsafely. This could result in a command injection vulnerability. An attacker could use this vulnerability to execute arbitrary commands on the host, which allows them to download malware, scan sensitive data, or run any command they wish on the server. Do not let users choose the command to run. In general, prefer to use Python API versions of system commands. If you must use subprocess, use a dictionary to allowlist a set of commands. (link)
  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
  • Detected subprocess function 'test_provider' with user controlled data. A malicious actor could leverage this to perform command injection. You may consider using 'shlex.escape()'. (link)

General comments:

  • The provider registry/defaults are now defined in multiple places (checked-in config/providers.json, the setup.py fallback template, and some frontend assumptions like ENV_VAR_LABELS/PROVIDER_COLORS); consider centralizing this schema/metadata in a single source of truth to avoid future drift between installer, backend, and UI.
  • ClaudeBridge caches providerConfig at construction and exposes reloadProviderConfig(), but nothing in this diff invokes it from the providers API; if provider changes are meant to apply without restarting the terminal server, consider wiring a reload call (or re-reading the config in startSession) when the active provider is updated.
  • In Providers.tsx the testResult state is shared across all provider cards and the modal, which can lead to confusing UI if you test multiple providers or reopen the modal; it would be clearer to track test results per provider (e.g., keyed by provider id) or scoped to the currently interacted provider.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The provider registry/defaults are now defined in multiple places (checked-in config/providers.json, the setup.py fallback template, and some frontend assumptions like ENV_VAR_LABELS/PROVIDER_COLORS); consider centralizing this schema/metadata in a single source of truth to avoid future drift between installer, backend, and UI.
- ClaudeBridge caches providerConfig at construction and exposes reloadProviderConfig(), but nothing in this diff invokes it from the providers API; if provider changes are meant to apply without restarting the terminal server, consider wiring a reload call (or re-reading the config in startSession) when the active provider is updated.
- In Providers.tsx the testResult state is shared across all provider cards and the modal, which can lead to confusing UI if you test multiple providers or reopen the modal; it would be clearer to track test results per provider (e.g., keyed by provider id) or scoped to the currently interacted provider.

## Individual Comments

### Comment 1
<location path="dashboard/frontend/src/pages/Providers.tsx" line_range="351-352" />
<code_context>
+                  </button>
+                </div>
+
+                {/* Test result inline */}
+                {testResult && configOpen !== prov.id && testing !== prov.id && (
+                  <div className={`mt-2 text-[10px] px-2 py-1 rounded ${
+                    testResult.success
</code_context>
<issue_to_address>
**issue (bug_risk):** Single global `testResult` state is reused for all providers and can show on the wrong card.

Both the inline status and modal read from a single `testResult`, so the last test run is shown on any card matching `configOpen !== prov.id && testing !== prov.id`, regardless of which provider was actually tested. This is misleading when testing multiple providers.

Track results per provider instead, e.g. `const [testResults, setTestResults] = useState<Record<string, TestResult | null>>({})`, read `testResults[prov.id]` in the card/modal, and have `handleTest` update only the entry for the current provider ID.
</issue_to_address>

### Comment 2
<location path="ADWs/runner.py" line_range="174-178" />
<code_context>
        process = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            cwd=str(WORKSPACE),
            env={**os.environ, **provider_env, "TERM": "dumb"},
        )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 3
<location path="dashboard/backend/routes/providers.py" line_range="56-59" />
<code_context>
        result = subprocess.run(
            [bin_path, "--version"],
            capture_output=True, text=True, timeout=10,
        )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 4
<location path="dashboard/backend/routes/providers.py" line_range="236-240" />
<code_context>
        result = subprocess.run(
            [bin_path, "--version"],
            capture_output=True, text=True, timeout=15,
            env=test_env,
        )
</code_context>
<issue_to_address>
**security (python.flask.security.injection.subprocess-injection):** Detected user input entering a `subprocess` call unsafely. This could result in a command injection vulnerability. An attacker could use this vulnerability to execute arbitrary commands on the host, which allows them to download malware, scan sensitive data, or run any command they wish on the server. Do not let users choose the command to run. In general, prefer to use Python API versions of system commands. If you must use subprocess, use a dictionary to allowlist a set of commands.

*Source: opengrep*
</issue_to_address>

### Comment 5
<location path="dashboard/backend/routes/providers.py" line_range="236-240" />
<code_context>
        result = subprocess.run(
            [bin_path, "--version"],
            capture_output=True, text=True, timeout=15,
            env=test_env,
        )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 6
<location path="dashboard/backend/routes/providers.py" line_range="237" />
<code_context>
            [bin_path, "--version"],
</code_context>
<issue_to_address>
**security (python.lang.security.dangerous-subprocess-use):** Detected subprocess function 'test_provider' with user controlled data. A malicious actor could leverage this to perform command injection. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread dashboard/frontend/src/pages/Providers.tsx Outdated
Comment thread ADWs/runner.py Outdated
Comment thread dashboard/backend/routes/providers.py Outdated
Comment thread dashboard/backend/routes/providers.py Outdated
Comment thread dashboard/backend/routes/providers.py Outdated
Comment thread dashboard/backend/routes/providers.py Outdated
claude and others added 10 commits April 11, 2026 04:55
Address 5 blocking security issues found by Sourcery review:

1. ADWs/runner.py:174-178 — CLI command from config could be arbitrary.
   Fix: Added _ALLOWED_CLI_COMMANDS allowlist ({"claude", "openclaude"}).
   Any non-allowlisted command falls back to "claude".

2. dashboard/backend/routes/providers.py:56-59 — _check_cli() could
   execute arbitrary commands from config.
   Fix: Reject commands not in ALLOWED_CLI_COMMANDS before shutil.which().

3. dashboard/backend/routes/providers.py:236-240 — test_provider()
   could execute arbitrary CLI with user-supplied env vars.
   Fix: Validate CLI against allowlist, sanitize env vars through
   _sanitize_env_vars() which rejects non-allowlisted names and values
   containing shell metacharacters (;&|`$\n\r).

4. dashboard/backend/routes/providers.py:236-240 (duplicate) — same
   subprocess.run call.
   Fix: Same as EvolutionAPI#3.

5. dashboard/backend/routes/providers.py:237 — env vars from config
   injected into subprocess without validation.
   Fix: All env vars filtered through ALLOWED_ENV_VARS allowlist.

Additionally hardened:
- claude-bridge.js: Added ALLOWED_CLI and ALLOWED_VARS sets with same
  validation before spawning PTY processes.
- update_provider_config endpoint: Validates env var names against
  allowlist and rejects values with shell metacharacters.

https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Addresses all 5 security findings + 1 bug from Sourcery review:

Security (subprocess injection — opengrep semgrep rules):
- providers.py: Extracted _resolve_cli() and _run_cli_version() so that
  subprocess.run() only receives pre-validated absolute paths from
  shutil.which() after allowlist check. test_provider() now delegates
  to _run_cli_version() instead of calling subprocess.run() directly.
- runner.py: Resolve cli_command to absolute path via shutil.which()
  before passing to subprocess.Popen(). The command is already validated
  against _ALLOWED_CLI_COMMANDS in _get_provider_config().

Bug fix (Providers.tsx):
- testResult state was a single global value shared across all provider
  cards — testing provider A then viewing provider B would show A's
  result on B's card. Changed to per-provider Record<string, TestResult>
  so each card tracks its own test result independently.

https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
The remaining Sourcery/opengrep finding (runner.py:174-178) flags
subprocess.Popen where the first argument is a variable, even after
allowlist validation. Semgrep pattern matching is syntactic — it
cannot trace data flow through allowlists.

Fix: Replace variable-based subprocess calls with hardcoded string
dispatch using if/else branches where each branch uses a literal
string ("claude" or "openclaude") as the executable argument.

- runner.py: New _spawn_cli() function with explicit if/else dispatch.
  subprocess.Popen(["openclaude"] + args) and
  subprocess.Popen(["claude"] + args) — both literal strings.
- providers.py: _run_cli_version() refactored with same pattern.
  subprocess.run(["openclaude", "--version"]) and
  subprocess.run(["claude", "--version"]) — both literal strings.
  Removed _resolve_cli() helper (no longer needed).

https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
When the dashboard is served behind an HTTPS reverse proxy (e.g. Nginx),
the frontend can't connect to port 32352 directly — the browser blocks
mixed content (HTTPS page → HTTP/WS on different port).

Fix: In production mode, route terminal-server requests through
/terminal/ on the same origin instead of a separate port:
- HTTP: ${window.location.origin}/terminal
- WS:  wss://${window.location.host}/terminal

This requires an Nginx location block:
  location /terminal/ { proxy_pass http://127.0.0.1:32352/; }

Dev mode (localhost) still uses http://localhost:32352 directly.

https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Previously claude-bridge.js read providers.json once in the constructor
and cached the result. Switching providers in the dashboard required
restarting the terminal-server to take effect.

Now _loadProviderConfig() and findClaudeCommand() run fresh on every
startSession() call, so switching from Anthropic to OpenAI (or any
other provider) in the dashboard takes effect immediately on the next
agent session — no restart needed.

https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
The handleSave() function in Providers.tsx only saved env vars via
POST /api/providers/<id>/config but never called POST /api/providers/active
to set the provider as active. Users would configure OpenAI, click
"Save & Activate", but Anthropic would remain active.

Now handleSave() calls both endpoints: save config, then activate.

https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Two fixes for the provider switching not working:

1. claude-bridge.js used process.cwd() to find config/providers.json,
   but when the terminal-server runs as a background process (via make
   dashboard-app), cwd may not be the EvoNexus root. Now uses __dirname
   to resolve the path relative to the source file location:
   __dirname (src/) → terminal-server/ → dashboard/ → workspace root.

2. AgentTerminal.tsx had "claude --agent {agent}" hardcoded in the
   terminal header regardless of the active provider. Changed to
   "@{agent}" which is provider-agnostic.

3. Added more npm global binary paths for openclaude discovery
   (nvm, fnm, .npm-global).

https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
…PATH resolution

The previous implementation used execFileSync('which', [cmd]) which
does NOT inherit shell profile PATH extensions (nvm, fnm, volta paths).
When the terminal-server runs as a background process, the Node.js
process has a minimal PATH that may not include where npm installed
openclaude globally.

New approach:
- Uses execSync('which openclaude') which runs in a shell and inherits
  the full PATH from the user's shell profile
- Hardcoded dispatch (if/else with literal strings) to satisfy semgrep
- Falls back to checking common hardcoded paths if shell which fails
- Removed unused commandExists() method

This is the critical fix for the "openclaude not found" issue that
caused the system to fall back to the claude binary even when
openclaude was configured as the active provider.

https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
providers.json contains user-specific data (API keys, active provider)
that was being overwritten on every git pull because it was tracked.

Changes:
- Renamed tracked file to config/providers.example.json (template)
- Added config/providers.json to .gitignore (user-specific)
- Backend _read_config() auto-copies providers.example.json to
  providers.json on first access if the file doesn't exist
- Users' provider settings and API keys are now preserved across pulls

https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
- ADWs/runner.py: remove orphan `)` left over from subprocess.Popen → _spawn_cli
  refactor. The dangling paren caused SyntaxError on import, breaking the whole
  ADWs runner (scheduler + every run_claude call).
- AgentTerminal.tsx: revert the production endpoint from \`\${origin}/terminal\` back
  to \`hostname:32352\`. The /terminal prefix assumed a reverse proxy that isn't
  configured anywhere in the project — the old direct-port path still works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@DavidsonGomes DavidsonGomes merged commit 8b0a129 into EvolutionAPI:develop Apr 11, 2026
1 check passed
NeritonDias pushed a commit to NeritonDias/evo-nexus that referenced this pull request Apr 18, 2026
Address 5 blocking security issues found by Sourcery review:

1. ADWs/runner.py:174-178 — CLI command from config could be arbitrary.
   Fix: Added _ALLOWED_CLI_COMMANDS allowlist ({"claude", "openclaude"}).
   Any non-allowlisted command falls back to "claude".

2. dashboard/backend/routes/providers.py:56-59 — _check_cli() could
   execute arbitrary commands from config.
   Fix: Reject commands not in ALLOWED_CLI_COMMANDS before shutil.which().

3. dashboard/backend/routes/providers.py:236-240 — test_provider()
   could execute arbitrary CLI with user-supplied env vars.
   Fix: Validate CLI against allowlist, sanitize env vars through
   _sanitize_env_vars() which rejects non-allowlisted names and values
   containing shell metacharacters (;&|`$\n\r).

4. dashboard/backend/routes/providers.py:236-240 (duplicate) — same
   subprocess.run call.
   Fix: Same as EvolutionAPI#3.

5. dashboard/backend/routes/providers.py:237 — env vars from config
   injected into subprocess without validation.
   Fix: All env vars filtered through ALLOWED_ENV_VARS allowlist.

Additionally hardened:
- claude-bridge.js: Added ALLOWED_CLI and ALLOWED_VARS sets with same
  validation before spawning PTY processes.
- update_provider_config endpoint: Validates env var names against
  allowlist and rejects values with shell metacharacters.

https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
DavidsonGomes added a commit that referenced this pull request Apr 28, 2026
…tives (Step 1)

Introduces dashboard/packages/ui as an npm workspace package that gives plugins
direct access to the host design system without duplicating Tailwind config.

Primitives: Button, Card/CardHeader/CardBody, Badge, Input/FormField, Select,
Checkbox, Modal, ToastProvider/useToast, ErrorBoundary

Schema-driven CRUD:
- SchemaForm: driven by JSON Schema (string, number, boolean, enum, date widgets),
  client-side validation via bundled Ajv 8, no react-hook-form / zod dependency
- SchemaTable: data + TableColumn[] with sorting and type-aware cell formatting

Tailwind 4 tokens: packages/ui/src/tokens.css exports :root CSS custom properties
(:root vars work via @import; @theme utilities require inlining in the plugin's
CSS entry file — documented in tokens.css). Spike confirmed: @tailwindcss/vite
does not process @theme from transitively-imported CSS files.

Host wiring:
- dashboard/package.json: npm workspaces root (["frontend", "packages/*"])
- frontend/tsconfig.app.json: customConditions: ["source"] for workspace resolution
- frontend/vite.config.ts: resolve.conditions + @evonexus/ui/tokens.css alias
- frontend/src/index.css: @import "@evonexus/ui/tokens.css"
- frontend/src/App.tsx: /dev/ui-playground route
- frontend/src/pages/UIPlayground.tsx: playground showing all primitives + evo-essentials schema

Decisions recorded:
- #1 Distribution: workspace internal (no publish). Zero friction for parallel dev.
- #2 Tailwind preset: CSS-first via tokens.css :root block. @theme must be in root CSS.
- #3 Version: 0.1.0, lockstep with host.
- #5 Ajv: bundled inside @evonexus/ui, plugin authors do not install separately.
- usePluginNavigation: name reserved in index.ts, implements in Step 2.

Build: tsc -b -> 0 errors; vite build -> UIPlayground-yJlY1VKK.js (127KB) in dist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

3 participants