feat: multi-provider support + setup hardening + UI redesign#4
feat: multi-provider support + setup hardening + UI redesign#4DavidsonGomes merged 43 commits intoEvolutionAPI:developfrom
Conversation
## OpenAI Authentication (2 methods) ### Browser OAuth (works with ANY OpenAI org) 1. Dashboard generates PKCE authorize URL (redirect_uri=localhost:1455) 2. User opens link, authorizes on OpenAI 3. Browser redirects to localhost:1455 (shows error page — expected) 4. User copies the callback URL from browser address bar 5. User pastes URL into dashboard text field 6. Backend extracts code, exchanges for tokens, saves ~/.codex/auth.json ### Device Auth (for orgs that allow it) 1. Dashboard requests device code from OpenAI 2. Shows user_code + verification URL 3. User opens URL, enters code 4. Dashboard polls until authorized 5. Exchanges for tokens, saves auth.json ## Nginx Auto-Config - New configure_access() in setup.py - Asks: local (localhost:8080) or domain with SSL - Auto-generates Nginx config with: - HTTP → HTTPS redirect - / → localhost:8080 (dashboard) - /terminal/ → localhost:32352 (terminal-server WebSocket) - Certbot auto or manual SSL cert paths ## Setup Wizard Changes - OpenAI provider: choice between API Key (GPT-4.x) or Codex OAuth (GPT-5.x) - Codex OAuth: "authenticate via Dashboard → Providers → Login com OpenAI" - Gemini, Bedrock, Vertex marked as "coming soon" - Removed codex_auth as separate provider (unified into openai) ## Backend (providers.py) - POST /api/providers/openai/auth-start — PKCE + authorize URL - POST /api/providers/openai/auth-complete — token exchange from callback URL - POST /api/providers/openai/device-start — device code request - POST /api/providers/openai/device-poll — poll for authorization - GET /api/providers/openai/status — check auth.json validity - POST /api/providers/openai/logout — remove auth.json ## Frontend (Providers.tsx) - Auth modal with 2 tabs: Browser OAuth / Device Auth - Browser OAuth: link + paste callback URL field - Device Auth: code display + auto-polling - Auth status badge + logout button on OpenAI card ## claude-bridge.js - Auto-detects ~/.codex/auth.json - Removes OPENAI_API_KEY from env vars when auth.json exists - Prevents API key from overriding OAuth token https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
OAuth endpoints don't need Flask-Login auth — PKCE state in session provides security. The @login_required decorator was blocking requests even when the user was logged into the dashboard. Also updated setup.py: - Domain mode: configure Nginx first, then build+start, redirect to dashboard - Simplified provider list (3 active + 3 coming soon) - OpenAI: auth choice (API key or Codex OAuth via dashboard) https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Prerequisites (Node.js, npm, uv, Claude Code) are now checked with an offer to install automatically. The setup only fails if the user declines installation AND the tool is required. - Node.js: installs via nodesource setup script - uv: installs via astral.sh install script - Claude Code: installs via npm - Each tool: asks "Install X? (Y/n)" before proceeding https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
AgentTerminal.tsx: In production, connect to terminal-server via /terminal/ path on same origin (proxied by Nginx) instead of direct port 32352 which fails behind HTTPS proxy. setup.py: Install terminal-server npm deps before starting. Verify both services are running after startup. Use proper cwd for terminal-server so config paths resolve correctly. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
The old format used {"openai-codex": {"type":"oauth","access":...}}
which OpenClaude doesn't recognize. The correct format is:
{
"auth_mode": "Chatgpt",
"tokens": {
"access_token": "...",
"refresh_token": "...",
"id_token": "...",
"account_id": "..."
},
"last_refresh": "2026-04-11T..."
}
Also extracts chatgpt_account_id from the JWT access token payload
and updates openai_status to handle both old and new format.
https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
ROOT CAUSE: The spawn() call spreads process.env into the child environment. If OPENAI_API_KEY exists in the host shell (even stale), it overrides the Codex OAuth auth.json, causing "Incorrect API key" errors. The previous fix only deleted from the providerEnv local object, which had no effect on inherited process.env vars. FIX: Clone process.env into baseEnv, then DELETE OPENAI_API_KEY from baseEnv when Codex OAuth auth.json exists. This ensures the spawned openclaude process has NO OPENAI_API_KEY in its environment, forcing it to use ~/.codex/auth.json instead. Also returns active provider name from _loadProviderConfig() so startSession() can make provider-specific decisions. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Terminal-server npm install and service startup now run for ALL modes (local and remote), not just remote. The user should never need to manually install deps or start services after make setup. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
/tmp files created by sudo become root-owned, causing permission errors when services restart as non-root user. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
When setup runs with sudo, services were started as root. This causes: 1. --dangerously-skip-permissions blocked (Claude/OpenClaude refuse root) 2. chdir failures (wrong home directory) 3. auth.json not found (looks in /root/.codex/ instead of user home) Fix: detect SUDO_USER env var and use su - to start services as the original user who ran sudo. Also chown the workspace to that user. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
The previous approach of {...process.env} then deleting OPENAI_API_KEY
still leaked the stale key somehow. Nuclear fix: build a CLEAN env
from scratch with only whitelisted system vars (HOME, PATH, USER, etc.)
plus the provider-specific vars. Nothing else.
This guarantees that NO stale OPENAI_API_KEY, ANTHROPIC_API_KEY, or
any other env var from the host shell leaks into the spawned CLI.
https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
…auth failure THE BUG: OpenClaude requires OPENAI_MODEL env var to properly resolve Codex OAuth authentication. Without it, OpenClaude falls back to API key auth and fails with "OPENAI_API_KEY is required". The providers.json had OPENAI_MODEL="" (empty) which was filtered out by the v!='' check, leaving the spawned process with NO model set. FIX: claude-bridge.js now defaults OPENAI_MODEL to gpt-5.4-mini when the active provider is openai/codex_auth and no model is configured. Also updated providers.example.json to have gpt-5.4-mini as default. Confirmed working: the exact same command with OPENAI_MODEL=gpt-5.4-mini succeeds from CLI — "Olá!" response from Oracle agent. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
… systemd The .venv created by 'uv sync' (run as root via sudo) was owned by root. When services started as ubuntu (via su -), uv couldn't access .venv. Fix: Move chown -R to run IMMEDIATELY after all installs complete, BEFORE starting services. Also disable any existing evonexus.service to prevent conflicts with old processes. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
When using OpenAI, OpenRouter, Gemini, or any non-Anthropic provider, GPT models don't follow agent system prompts as strongly as Claude. The model knows about the agent but identifies as "Claude" instead. Fix: append a persona enforcement instruction via --append-system-prompt when the active provider is NOT Anthropic and an agent is loaded. This forces the model to fully embody the agent persona. Only applied for non-Anthropic providers — Claude handles agent personas natively without reinforcement. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Converted all 38 agent avatar images from 2048x2048 PNG to 512x512 WebP: - Total size: 271MB → 1.7MB - Per image: ~7MB → ~40KB average - Quality: 85% WebP (visually identical at 512px) Updated agent-meta.ts references from .png to .webp. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
su - user -c '... &' doesn't persist background processes — they die when su exits. Now creates start-services.sh that uses nohup + direct .venv/bin/python (no uv wrapper) for reliable background execution. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
When uv sync runs as root, .venv/bin/python symlinks to /root/.local/share/uv/python/ which is inaccessible to non-root users. Now runs uv sync via su - SUDO_USER so the venv uses the user's Python. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Claude Code and OpenClaude block this flag for root users with: "cannot be used with root/sudo privileges for security reasons" Fix: detect root via process.getuid() === 0 and skip the flag. The trust prompt is handled by the existing PTY auto-accept code in claude-bridge.js (detects "Do you trust the files" and sends Enter). This enables EvoNexus to work on VPS instances where root is the only available user. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
The agent name was sometimes null because options.agent wasn't set when reconnecting to existing sessions. Now uses session.agentName as fallback, which is set when the session is created via POST /api/sessions/for-agent. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
THE BUG: In server.js startClaude(), the call to startSession() had:
{ agent: agentForSession, ..., ...options }
Since ...options was AFTER agent:, and options.agent could be undefined
(when reconnecting to persisted sessions), it OVERWROTE the agent
with undefined. The --agent flag was never passed to openclaude.
FIX: Move ...options BEFORE agent: so agentForSession always wins:
{ ...options, agent: agentForSession, ... }
Also added explicit logging of agent name and spawn args for debugging.
https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Old sessions in ~/.claude-code-web/sessions.json were being restored with stale processes that didn't have --agent or --append-system-prompt. Now start-services.sh deletes sessions.json before starting the terminal-server. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
--append-system-prompt is too weak for GPT models — they ignore the appended persona instruction and respond as "OpenClaude" instead of the agent persona. --system-prompt REPLACES the default system prompt entirely, making the agent persona the primary instruction. Now reads the agent .md file body and passes it as the system prompt with a persona enforcement suffix. Confirmed working: GPT-5.4 responds "Sou Oracle — o ponto de entrada do EvoNexus" with --system-prompt. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
- Add build-essential as first prerequisite check (node-pty requires g++) - Auto-install via apt or yum if missing - Remove 2>/dev/null from terminal-server npm install to surface errors - Nginx: self-signed SSL as default (Cloudflare Full compatible) - Nginx: IPv6 listeners on ports 80 and 443 - Nginx: auto-open firewall ports (ufw + iptables) - Nginx: remove default site, auto-enable evonexus site https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
- Add apt-get update/upgrade at start of check_prerequisites() (DEBIAN_FRONTEND=noninteractive to avoid interactive prompts) - Upgrade Node.js from 22.x (maintenance) to 24.x (active LTS) - Change SSL default from self-signed to certbot (Let's Encrypt) - Reuse existing certbot cert if found (idempotent re-runs) - Auto-fallback to self-signed if certbot fails (DNS not ready) - Fix SSL key permissions (chmod 600) after generation - Remove 2>/dev/null from certbot to surface errors https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
When running `sudo make setup`, uv was installed in /root/.local/bin/ but `uv sync` runs as SUDO_USER (ubuntu) who didn't have uv. Result: .venv never created, dashboard backend fails to start (502). Fix: install uv via `su - $SUDO_USER` so it lands in the user's ~/.local/bin/. Also add user's uv path to root's PATH for the rest of setup, and remove 2>/dev/null from uv sync to surface errors. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
…o WebP
Setup fixes:
- Fix getent IndexError with try/except fallback to /home/{user}
- Remove 2>/dev/null from frontend npm build (surface errors)
Dashboard UX:
- Remove dead /chat Quick Actions from Overview
- Replace with Agents and Providers quick actions
- Redirect to /providers after first-time setup completion
Image optimization (PNG → WebP, ~70% reduction):
- docs/imgs/: 23 images (2.8MB → 916KB)
- public/: 5 images (2.0MB → 540KB)
- site/public/assets/: 6 images (2.1MB → 583KB)
- dashboard hero: 44KB → 16KB
- Updated all references in docs, README, site
https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
uv install was failing verification because: 1. uv installed at /home/ubuntu/.local/bin/ (via su - ubuntu) 2. _check_tool re-verified as root without that path in PATH 3. PATH update happened AFTER _check_tool returned — too late Fix: resolve user home and add ~/.local/bin to PATH BEFORE calling _check_tool, so the post-install verification finds the binary. Also: suppress verbose apt output during system update and build-essential install — show only clean status messages. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Onboarding fix: - workspace-status endpoint now checks if owner field is filled, not just if workspace.yaml exists. Remote mode creates the file with empty owner/company, so Setup.tsx Step 1 was being skipped. - Setup.tsx always sends workspace data when Step 1 was shown, so owner/company get written to workspace.yaml on first login. Clean setup output: - All package installs (npm, nginx, certbot, build-essential) redirect stdout/stderr to /dev/null - Use \r carriage return to overwrite "Installing..." with checkmark - No more raw apt/npm output cluttering the terminal - Only clean status lines: Installing... → ✓ installed https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Old avatar PNGs (271MB) remain in git history even after WebP conversion. Shallow clone skips history and downloads only ~5-10MB. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Complete visual overhaul of onboarding and login screens: Setup.tsx: - Animated gradient orbs background (green/blue, floating animation) - Subtle grid pattern overlay for depth - Glassmorphism card with backdrop-blur, border glow, top highlight - Step indicator with animated ping on active step - Lucide icons inside all input fields (User, Building2, Globe, etc.) - Gradient CTA button with hover glow effect - Slide transitions between steps (left/right) - Loading spinner with branded colors - Features bar footer (38 AI Agents, 137 Skills, Multi-Provider) - Sparkles icon in header and CTA Login.tsx: - Same animated background for visual consistency - Glassmorphism card matching Setup design - Icon-enhanced inputs (User, KeyRound) - Gradient button with loading spinner - fadeInUp entrance animation Design language: Dark glassmorphism with Evolution palette (#00FFA7 accent, #060a13 base), Inter font, uppercase tracking labels, white/[0.03-0.08] transparency layers. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Complete rewrite following frontend-design skill guidelines: "Avoid generic AI patterns — no sparkles icons, no cliche purple gradients, no predictable symmetric layouts." Changes from previous version: - Removed Sparkles icon (AI cliche) - Removed gradient orbs/blur (generic glassmorphism) - Removed grid overlay (overused pattern) - Removed animated ping on step indicator - Removed lucide icons inside inputs (cluttered) - Removed gradient CTA buttons (over-styled) New design approach: - Canvas-based animated network mesh (particles + edges, neural-network aesthetic that is unique to EvoNexus) - Solid card with intentional borders, no backdrop-blur - Custom SVG logomark (layered prism, not a generic icon) - Tab-style step nav instead of numbered circles - Flat solid CTA button (#00FFA7) with proper hover states - Proper autocomplete attributes on all inputs - Minimal footer with stats as data, not decoration - Labels use 11px uppercase tracking for hierarchy - Dark palette: #080c14 base, #0b1018 card, #152030 borders Follows Vercel Web Interface Guidelines: - focus:outline with ring, never outline-none alone - autocomplete on all inputs - active voice, specific button labels - proper label hierarchy https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Backend: - POST /providers/active now accepts "none" to disable all providers - Validation allows "none" as a valid provider_id Claude-bridge: - Block session start when active provider is "none" - Show clear message: "No AI provider is active" with instructions - Prevent any CLI from spawning without an active provider Frontend (Providers.tsx redesign): - Toggle switch (on/off) replaces Activate button - Turning off sets active_provider to "none" - Turning on sets active_provider to that provider - List layout instead of card grid (cleaner, scannable) - Same visual language as Setup/Login (dark palette, borders, uppercase labels, Inter font) - Removed Star icon and badge decorations - Compact status bar with install status + counts - Consistent modal styling with Setup page inputs https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Reviewer's GuideImplements full multi-provider AI support (Anthropic/OpenRouter/OpenAI with Codex OAuth), hardens the VPS setup pipeline (prereq auto-install, SSL/nginx/firewall, permissions, services), redesigns Setup/Login/Providers UI, restores onboarding flow, and aggressively optimizes images/assets to WebP while tightening provider env handling and agent persona enforcement for non-Anthropic models. Sequence diagram for OpenAI Codex OAuth via dashboardsequenceDiagram
actor User
participant Browser
participant Frontend as Providers_page
participant Backend as Providers_API
participant OpenAIAuth as OpenAI_auth_server
participant OpenAIToken as OpenAI_token_endpoint
participant FS as File_system
User->>Browser: Open /providers and click Login on OpenAI
Browser->>Frontend: Click event
Frontend->>Backend: POST /api/providers/openai/auth-start
Backend->>Backend: Generate PKCE
Backend->>Backend: Store code_verifier in session
Backend-->>Frontend: authorize_url
Frontend-->>User: Show "Open OpenAI Login" link
User->>OpenAIAuth: Open authorize_url and login
OpenAIAuth-->>User: Redirect to callback URL (with code)
User->>Frontend: Paste callback URL into modal
Frontend->>Backend: POST /api/providers/openai/auth-complete
Backend->>Backend: Extract code from URL
Backend->>Backend: Load code_verifier from session
Backend->>OpenAIToken: POST /oauth/token (code, code_verifier)
OpenAIToken-->>Backend: access_token, refresh_token, id_token
Backend->>FS: Write ~/.codex/auth.json (auth_mode Chatgpt)
Backend->>FS: Update config/providers.json (active_provider=openai)
Backend-->>Frontend: {status: ok, message}
Frontend->>Backend: GET /api/providers/openai/status
Backend-->>Frontend: {authenticated: true, method: codex_oauth}
Frontend-->>User: Show OAuth badge and OpenAI as active provider
Sequence diagram for starting an agent session with provider validation and persona enforcementsequenceDiagram
actor User
participant Browser
participant Frontend as Agent_terminal_UI
participant TermServer as Terminal_server
participant Bridge as Claude_bridge
participant ProvCfg as providers.json
participant FS as File_system
participant CLI as Claude_or_OpenClaude_CLI
User->>Browser: Open agent terminal (select agent)
Browser->>Frontend: Load AgentTerminal
Frontend->>TermServer: WS startClaude(options.agent)
TermServer->>Bridge: startSession(sessionId, options)
Bridge->>ProvCfg: Read providers.json
ProvCfg-->>Bridge: {active_provider, providers}
Bridge->>Bridge: Determine active provider
alt no active provider ("none" or missing)
Bridge-->>TermServer: Error "No AI provider is active"
TermServer-->>Frontend: WS message (error + guidance)
Frontend-->>User: Show error, link to Providers
else provider active
Bridge->>FS: Read agent markdown (.claude/agents/{agent}.md)
FS-->>Bridge: Agent definition (or fallback)
Bridge->>Bridge: Build cleanEnv (whitelist system vars)
Bridge->>Bridge: If provider is openai and no OPENAI_MODEL, set gpt-5.4
Bridge->>CLI: spawn(cli_command, [--agent, --system-prompt], env=cleanEnv+provider_env)
CLI-->>Bridge: Stream terminal output
Bridge-->>TermServer: onOutput(data)
TermServer-->>Frontend: WS output
Frontend-->>User: Render terminal and agent persona responses
end
Class diagram for providers configuration and OpenAI Codex auth fileclassDiagram
class ProvidersConfig {
+string active_provider
+Map~string,ProviderEntry~ providers
}
class ProviderEntry {
+string id
+string name
+string description
+string cli_command
+Map~string,string~ env_vars
+bool has_config
+bool installed
+bool requires_logout
+string default_model
+string default_base_url
+string default_region
+bool coming_soon
}
class OpenAIAuthFile {
+string auth_mode
+Tokens tokens
+string last_refresh
}
class Tokens {
+string access_token
+string refresh_token
+string id_token
+string account_id
}
ProvidersConfig "1" o-- "*" ProviderEntry : providers
OpenAIAuthFile "1" o-- "1" Tokens : tokens
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 4 issues, and left some high level feedback:
- In setup.py, several os.system calls interpolate user-provided values like the domain directly into shell commands (certbot, openssl, nginx, su); consider validating/sanitizing these inputs or switching to subprocess with argument lists to avoid potential command injection issues.
- The default OpenAI model differs between setup (e.g. 'gpt-4.1' in choose_provider) and the terminal bridge fallback ('gpt-5.4'); it would be good to centralize this default so the UX and behavior are consistent across configuration and runtime.
- The /api/providers/openai/status endpoint is unauthenticated and exposes whether Codex auth is configured; if this information should be restricted, consider adding login_required or otherwise limiting who can call it.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In setup.py, several os.system calls interpolate user-provided values like the domain directly into shell commands (certbot, openssl, nginx, su); consider validating/sanitizing these inputs or switching to subprocess with argument lists to avoid potential command injection issues.
- The default OpenAI model differs between setup (e.g. 'gpt-4.1' in choose_provider) and the terminal bridge fallback ('gpt-5.4'); it would be good to centralize this default so the UX and behavior are consistent across configuration and runtime.
- The /api/providers/openai/status endpoint is unauthenticated and exposes whether Codex auth is configured; if this information should be restricted, consider adding login_required or otherwise limiting who can call it.
## Individual Comments
### Comment 1
<location path="dashboard/backend/routes/providers.py" line_range="362-363" />
<code_context>
+# ── OpenAI Auth Flow ──────────────────────────────
+
+
+@bp.route("/api/providers/openai/auth-start", methods=["POST"])
+def openai_auth_start():
+ """Generate PKCE + authorize URL for Browser OAuth flow."""
+ code_verifier = secrets.token_urlsafe(64)
</code_context>
<issue_to_address>
**🚨 issue (security):** OpenAI OAuth endpoints should likely be protected with authentication to avoid unauthenticated token writes and provider changes
These new OpenAI OAuth/device auth endpoints (`/api/providers/openai/auth-start`, `/auth-complete`, `/device-start`, `/device-poll`, `/status`) aren’t protected with `@login_required` but can write to `~/.codex/auth.json` and switch `active_provider` to `openai`. This allows unauthenticated users or CSRF to trigger auth flows and change provider state.
Please gate the mutating endpoints behind `@login_required` (or equivalent) so only authenticated dashboard users can initiate/complete these flows and modify provider state. If they must be reusable from the CLI, consider tightening their scope or adding extra validation to prevent unauthenticated state changes.
</issue_to_address>
### Comment 2
<location path="dashboard/backend/routes/config.py" line_range="21-23" />
<code_context>
+ try:
+ content = config_path.read_text(encoding="utf-8")
+ import yaml
+ data = yaml.safe_load(content) or {}
+ ws = data.get("workspace", data)
+ owner = (ws.get("owner") or "").strip()
+ return jsonify({"configured": bool(owner)})
+ except Exception:
</code_context>
<issue_to_address>
**issue (bug_risk):** Workspace status now checks `owner` key, but CLI setup writes `owner_name`, which may incorrectly mark configured workspaces as unconfigured
This logic only treats the workspace as configured when `workspace.owner` is non-empty, but the CLI currently writes `owner_name` (and existing YAML uses `owner_name`). As a result, workspaces created via `setup.py` may now be reported as `configured: false` even though they’re valid.
To avoid this regression, either read both keys (e.g. `owner = (ws.get("owner") or ws.get("owner_name") or "").strip()`) or update the CLI to write the same key this endpoint expects so they stay consistent.
</issue_to_address>
### Comment 3
<location path="dashboard/terminal-server/src/claude-bridge.js" line_range="64-65" />
<code_context>
}
- return { cli_command: cliCommand, env_vars: envVars };
+ return { cli_command: cliCommand, env_vars: envVars, active };
} catch (err) {
console.warn(`[provider] Could not read providers.json: ${err.message}`);
return { cli_command: 'claude', env_vars: {} };
</code_context>
<issue_to_address>
**issue (bug_risk):** Fallback return on providers.json read error does not set `active`, which now causes sessions to be blocked
In the success path `_loadProviderConfig` now returns `{ cli_command, env_vars, active }`, and `startSession` checks `providerConfig.active` to decide whether to block:
```js
if (!providerConfig.active || providerConfig.active === 'none') {
// abort
}
```
But in the `catch` branch you still return only `{ cli_command: 'claude', env_vars: {} }`, so `active` is `undefined` and `startSession` will always treat this as "no provider active" and block, instead of falling back to Claude.
To keep the previous behavior, the error path should also set `active` to the default provider, e.g.:
```js
return { cli_command: 'claude', env_vars: {}, active: 'anthropic' };
```
</issue_to_address>
### Comment 4
<location path="dashboard/frontend/src/pages/Providers.tsx" line_range="90-93" />
<code_context>
+function NetworkCanvas() {
+ const ref = useRef<HTMLCanvasElement>(null)
+
+ useEffect(() => {
+ const canvas = ref.current
+ if (!canvas) return
</code_context>
<issue_to_address>
**suggestion (performance):** Device auth polling interval is hard-coded instead of using server-provided `interval`
The `/device-start` response includes an `interval` value:
```json
{"user_code": ..., "verification_url": ..., "interval": X, ...}
```
Consider storing this `interval` when you set `deviceCode` and using it in `setInterval(pollDeviceAuth, interval)` instead of the fixed `5000` to respect the server’s recommended polling cadence and avoid unnecessary requests.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Merge origin/develop into codex-auth. Single conflict in site/src/pages/Home.tsx: upstream added EVO_NEXUS.png import. Resolved by converting to WebP and keeping all our WebP imports. https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
| """Check if a tool is installed. If not, offer to install it.""" | ||
| try: | ||
| result = subprocess.run(["claude", "--version"], capture_output=True, text=True, timeout=5) | ||
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) |
There was a problem hiding this comment.
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
| ret = os.system(f"{install_cmd} > /dev/null 2>&1") | ||
| # Re-check after install | ||
| try: | ||
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) |
There was a problem hiding this comment.
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
| npm_ok = False | ||
| for cmd in ["npm", "npm.cmd"]: | ||
| try: | ||
| result = subprocess.run([cmd, "--version"], capture_output=True, text=True, timeout=5) |
There was a problem hiding this comment.
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
| os.system(f"cd {WORKSPACE} && uv sync -q 2>/dev/null") | ||
| _sudo_user = os.environ.get("SUDO_USER", "") | ||
| if _sudo_user and os.getuid() == 0: | ||
| os.system(f"su - {_sudo_user} -c 'cd {WORKSPACE} && uv sync -q'") |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-system-call-tainted-env-args): Found user-controlled data used in a system call. This could allow a malicious actor to execute commands. Use the 'subprocess' module instead, which is easier to use without accidentally exposing a command injection vulnerability.
Source: opengrep
| sudo_user = os.environ.get("SUDO_USER", "") | ||
| if sudo_user and os.getuid() == 0: | ||
| print(f" {DIM}Fixing file ownership for {sudo_user}...{RESET}") | ||
| os.system(f"chown -R {sudo_user}:{sudo_user} {WORKSPACE}") |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-system-call-tainted-env-args): Found user-controlled data used in a system call. This could allow a malicious actor to execute commands. Use the 'subprocess' module instead, which is easier to use without accidentally exposing a command injection vulnerability.
Source: opengrep
| logs_dir = WORKSPACE / "logs" | ||
| logs_dir.mkdir(exist_ok=True) | ||
| if sudo_user and os.getuid() == 0: | ||
| os.system(f"chown -R {sudo_user}:{sudo_user} {logs_dir}") |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-system-call-tainted-env-args): Found user-controlled data used in a system call. This could allow a malicious actor to execute commands. Use the 'subprocess' module instead, which is easier to use without accidentally exposing a command injection vulnerability.
Source: opengrep
| os.chmod(startup_script, 0o755) | ||
|
|
||
| if sudo_user: | ||
| os.system(f"su - {sudo_user} -c '{startup_script}'") |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-system-call-tainted-env-args): Found user-controlled data used in a system call. This could allow a malicious actor to execute commands. Use the 'subprocess' module instead, which is easier to use without accidentally exposing a command injection vulnerability.
Source: opengrep
| """Check if a tool is installed. If not, offer to install it.""" | ||
| try: | ||
| result = subprocess.run(["claude", "--version"], capture_output=True, text=True, timeout=5) | ||
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) |
There was a problem hiding this comment.
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
| ret = os.system(f"{install_cmd} > /dev/null 2>&1") | ||
| # Re-check after install | ||
| try: | ||
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) |
There was a problem hiding this comment.
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
| npm_ok = False | ||
| for cmd in ["npm", "npm.cmd"]: | ||
| try: | ||
| result = subprocess.run([cmd, "--version"], capture_output=True, text=True, timeout=5) |
There was a problem hiding this comment.
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
| os.system(f"cd {WORKSPACE} && uv sync -q 2>/dev/null") | ||
| _sudo_user = os.environ.get("SUDO_USER", "") | ||
| if _sudo_user and os.getuid() == 0: | ||
| os.system(f"su - {_sudo_user} -c 'cd {WORKSPACE} && uv sync -q'") |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-system-call-tainted-env-args): Found user-controlled data used in a system call. This could allow a malicious actor to execute commands. Use the 'subprocess' module instead, which is easier to use without accidentally exposing a command injection vulnerability.
Source: opengrep
| sudo_user = os.environ.get("SUDO_USER", "") | ||
| if sudo_user and os.getuid() == 0: | ||
| print(f" {DIM}Fixing file ownership for {sudo_user}...{RESET}") | ||
| os.system(f"chown -R {sudo_user}:{sudo_user} {WORKSPACE}") |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-system-call-tainted-env-args): Found user-controlled data used in a system call. This could allow a malicious actor to execute commands. Use the 'subprocess' module instead, which is easier to use without accidentally exposing a command injection vulnerability.
Source: opengrep
| logs_dir = WORKSPACE / "logs" | ||
| logs_dir.mkdir(exist_ok=True) | ||
| if sudo_user and os.getuid() == 0: | ||
| os.system(f"chown -R {sudo_user}:{sudo_user} {logs_dir}") |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-system-call-tainted-env-args): Found user-controlled data used in a system call. This could allow a malicious actor to execute commands. Use the 'subprocess' module instead, which is easier to use without accidentally exposing a command injection vulnerability.
Source: opengrep
| os.chmod(startup_script, 0o755) | ||
|
|
||
| if sudo_user: | ||
| os.system(f"su - {sudo_user} -c '{startup_script}'") |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-system-call-tainted-env-args): Found user-controlled data used in a system call. This could allow a malicious actor to execute commands. Use the 'subprocess' module instead, which is easier to use without accidentally exposing a command injection vulnerability.
Source: opengrep
1. Security: add @login_required to all OpenAI OAuth endpoints
(auth-start, auth-complete, device-start, device-poll, status)
Previously unauthenticated users could trigger auth flows and
write to ~/.codex/auth.json.
2. Bug risk: workspace-status checks both 'owner' and 'owner_name'
CLI setup writes owner_name, dashboard writes owner. Now both
keys are checked so workspaces from either source are detected.
3. Bug risk: claude-bridge catch fallback returns active: 'anthropic'
Previously the error path returned {cli_command, env_vars} without
active, causing startSession to treat it as "none" and block all
sessions when providers.json was unreadable.
4. Performance: device auth polling uses server-provided interval
Instead of hardcoded 5000ms, now uses deviceCode.interval from
the /device-start response (default 5s if not provided).
https://claude.ai/code/session_01EsVmzavc8eQK3TwPKMyzFq
Summary / Resumo
Adds complete multi-provider AI support to EvoNexus, allowing users to switch between Anthropic (native Claude), OpenAI (GPT-5.x via Codex OAuth or API key), and OpenRouter (200+ models) — all configurable from a redesigned dashboard. Includes a ground-up rewrite of the setup wizard for clean VPS installs, a professional UI redesign of onboarding/login/providers screens, and massive image optimization (271MB to 1.7MB).
Adiciona suporte completo multi-provider ao EvoNexus, permitindo alternar entre Anthropic (Claude nativo), OpenAI (GPT-5.x via Codex OAuth ou API key) e OpenRouter (200+ modelos) — tudo configuravel pelo dashboard redesenhado. Inclui reescrita completa do setup wizard para instalacao limpa em VPS, redesign profissional da UI de onboarding/login/providers, e otimizacao massiva de imagens (271MB para 1.7MB).
1. Multi-Provider Architecture / Arquitetura Multi-Provider
Provider Toggle System
"none"disables all providersPOST /providers/activenow accepts"none"as valid provider_idToggle on/off por provider no dashboard. Quando nenhum esta ativo, sessoes sao bloqueadas com mensagem clara no terminal.
OpenAI Codex OAuth
~/.codex/auth.json) withauth_mode: "Chatgpt"OPENAI_API_KEYwhen OAuth is activeauth-start,auth-complete,device-start,device-poll,status,logoutFluxo OAuth completo via dashboard — browser OAuth + device auth. Tokens salvos no formato correto do Codex.
Agent Persona Enforcement
--system-promptreplaces the entire default prompt (instead of--append-system-promptwhich was too weak).claude/agents/{agent}.mdand injects it with strong enforcement instructionsGPT/Gemini agora incorporam a persona do agente corretamente via --system-prompt que substitui o prompt padrao.
Clean Environment Whitelist
OPENAI_API_KEYfromprocess.envleaking into spawned sessionsOPENAI_MODELtogpt-5.4when missing--dangerously-skip-permissionsfor root users (Claude/OpenClaude reject it)Ambiente limpo com whitelist de vars — previne vazamento de API keys obsoletas.
2. Setup Hardening / Robustez do Setup (
setup.py)Auto-Install Prerequisites
All prerequisites are auto-installed without user intervention:
apt-get update && apt-get upgradewithDEBIAN_FRONTEND=noninteractive(no prompts, clean output)SUDO_USER, not root, fixing .venv symlink permissionsInstalling... -> checkmark)Todos os pre-requisitos instalados automaticamente. Output limpo sem logs verbosos.
SSL & Nginx Auto-Configuration
chmod 600) after generationlisten [::]:80/listen [::]:443 ssl)Certbot como padrao, self-signed como fallback. IPv6, firewall e permissoes SSL configurados automaticamente.
Permission Handling (sudo)
uv syncruns asSUDO_USER(not root) so .venv symlinks point to user's Pythonchown -Rfixes workspace ownership before starting serviceschmod +x .venv/bin/ensures Python binaries are executable after chownsu - $SUDO_USERto avoid root-owned processesPermissoes tratadas corretamente quando rodando com sudo — uv, .venv e servicos rodam como o usuario original.
Service Startup
start-services.shwith nohup for persistent background processes~/.claude-code-web/sessions.jsonon every start (prevents agent persona issues)3. Dashboard Onboarding / Onboarding do Dashboard
Onboarding Flow Restored
workspace.yamlwith emptyowner/company. The dashboard checked if the file existed (configured: true) and skipped Step 1 (name, company, timezone, language)./api/config/workspace-statusnow reads the YAML and checks ifownerfield is actually filled. Empty owner =configured: false= Step 1 is shown.workspace.yaml.Onboarding restaurado — verifica se owner esta preenchido, nao apenas se o arquivo existe.
Post-Setup Flow
/providersto configure AI providerDead Route Removal
/chatQuick Actions from Overview (route didn't exist in router)Sun,MessageSquareimports from lucide-react4. UI/UX Redesign / Redesign Visual
Setup & Login Pages — Complete Rewrite
Animated Background:
rgba(0, 255, 167, 0.25)) on deep dark (#080c14)Card Design:
#0b1018) with intentional borders (#152030), no backdrop-blurForm UX:
autocompleteattributes on all inputs (username, email, new-password, current-password)#0f1520background,#1e2a3aborders, green focus ring#00FFA7) with hover (#00e69a) and active (#00cc88) statesWhat was removed (AI cliches):
Background animado real via Canvas API (mesh neural), card solido sem glass, sem cliches de IA, tipografia profissional.
Providers Page — Redesign
role="switch"andaria-checked5. Image Optimization / Otimizacao de Imagens
Agent Avatars (PNG to WebP)
agent-meta.tsDocumentation & Screenshots (PNG to WebP)
Total: 167 files changed, 1,533 insertions(+), 732 deletions(-) 40 commits on
codex-authbranchCommits (40)
Features
04ad2f2feat: OpenAI Codex OAuth via Dashboard + Nginx auto-config453f1cdfeat: enforce agent persona for non-Anthropic providersb12633cfeat(setup): system update, Node.js 24.x LTS, certbot as default SSL045e62ffeat: setup hardening, remove /chat, redirect to /providers, images to WebP5f05d99feat(ui): premium dark glassmorphism redesign for Setup + Login240afe1feat(ui): professional redesign — canvas network bg, no AI clichesbd57cb2feat: provider toggle switch + session blocking when none activeCritical Fixes
51ff3f5fix(critical): whitelist env vars instead of spreading process.env2cd3e53fix(critical): add default OPENAI_MODEL when missingad34dfafix(critical): remove OPENAI_API_KEY from process.env before spawn5d8e54ffix(critical): start services as SUDO_USER, not rootf99b2b7fix(critical): chown workspace BEFORE starting services4e3a10dfix(critical): run uv sync as SUDO_USER, not roote6df10dfix(critical): ...options spread was overwriting agent with undefinedcc70472fix(critical): use --system-prompt instead of --append-system-promptPerformance
5ee1622perf: optimize agent avatars — 271MB PNG to 1.7MB WebP (99.4% reduction)Setup & Permissions
b0617b1fix: add build-essential prereq + nginx SSL/IPv6/firewall improvements6f40ce5fix: install uv for SUDO_USER, not root — .venv was not created71d878efix: uv PATH resolution before verification + clean install output4ad21d7fix: restore onboarding when owner is empty + clean setup outputb076527fix: chmod +x .venv/bin/ after chown to fix Permission deniedec7414dfix: skip --dangerously-skip-permissions when running as rootSummary by Sourcery
Add multi-provider AI selection with OpenAI Codex OAuth support, harden the VPS setup wizard, and redesign the dashboard authentication and provider screens while optimizing image assets.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Chores: