# Numu × Claude AI Connectors — Production Rollout Checklist

A linear list of things ops must verify before flipping `NUMU_AI_ENABLED=true` in production.

Pair this doc with [`config/ai.php`](../config/ai.php) — every checklist item below traces back to a knob in that file or an env var it reads.

---

## 1. Environment variables

Set in `.env` (or your secret manager):

```ini
# Master flag — leave OFF until every other item below is checked.
NUMU_AI_ENABLED=false

# Circuit breaker (defaults shown; tighten after settle-in)
NUMU_AI_CB_WINDOW=60        # seconds of sliding-window error counting
NUMU_AI_CB_THRESHOLD=50     # errors in window before lockout
NUMU_AI_CB_LOCKOUT=300      # seconds locked once tripped

# CORS — comma-separated. Browser-origin install in claude.ai uses this.
# Server-to-server MCP traffic does NOT need an entry.
NUMU_AI_CORS_ALLOWED_ORIGINS=https://claude.ai

# Optional — disable URL query-string scrubbing if your payloads
# genuinely need full URLs in audit. Default is safe (true).
# NUMU_AI_SCRUB_QUERY_STRINGS=false
```

## 2. Database-level append-only grant

The PHP-level observer ([`AiActivityLogObserver`](../app/Observers/AiActivityLogObserver.php)) catches dev mistakes loudly. The real boundary is at the DB:

```sql
-- Revoke from the application's DB user.
REVOKE UPDATE, DELETE ON numu.ai_activity_logs FROM 'numu_app'@'%';

-- A separate, audited user gets the retention privileges. Use only
-- from a documented sweeper job — never from request-path code.
GRANT DELETE ON numu.ai_activity_logs TO 'numu_retention'@'localhost';
```

After running the grant:

```sql
-- Verify
SHOW GRANTS FOR 'numu_app'@'%';
-- Expected: INSERT, SELECT only on ai_activity_logs.
```

## 3. Queue worker

The task pipeline ([Week 6](../app/Jobs/Ai/StartAiTaskJob.php)) needs queues `ai-bulk` and `ai-read` continuously drained.

### supervisord unit (recommended)

```ini
; /etc/supervisor/conf.d/numu-ai-queue.conf
[program:numu-ai-queue]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/numu/artisan queue:work --queue=ai-bulk,ai-read --tries=3 --sleep=2 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/numu/ai-queue.log
stopwaitsecs=60
```

Reload supervisor:

```bash
sudo supervisorctl reread && sudo supervisorctl update
sudo supervisorctl start numu-ai-queue:*
```

### Why two procs

One handles the long-running `ai-bulk` enumeration; the other drains the per-item `ai-read` chunks. A single proc would let a 5,000-investor enumeration block fan-out.

### Health check

The dashboard's [Sessions page](../resources/views/admin/ai/sessions/index.blade.php) bumps counters in-place; if `tool_call_count` stops climbing on a `running` session you have a queue stoppage. Cross-check with:

```bash
php artisan queue:monitor ai-bulk,ai-read
```

## 4. MCP server

Run from [`mcp-server/`](../mcp-server/README.md). Recommended:

```ini
; /etc/supervisor/conf.d/numu-mcp.conf
[program:numu-mcp]
command=/usr/bin/node /var/www/numu/mcp-server/dist/index.js
environment=NUMU_LARAVEL_URL="https://numu.example.com",NUMU_MCP_PORT="7411"
directory=/var/www/numu/mcp-server
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/numu/mcp-server.log
```

Build before first run:

```bash
cd /var/www/numu/mcp-server
npm ci --omit=dev
npm run build
```

### Reverse-proxy block

Front with nginx so Claude.ai talks to `mcp.numu.example.com` over TLS:

```nginx
server {
    listen 443 ssl http2;
    server_name mcp.numu.example.com;

    # Stream long responses — MCP Streamable HTTP keeps connections
    # open for the SSE leg.
    location /mcp/ {
        proxy_pass http://127.0.0.1:7411;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_read_timeout 300s;
    }
    location = /healthz {
        proxy_pass http://127.0.0.1:7411;
    }
}
```

