Skip to main content
The in-app AI copilot is a chat popup inside the DealDash web app. It acts as the signed-in user: every action it takes goes through the app’s own /api/* routes with that user’s JWT, so reads and writes are scoped, audited, and validated exactly like a request from the SPA. It is a separate path from the external MCP agent bridge at /api/agent/* (which serves out-of-app agents with its own OAuth and Telegram approval tiers). The copilot does not use the MCP transport, agent OAuth, or the agent-bridge approval-tier middleware.

What It Is

  • A self-hosted CopilotKit runtime served at /api/copilot (Express, server/routes/copilot.ts).
  • A per-request BuiltInAgent built for the signed-in user. It resolves the Supabase JWT to the local user, then calls the app’s own REST routes over loopback.
  • 44 server tools across 12 families: deals, tasks, contacts, influencers, activity, suggestions, context, templates, memory, links, screenshots, and social sessions.
  • Server tools are reads plus safe low-risk writes only. Anything sensitive runs as a frontend action gated by human approval (see below).
  • Tool results are size-capped before they re-enter the prompt on later steps (server/lib/copilot/capResult.ts, COPILOT_TOOL_RESULT_MAX_CHARS, default 16 KB) — a large search/list is trimmed to its first records with a _truncated marker so input tokens stay bounded.
Gating:
  • The server endpoint returns a clean 503 when no model provider is configured at all (isCopilotConfigured() checks for any of the Anthropic / OpenAI / Google / OpenRouter keys or the Hermes base URL).
  • The client surface is controlled by VITE_COPILOT_ENABLED: production builds show the copilot unless the flag is explicitly false; local/dev builds remain opt-in with VITE_COPILOT_ENABLED=true.
  • CopilotKit telemetry is disabled by default (COPILOTKIT_TELEMETRY_DISABLED).

Chat UI & Discoverability

The chat is a CopilotPopup (client/src/components/copilot/CopilotProvider.tsx) with a branded theme (copilot-theme.css) and a custom header offering five controls:
  • What can I ask? (grid) — opens the Capabilities sheet (CopilotCapabilitiesSheet.tsx), an organized launcher with one section per tool family (pipeline & deals, views/LinkShot, contacts, influencers, tasks, screenshots, social sessions, links, memory, templates) and curated tap-to-send starter prompts. This makes the full tool surface discoverable instead of relying only on the dynamic suggestion chips.
  • New chat — starts a fresh thread.
  • Conversations (history) — the threads panel (ConversationHistorySheet.tsx): list, resume, rename, archive, delete, and search past conversations.
  • Settings (gear) — the Settings sheet (CopilotSettingsSheet.tsx) surfaces the provider / model / BYOK card right inside the assistant (admins also get a shortcut to the MCP-server registry).
  • Close.
Other UI features: route-aware LLM suggestion chips (CopilotSuggestions.tsx), image attachments (uploaded to DealDash screenshot storage, passed to the vision model as a URL), in-chat ApprovalCards for sensitive actions, and thread persistence (each turn is saved server-side and re-hydrated on reload). All surfaces are responsive — a docked popup on desktop, a full-height sheet above the mobile nav.

Reliability & Resource Safety

The copilot streams model output over Server-Sent Events. A stalled stream that holds the response open past Cloudflare’s ~100 s edge timeout produces a 524, which makes CopilotKit retry-storm — and that storm can exhaust the database connection pool and take the whole site down. Several bounds prevent this; all default to safe values and are env-tunable: Production policy: run a paid model (never free-rotate) on prod, since the free-rotate timeouts don’t cover the paid path — the request deadline above is what protects it. The mechanism is covered by tests/integration/copilot-pool-loadtest.test.ts (50 concurrent hung calls bound out while a healthy route stays responsive; an overloaded pool starves without reclaim and recovers with it).

Multi-Provider Model Backend

The copilot’s LLM is pluggable per user. Each user picks a provider and model in the /settings → “AI Copilot Model” card, or from the gear icon in the chat header (the same card, surfaced in a sheet). Supported providers:
  • Anthropic, OpenAI, Google — resolve as model strings (CopilotKit built-ins).
  • OpenRouter and self-hosted Hermes / OpenAI-compatible endpoints — resolve as AI SDK model instances (BuiltInAgent accepts these directly).
Credentials come from two sources:
  • Server keys, role/premium-gated via COPILOT_SERVER_KEY_ROLES.
  • Encrypted per-user BYOK (bring-your-own-key), stored AES-256-GCM at rest.
Each user’s choice lives in the userId-scoped copilot_provider_config table (RLS-protected), managed via GET/PUT /api/copilot/provider-config. Egress consent: sending CRM data to a non-default third-party provider requires per-user, per-provider consent. This is default-deny — data does not leave to a new provider until the user explicitly consents.

OpenRouter Catalog + Free Rotation

A curated OpenRouter catalog (server/lib/copilot/modelCatalog.ts) is served to the settings UI as a grouped model picker. It includes top picks for tool-heavy CRM work, best-in-class Anthropic / OpenAI / Gemini / DeepSeek models, and a free pool. The free-rotate sentinel rotates across the tool-capable free models in FREE_ROTATION_POOL, failing over to the next model on any non-2xx response (free endpoints flap with 404/429), with a short per-model cooldown (freeRotation.ts). This keeps the copilot running for free. Paid models degrade instead of hard-failing: when a paid OpenRouter model on the server key hits the key’s monthly limit or runs out of credits (402, or a 403 whose body says so), the request is rewritten to the free pool and handed to the same rotation (createPaidToFreeFallbackFetch). Moderation 403s and other errors pass through untouched, and every degradation is persisted as a rotation event so it shows up in the admin observability view. Role-gating rules:
  • Free models are exempt from role-gating (zero cost) — any user can use them.
  • Paid models on the server key still require an eligible COPILOT_SERVER_KEY_ROLES role or the user’s own BYOK key. A base user cannot bill the operator’s paid balance.
Free-by-default for everyone: the operator sets COPILOT_PROVIDER=openrouter + COPILOT_MODEL=free-rotate (the consented baseline) to run the copilot free out of the box. Verify free tool-calling against the live API:
This is a manual operator tool, not run in CI.

Human-in-the-Loop Trust Model

Frontend writes are CopilotKit actions (client/src/components/copilot/). The trust tiers mirror the agent-bridge model:
  • Low-risk writes auto-execute (no approval): create/complete/reopen tasks, upsert contact/influencer, add notes, add/remove labels, link a screenshot to a deal, create a short link, save memory.
  • Sensitive writes and ALL deletes require an in-chat ApprovalCard (renderAndWaitForResponse): create/update deal, bulk view-check import, payment add/update/delete, and every delete (link, screenshot, note).
Server tools are reads plus safe low-risk writes only — the sensitive operations live on the frontend behind the approval gate.

Self-Learning (Per-User)

The copilot learns per user across sessions. This is not a native CopilotKit feature — it is built on the existing agent_memories table. The loop:
  1. HITL approve/cancel of sensitive actions emits a signal to agent_learning_signals (POST /api/copilot/feedback, fire-and-forget from the approval cards).
  2. A repeated pattern (≥3 of the same action + outcome) graduates into a durable agent_memories row (sourceType='learned', server-templated content — never free text, so it is injection-safe).
  3. buildDealDashAgent injects the top active learnings (userId-scoped, character-budgeted, labeled non-authoritative) into the system prompt each session.
  4. Stale learned memories decay/archive after 60 days unreinforced (curateLearnings, throttled).
Strictly per-user — there is no cross-tenant learning. The explicit save_memory / list_memories tools feed the same injection path. Logic lives in server/lib/copilot/learning.ts.

Codex (Experimental, Off by Default)

A Codex / ChatGPT-subscription provider is available behind the COPILOT_CODEX_EXPERIMENTAL flag (default false). When the flag is unset, Codex is absent from the catalog, rejected on save, and hidden in the settings card — zero behavior change. It is unofficial / ToS-grey: it drives the copilot via the private chatgpt.com/backend-api/codex/responses endpoint using your personal ChatGPT/Codex subscription. There is no API key — you “Connect your Codex login” by pasting ~/.codex/auth.json (run codex login locally first). The server parses it into a normalized token blob, stores only that blob, encrypted at rest (never the raw paste, never returned to the client), and self-refreshes the access token before each request (server/lib/copilot/codex.ts). Egress consent is still required (Codex is non-default). Caveat — tool calls are UNVERIFIED. The private endpoint may strip the tools array, which would make Codex unusable for the tool-driven copilot. This cannot be confirmed without a real token. Before trusting Codex, run the make-or-break gate:
This is a manual operator tool, not run in CI.

Admin Controls

The copilot has an operator/admin surface that acts across users — a distinct third concern from /api/copilot (per-user, acts-as-the-signed-in-user) and /api/agent/* (the external MCP bridge). It is admin-only and reads are deliberately separate from the per-user query layer so the privilege boundary is explicit. Dashboard: /admin/copilot (client/src/pages/admin/Copilot.tsx, admin-only via AdminRoute, linked from the admin nav). Tabs:
  • Overview — usage headline, learning aggregates, free-rotation snapshot, and the free-tier cap.
  • Usage — paginated per-user FREE-model usage rollups.
  • Providers — every user’s provider/model, BYOK presence (boolean only), and egress consent — all secrets redacted.
  • Learning — self-learning signal and graduated-memory aggregates across users (CRM data is only counted, never surfaced).
  • Observability — free-rotation failovers, per-model aggregates, and live cooldowns.
Admin endpoints (server/routes/adminCopilot.ts, mounted at /api/admin/copilot with requireAuth + requireAdmin, ahead of the generic /api/admin): GBrain project memory for the in-app copilot is a separate server-side concern from local Claude/Codex stdio MCP config. Operators can expose it in either of two ways:
  • Register a public HTTP/SSE GBrain MCP endpoint in the admin MCP registry.
  • Set COPILOT_GBRAIN_MCP_URL with optional COPILOT_GBRAIN_MCP_TRANSPORT=http|sse and COPILOT_GBRAIN_MCP_AUTH_HEADER.
Use HTTP when the endpoint needs a static auth header; unauthenticated/public SSE is allowed. The env-backed GBrain endpoint is loaded before DB-registered MCP rows, skips duplicate DB rows for the same URL, and remains available if the registry query fails. It uses the same public-URL SSRF guard and header validation as admin-registered MCP servers; do not put credentials in the URL. Production should also keep DEALDASH_GBRAIN_REQUIRE_CLOUD=true with a dedicated DEALDASH_GBRAIN_DATABASE_URL. The runtime /api/readiness probe reads GBrain config.version when cloud GBrain is required, so deployment health catches a missing or unhealthy project-memory database. The cross-user query layer (server/lib/copilot/admin.ts) is intentionally separate from the per-user functions in usage.ts / providerConfig.ts / learning.ts. BYOK key ciphertext is never selected or exposed (only hasByokKey: boolean); learning proposedArgs (user CRM data) is only ever counted, never surfaced verbatim. Admin-settable free cap. The free-tier daily limit is set in the dashboard and persisted in the global app_settings table. usage.resolveFreeDailyLimit() makes the admin copilotFreeDailyLimit win (an explicit 0 means unlimited) over the COPILOT_FREE_DAILY_LIMIT env, which is only the fallback when an admin has never set it. System settings + enforcement. The admin Settings page (GET/PUT /api/admin/settings) now really hydrates and persists its system toggles (also backed by app_settings):
  • Maintenance mode (server/middleware/maintenance.ts) blocks non-admin /api traffic when on — exempting /api/auth, /api/admin, and health — and is fail-open. Admins always pass through.
  • Public signup (allowPublicSignup) is enforced in resolveUserFromToken: when off, it blocks NEW account creation while leaving EXISTING users unaffected (fail-open).
Persisted free-rotation failover events (copilot_rotation_events) and the app_settings table are both admin-only (RLS enabled, deny-by-default with no permissive policy); the server’s RLS-bypassing connection serves them through the requireAdmin-gated routes only.

Security

  • Identity always comes from the JWT. Tools call /api/* over loopback with the signed-in user’s token, so every read/write is scoped and audited like a normal request.
  • Provider keys are encrypted at rest (AES-256-GCM), never returned to the client, and never reach the model. Provider error bodies are redacted before reaching the model (tool.ts) so a provider 401 cannot leak a key.
  • Strict per-user isolation. Provider config, BYOK keys, and learned memories are all userId-scoped with RLS; there is no cross-tenant access or learning.
  • Admin reads are an explicit, requireAdmin-gated exception to the per-user RLS default — operator-across-users visibility, with BYOK key ciphertext never selected and all secrets redacted.

QA Status

The live-LLM QA for the copilot is operator-run, not automated in CI. Do not assume an automated pass. The manual QA handoff is in docs/superpowers/plans/2026-06-13-copilot-QA-checklist.md.

Source of Truth

  • README_FOR_AI.md — the “In-app AI copilot” sections (authoritative current description).
  • docs/superpowers/plans/2026-06-13-copilot-*.md — the implementation plans: multiprovider-foundation, openrouter-catalog, self-learning, codex-experimental, settings-ui, isolation-suite, and QA-checklist.