## 5. CORS

The [`cors.php`](../config/cors.php) policy scopes only to `api/v1/ai/*`. Confirm with a preflight test once `NUMU_AI_ENABLED=true`:

```bash
curl -i -X OPTIONS https://numu.example.com/api/v1/ai/investors \
  -H 'Origin: https://claude.ai' \
  -H 'Access-Control-Request-Method: GET' \
  -H 'Access-Control-Request-Headers: authorization,x-numu-session-id'
# Expect 204 + the right Access-Control-Allow-* headers.
```

A failed preflight = wrong env value. Common gotcha: a trailing slash on the origin (`https://claude.ai/`) fails to match `https://claude.ai`.

## 6. Smoke once-over before the flip

Run from a workstation that can reach production:

```bash
# 1. Health
curl -s https://mcp.numu.example.com/healthz | jq .

# 2. Auth boundary
curl -s -o /dev/null -w "%{http_code}\n" https://numu.example.com/api/v1/ai/investors
# Expect: 401

# 3. With a real Read token issued from /admin/ai/connectors
TOKEN=...
curl -s -o /dev/null -w "%{http_code}\n" \
  https://numu.example.com/api/v1/ai/investors \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Numu-Session-Id: smoke-prod-$(date +%s)"
# Expect: 200

# 4. The audit row landed (DB shell)
SELECT id, action_type, connector, admin_id, ip, created_at
FROM ai_activity_logs
WHERE action_type = 'investor.list'
ORDER BY id DESC LIMIT 5;
```

## 7. Operational dashboards

After enabling, the admin team monitors:

- [`/admin/ai/sessions`](../resources/views/admin/ai/sessions/index.blade.php) — one row per Claude conversation.
- [`/admin/ai/activity`](../resources/views/admin/ai/activity/index.blade.php) — flat feed with date / admin / connector filters.
- [`/admin/ai/tasks`](../resources/views/admin/ai/tasks/index.blade.php) — task progress + resume.
- [`/admin/ai/change-requests`](../resources/views/admin/ai/change-requests/index.blade.php) — approval queue.

## 8. Rollback procedure

Two flips, no DB changes required:

```bash
# 1. Block all NEW traffic
php artisan tinker --execute='Config::set("ai.enabled", false);'
# OR more durably: edit .env, then `php artisan config:clear`.

# 2. Stop in-flight queue work (graceful)
sudo supervisorctl stop numu-ai-queue:*

# Optional 3. Revoke specific tokens individually from
#   /admin/ai/connectors — does NOT require a full disable.
```

A re-enable is just `NUMU_AI_ENABLED=true` + `config:cache`.

## 9. What's deliberately NOT in this rollout

These are documented elsewhere or intentionally out of scope:

| Item | Where it lives |
|---|---|
| Building a Read token UI for an analyst role with no manage perm | Future ticket — current model gates token creation behind `ai.connectors.manage` |
| Real LLM bio generation in `EnrichBiosTaskKind` | [Week 6 doc](../app/Services/Ai/Tasks/Kinds/EnrichBiosTaskKind.php#L66) — swap `draftBio()` for an Anthropic API call |
| Horizon (Redis queue dashboard) | Out of scope — DB queue chosen for ops simplicity. Add when fan-out exceeds DB queue throughput. |
| Pen test | External engagement |

---

## Sign-off checklist

Before merging the production toggle PR:

- [ ] Env vars set in `.env` AND in the secret manager
- [ ] DB grant verified with `SHOW GRANTS`
- [ ] supervisord units installed + reloaded
- [ ] MCP server reachable over HTTPS with valid cert
- [ ] CORS preflight returns 204 from a `claude.ai` origin
- [ ] Smoke once-over (section 6) passes
- [ ] Dashboards (`/admin/ai/*`) load for the admin team
- [ ] One Read token + one Full token minted and stored in the team password manager
- [ ] Rollback procedure (section 8) tested in staging
