From 6d8eeea4ebae15132ca68f7174db107c4b30cab6 Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 20:40:12 -0400 Subject: [PATCH 01/13] fix(rate_limiter): correct empty dict initialization (tuple->dict) The RateLimiter.__init__ used =() (empty tuple) instead of ={} (empty dict) for the _sessions field. This caused an AttributeError on the first request because tuples don't have a .get() method. Fixes: C1 (CRITICAL - runtime crash on first request) --- scripts/rate_limiter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rate_limiter.py b/scripts/rate_limiter.py index 4525186..b2666ac 100644 --- a/scripts/rate_limiter.py +++ b/scripts/rate_limiter.py @@ -36,7 +36,7 @@ class RateLimiter: def __init__(self, config: RateLimitConfig, redis_client=None): self.config = config self.redis = redis_client - self._sessions: dict[str, SessionState] =() + self._sessions: dict[str, SessionState] = {} def check_request(self, session_id: str, user_id: str, message_length: int = 0) -> dict: """ From 982a54bbdafdb1ddbd2f4f2fbcea7a21f462f183 Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 20:40:22 -0400 Subject: [PATCH 02/13] fix(health_monitor): correct Redis URL default (postgres->redis) The default REDIS_URL referenced postgres:5432 (PostgreSQL port) instead of redis:6379. This was a copy-paste error that would cause the health monitor to fail connecting to Redis if the env var was not set. Fixes: C2 (CRITICAL - wrong Redis URL default) --- scripts/health_monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/health_monitor.py b/scripts/health_monitor.py index a2d751f..47e667b 100644 --- a/scripts/health_monitor.py +++ b/scripts/health_monitor.py @@ -17,7 +17,7 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") logger = logging.getLogger("health-monitor") -REDIS_URL = os.getenv("REDIS_URL", "redis://redis:***@postgres:5432/helpdesk") +REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0") SERVICES = { "llama": "http://llama:8081/health", From d9a71d96eb33de65ba889eb17ef76f3f89ac397b Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 20:40:40 -0400 Subject: [PATCH 03/13] fix(whatsapp_webhook): correct Redis URL default (postgres->redis) The default REDIS_URL referenced postgres:5432 (PostgreSQL port) instead of redis:6379. This was a copy-paste error that would cause the WhatsApp webhook to fail connecting to Redis if the env var was not set. Fixes: C3 (CRITICAL - wrong Redis URL default) --- scripts/whatsapp_webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/whatsapp_webhook.py b/scripts/whatsapp_webhook.py index 261dfdf..3b1ade0 100644 --- a/scripts/whatsapp_webhook.py +++ b/scripts/whatsapp_webhook.py @@ -26,7 +26,7 @@ # ═══════════════════════════════════════════════════ HELPDESK_AGENT_URL = os.getenv("HELPDESK_AGENT_URL", "http://helpdesk-agent:8080") -REDIS_URL = os.getenv("REDIS_URL", "redis://redis:***@postgres:5432/helpdesk") +REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0") WHATSAPP_WEBHOOK_SECRET = os.getenv("WHATSAPP_WEBHOOK_SECRET", "change_me") WHATSAPP_TOKEN = os.getenv("WHATSAPP_TOKEN", "") WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "") From 183bfde149912828d6b962b8e77c9c1a1c847cee Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 20:40:54 -0400 Subject: [PATCH 04/13] fix(zammad): add missing import requests The Zammad adapter used requests.post(), requests.get(), and requests.patch() but never imported the requests module. Any ticket operation on Zammad would crash with NameError. Fixes: C4 (CRITICAL - NameError on Zammad operations) --- ticket_platforms/zammad.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ticket_platforms/zammad.py b/ticket_platforms/zammad.py index d680c72..1d1ee8e 100644 --- a/ticket_platforms/zammad.py +++ b/ticket_platforms/zammad.py @@ -6,6 +6,8 @@ from typing import Any +import requests + from .base import Ticket from .registry import register From 306bedea813e97e8fe244118da7d3030df566f93 Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 20:41:15 -0400 Subject: [PATCH 05/13] chore(dependabot): remove npm ecosystem (no package.json) Dependabot was configured for npm ecosystem but no package.json exists in the repository. This was a template vestige from the J1 repo template that would cause Dependabot errors. Fixes: D2 (DEGRADED - Dependabot ecosystem mismatch) --- .github/dependabot.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4f46c24..8439320 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,11 +5,6 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 10 - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 10 - package-ecosystem: "docker" directory: "/" schedule: From a12a0209e03ba793ce8bc1ab0169ac645e2929a6 Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 20:41:31 -0400 Subject: [PATCH 06/13] chore: remove dead code osticket_tool.py The standalone osTicket wrapper (osticket_tool.py) predates the adapter pattern and is superseded by ticket_platforms/osticket.py. It is not referenced by any Docker service, config, or import. Fixes: D4 (DEGRADED - dead code) --- osticket_tool.py | 155 ----------------------------------------------- 1 file changed, 155 deletions(-) delete mode 100644 osticket_tool.py diff --git a/osticket_tool.py b/osticket_tool.py deleted file mode 100644 index 5c37b3e..0000000 --- a/osticket_tool.py +++ /dev/null @@ -1,155 +0,0 @@ -""" -osTicket tool wrapper for Hermes Agent. -Uses osTicket REST API (v1 HTTP API). -Assumes an API-enabled agent account exists in osTicket. -""" - -import os -import re - -import requests - -OSTICKET_BASE_URL = os.environ.get("OSTICKET_BASE_URL", "").rstrip("/") -OSTICKET_API_KEY = os.environ.get("OSTICKET_API_KEY", "") - -DEFAULT_DEPT_ID = int(os.environ.get("OSTICKET_DEFAULT_DEPT_ID", "1")) -DEFAULT_PRIORITY = os.environ.get("OSTICKET_DEFAULT_PRIORITY", "low") -DEFAULT_SOURCE = os.environ.get("OSTICKET_DEFAULT_SOURCE", "Web") -RATE_LIMIT_PER_MIN = int(os.environ.get("OSTICKET_RATE_LIMIT_PER_MIN", "5")) - - -def _require_config(): - missing = [] - if not OSTICKET_BASE_URL: - missing.append("OSTICKET_BASE_URL") - if not OSTICKET_API_KEY: - missing.append("OSTICKET_API_KEY") - if missing: - raise RuntimeError(f"Missing osTicket config: {', '.join(missing)}") - - -def _headers(): - return { - "X-API-Key": OSTICKET_API_KEY, - "Content-Type": "application/json", - } - - -def _sanitize(text: str) -> str: - if not text: - return "" - text = re.sub(r"<[^>]+>", "", text) # strip HTML tags - text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", text) # strip control chars - return text.strip() - - -def create_ticket( - user_id: str, - subject: str, - body: str, - *, - name: str | None = None, - email: str | None = None, - dept_id: int | None = None, - priority: str | None = None, - source: str | None = None, -) -> dict: - """ - Create a ticket in osTicket. - user_id: Hermes-side identifier (Telegram id, etc.) - """ - _require_config() - subject = _sanitize(subject) - body = _sanitize(body) - - if not name: - name = f"User {user_id}" - if not email and user_id: - email = f"telegram+{user_id}@helpdesk.local" - - payload = { - "name": name, - "email": email, - "phone": "", - "subject": subject, - "message": body, - "ip": "", - "priority": priority or DEFAULT_PRIORITY, - "status": "open", - "deptId": dept_id or DEFAULT_DEPT_ID, - "source": source or DEFAULT_SOURCE, - } - - url = f"{OSTICKET_BASE_URL}/api/http.php/tickets.json" - r = requests.post(url, json=payload, headers=_headers(), timeout=30) - r.raise_for_status() - data = r.json() - ticket = data.get("ticket", {}) - return { - "ticket_id": str(ticket.get("ticket_id") or data.get("id", "")), - "number": ticket.get("number"), - "status": ticket.get("status"), - "subject": subject, - } - - -def update_ticket(ticket_id: str, status: str | None = None, note: str | None = None) -> dict: - _require_config() - payload = {} - if status: - payload["status"] = status - if note: - payload["post"] = _sanitize(note) - payload["post_status"] = "open" - - url = f"{OSTICKET_BASE_URL}/api/http.php/tickets/{ticket_id}.json" - r = requests.put(url, json=payload, headers=_headers(), timeout=30) - r.raise_for_status() - return {"ticket_id": ticket_id, "status": status or "updated"} - - -def search_tickets(user_id: str, query: str, limit: int = 10) -> list[dict]: - _require_config() - q = _sanitize(query) - url = f"{OSTICKET_BASE_URL}/api/http.php/tickets.json" - params = { - "query": q, - "limit": limit, - } - r = requests.get(url, params=params, headers=_headers(), timeout=30) - r.raise_for_status() - data = r.json() - tickets = data.get("tickets", []) if isinstance(data, dict) else (data or []) - out = [] - for t in tickets: - out.append({ - "ticket_id": str(t.get("ticket_id") or t.get("id")), - "number": t.get("number"), - "subject": t.get("subject"), - "status": t.get("status"), - }) - return out - - -def close_ticket(ticket_id: str, reason: str | None = None) -> dict: - _require_config() - payload = {"status": "closed"} - if reason: - payload["post"] = _sanitize(reason) - payload["post_status"] = "closed" - url = f"{OSTICKET_BASE_URL}/api/http.php/tickets/{ticket_id}.json" - r = requests.put(url, json=payload, headers=_headers(), timeout=30) - r.raise_for_status() - return {"ticket_id": ticket_id, "status": "closed"} - - -if __name__ == "__main__": - # Quick connectivity check (does not require config if vars are set) - if OSTICKET_BASE_URL and OSTICKET_API_KEY: - try: - res = search_tickets("local", "test", limit=1) - print("osticket API OK, sample:", res[:1]) - except Exception as e: - print("osticket API check failed:", e) - else: - print("Set OSTICKET_BASE_URL and OSTICKET_API_KEY to test.") From 931fcad4e79b80952ec030538fa28e1f9bafc682 Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 20:41:47 -0400 Subject: [PATCH 07/13] chore(docker): pin searxng and n8n image tags Replaced :latest tags with specific versions for reproducible builds: - searxng/searxng:2025.1.1 - n8nio/n8n:1.80.0 Fixes: D7 (DEGRADED - unpinned Docker image tags) --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ebf3107..d37653b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -158,7 +158,7 @@ services: # SearXNG (Self-hosted Web Search) # ═══════════════════════════════════════════════════ searxng: - image: searxng/searxng:latest + image: searxng/searxng:2025.1.1 container_name: helpdesk-searxng ports: - "127.0.0.1:8888:8080" @@ -185,7 +185,7 @@ services: # n8n (Workflow Automation) # ═══════════════════════════════════════════════════ n8n: - image: n8nio/n8n:latest + image: n8nio/n8n:1.80.0 container_name: helpdesk-n8n ports: - "127.0.0.1:5678:5678" From d909f0a8b76147ebb903d7cbccef45b6d9ee249d Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 20:42:36 -0400 Subject: [PATCH 08/13] chore: add .dockerignore for smaller build context Adds a .dockerignore file to exclude unnecessary files from the Docker build context: .git/, secrets, models, data, IDE files, logs, reports, and non-essential directories (compose/, admin/, skills/, tools-ui/, workflows/). Fixes: D8 (DEGRADED - no .dockerignore) --- .dockerignore | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..927e021 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,50 @@ +# Git +.git/ +.gitignore + +# Secrets +.env +.env.example +.secrets/ +certs/ +*.pem +*.key + +# Models +models/ +*.gguf + +# Data +data/ +knowledge-base/ +email-queue/ + +# IDE +.idea/ +.vscode/ +*.swp + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Reports +reports/ + +# Not needed for build +.github/ +compose/ +admin/ +skills/ +tools-ui/ +workflows/ +*.md +Makefile +docker-compose*.yml +j1.yaml +helpdesk-agent-diagram-guide.html +dashboard-mockup.png +dashboard-realistic.png From c742a6a2691c35fa7fb2627db8fd30ea5443439d Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 21:32:57 -0400 Subject: [PATCH 09/13] chore: add __pycache__ and *.pyc to .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 6d5db69..c4b39d2 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,10 @@ email-queue/ .DS_Store Thumbs.db +# Bytecode +__pycache__/ +*.pyc + # Logs *.log data/logs/ From 0db7139fb553dfbbbabb9627617169744dad4d4b Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 21:33:00 -0400 Subject: [PATCH 10/13] =?UTF-8?q?docs(oracle):=20add=20INTENT.md=20?= =?UTF-8?q?=E2=80=94=20J1-PIPELINE=20Phase=20-1=20audit=20reconnaissance?= =?UTF-8?q?=20report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INTENT.md | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 INTENT.md diff --git a/INTENT.md b/INTENT.md new file mode 100644 index 0000000..4075691 --- /dev/null +++ b/INTENT.md @@ -0,0 +1,252 @@ +# INTENT.md — J1-PIPELINE Phase -1 (ORACLE) + +**Repository:** `OneByJorah/CommandDesk` +**Analysis Date:** 2026-07-05 +**Analyst:** J1-PIPELINE ORACLE (read-only) +**Status:** Intent Reconstructed + +--- + +## What This System Does + +### Technical Role + +CommandDesk is a **self-hosted, AI-powered helpdesk agent platform** that bridges local LLM inference with existing enterprise ticketing infrastructure. It is a complete, containerized stack that: + +1. **Runs a local LLM** (Qwen2.5-7B-Instruct via llama.cpp, Q4_K_M GGUF, 65K context) for all AI operations — no external API calls, no data leaving the host. +2. **Connects to multiple ticketing backends** via a plug-in adapter architecture: osTicket (REST API v1), Freshdesk (REST API v2 + MCP protocol), and Zammad (REST API). +3. **Accepts support requests from multiple channels**: WhatsApp Business API (webhook with intent detection), Email/IMAP (polling with email-to-ticket conversion), and a web interface (chat widget + PWA). +4. **Provides two-tier AI agent access**: + - **Helpdesk Agent** (port 8080) — restricted, end-user-facing. Can search/view/update/close the user's own tickets. Cannot create tickets. + - **Admin Agent** (port 8082) — full-access, internal. Can create tickets, manage knowledge base, view analytics, check system health, manage all tickets across all users. +5. **Maintains a semantic knowledge base** via ChromaDB (all-MiniLM-L6-v2 embeddings, cosine similarity) for instant answer retrieval from markdown/text documents. +6. **Automates workflows** via n8n: smart ticket routing (keyword-based classification), auto-escalation, daily digests (weekdays 9AM), satisfaction surveys (every 4 hours), and ticket-created notifications. +7. **Enforces security** at every layer: rate limiting (per-session + per-IP + per-endpoint via Nginx zones), content filtering (PII patterns: password, credit_card, ssn), audit logging (all requests logged to PostgreSQL), IP whitelisting on admin routes (Docker network only), Nginx reverse proxy with security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options), request size limits (4KB), API key authentication for admin agent, webhook signature verification (WhatsApp HMAC-SHA256). +8. **Provides observability** via a health monitor service (30s polling, Redis-backed metrics), analytics scripts (token usage, resolution rates, session stats, cost tracking, top issues), a web-based admin dashboard, and health endpoints on every service. + +### Operational Role + +CommandDesk operates as a **production-grade AI customer support layer** that sits on top of existing helpdesk systems. It does not replace osTicket/Freshdesk/Zammad — it augments them with AI triage, auto-response, and self-service capabilities while keeping all customer data on-premises. + +The system is designed to be deployed via `docker compose` with a single command, requiring only a GGUF model file and environment variables for the ticketing backends. It can also run in a standalone mode (outside Docker) using the `hermes-llama-cpp.config.yaml` and `llama-cpp-server.service` systemd unit. + +The system also includes an **agent bridge** (`config/agent-bridge.yaml`) that allows the main Hermes agent to delegate helpdesk/ticket/support intents to the CommandDesk agents, making it a drop-in helpdesk capability for the broader J1 ecosystem. + +--- + +## Why This Was Built + +### Real Problem + +Customer support teams face a fundamental tension: **AI helpdesk tools (Zendesk AI, Intercom Fin, Freshdesk Freddy) are powerful but expensive, send customer data to third-party cloud providers, and cannot be customized or extended by the team running them.** Open-source ticketing systems (osTicket, Zammad) are self-hosted but have no native AI capabilities. The gap between "self-hosted but dumb" and "AI-powered but cloud-locked" leaves organizations with an impossible choice: sacrifice privacy or sacrifice efficiency. + +### Why Existing Tools Were Insufficient + +| Tool | Limitation | +|------|-----------| +| **osTicket / Zammad** | No AI capabilities. Manual triage only. No knowledge base search. | +| **Freshdesk (free plan)** | No AI. API-only. Limited to 1 agent. | +| **Zendesk AI / Intercom Fin** | Cloud-only. Per-ticket pricing. Customer data leaves the host. No local LLM support. | +| **Generic LLM chatbots** | No ticketing integration. No multi-channel support. No workflow automation. No rate limiting or audit. | +| **DIY (LangChain + ticketing API)** | Fragile. No production hardening. No health monitoring. No session management. No human takeover flow. | + +### What Triggered Development + +The repository's commit history (starting 2026-06-16) shows a clear progression: + +1. **Initial scaffolding** (c79c1d1) — "Initial: helpdesk agent guide, llama.cpp configs, osTicket wrapper, admin dashboard." The project started as a standalone Hermes agent with osTicket integration. +2. **Adapter pattern** (09d6ce2) — "Add ticket platform registry, adapters, and updated guide/dashboard." The plug-in architecture was introduced immediately, suggesting the author knew multi-platform support was essential from day one. +3. **Containerization** (09bd882, bdef1d3, 963133d) — CI workflow, Docker Compose, self-hosted container stacks. The system was designed for Docker-first deployment from the beginning. +4. **Multi-channel expansion** (79b140b, 8ea7a9a, 8d093ea) — WhatsApp webhook, human takeover flow, tools UI, CI pipeline, screenshots. The system became production-ready. +5. **Enterprise hardening** (dbbffa7, 68030cf) — n8n workflows, MCP registry, analytics, production compose, J1 Dev Ops dashboard. The system became an enterprise platform. +6. **Standardization** (9e7c463, de89ae1) — Documentation, ruff auto-fixes, portfolio standardization. +7. **Security audit** (5c7db2d) — "audit(CommandDesk): sanitize email references." A dedicated security pass was made. + +The trigger was the realization that **no existing open-source project filled the gap between "self-hosted ticketing" and "AI-powered support"** in a way that was production-ready, secure, and extensible. + +### Ecosystem Fit + +CommandDesk is part of the **JorahOne (OneByJorah)** portfolio of self-hosted enterprise tools. It follows the same patterns as other J1 projects: + +- **100% self-hosted** — no cloud dependencies, no telemetry, no API keys for third-party AI services. +- **Docker Compose-first deployment** — single-command startup, consistent with other J1 stacks. +- **Plug-in architecture** — the ticket platform adapter pattern (`ticket_platforms/registry.py`) mirrors the skill/plugin architecture used across J1. +- **Production hardening** — rate limiting, audit logging, health monitoring, security headers, IP whitelisting. +- **n8n workflow integration** — automation layer shared across J1 projects. +- **Hermes Agent compatibility** — the system is designed to be bridged to the main Hermes agent via the agent-bridge config, making it a drop-in helpdesk capability for the broader J1 ecosystem. +- **MCP (Model Context Protocol) support** — Freshdesk integration uses the MCP protocol, making it compatible with the broader MCP ecosystem. + +The `compose/` directory contains overlay stacks for a broader self-hosted infrastructure vision: +- **Self-hosted stack**: STT (Whisper), TTS (Piper), headless browser (Alpine Chrome), Honcho (multi-agent memory) +- **Plus stack**: Alternative STT/TTS/browser/Honcho images +- **Automation**: n8n standalone +- **Monitoring**: Uptime Kuma +- **Mail**: Postal (self-hosted mail server) +- **Git**: Gitea +- **Knowledge**: SearXNG standalone +- **Storage**: PostgreSQL (pgvector) + MinIO +- **Wiki**: Outline +- **CI**: Python compile-check container + +This suggests CommandDesk was envisioned as the **helpdesk component of a larger self-hosted enterprise platform**, not just a standalone tool. + +--- + +## Operational Classification + +| Category | Classification | Evidence | +|----------|---------------|----------| +| **Production** | ✅ **PRIMARY** | Production docker-compose override (`.prod.yml`) with health checks, restart policies (`always`), resource limits (CPU + memory), health monitor service (30s polling), CI pipeline (lint + build + smoke test), CodeQL scanning (Python + JavaScript + TypeScript), Dependabot (pip + npm + docker + github-actions), security audit commit in history. Designed for real customer support operations. | +| **Automation** | ✅ **PRIMARY** | n8n workflows (ticket routing with keyword classification, auto-escalation, daily digest via cron, satisfaction surveys every 4 hours, ticket-created notifications), email-to-ticket conversion (IMAP polling every 60s), WhatsApp intent detection and routing (6 intent patterns), auto-response via local LLM. | +| **Security** | ✅ **PRIMARY** | Rate limiting at 3 layers (Nginx zones: general 10r/s, API 30r/s, login 1r/s; per-session limits; WhatsApp per-minute limits), content filtering (PII patterns: password, credit_card, ssn), audit logging (all requests logged to PostgreSQL), IP whitelisting on admin routes (Docker subnet only), Nginx security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy), request size limits (4KB), API key authentication for admin agent, webhook signature verification (WhatsApp HMAC-SHA256), `.env` never committed, secrets in `.gitignore`. | +| **Observability** | ✅ **PRIMARY** | Health monitor service (30s polling, Redis-backed metrics, 7 services monitored), analytics scripts (token usage, resolution rates, session stats, cost tracking, top issues, rate limit hits), admin dashboard (web-based), health endpoints on every service, JSON-format logging, PostgreSQL audit log. | +| **Experimental** | ❌ Not applicable | The system is past experimental — it has CI, security scanning, production configs, MCP support, and is designed for real deployment. | + +**Overall classification: Production / Automation / Security / Observability** + +CommandDesk is a **production-grade, self-hosted AI helpdesk agent** that automates customer support workflows while maintaining full data privacy. It is the answer to "how do we get AI-powered support without sending customer data to the cloud?" + +--- + +## Key Architectural Decisions + +1. **Local LLM only** — No OpenAI/Anthropic API keys. The system uses Qwen2.5-7B via llama.cpp with 65K context. This is a deliberate privacy-first choice. The model is downloaded once via `setup.sh` and runs entirely on-premises. + +2. **Adapter pattern for ticketing** — Each platform (osTicket, Freshdesk, Zammad) implements a common `Ticket` ABC with `create_ticket`, `update_ticket`, `search_tickets`, `close_ticket`. Adding a new platform means writing one class and registering it with `@register("name")`. The registry (`ticket_platforms/registry.py`) provides `get()`, `register()`, and `available()` functions. + +3. **Two-agent architecture** — Separating the end-user-facing agent (no ticket creation, restricted to own tickets) from the admin agent (full access, all tickets, analytics, KB management) prevents privilege escalation through the customer-facing interface. This is enforced at the config level (`hermes-config.yaml` vs `admin-agent-config.yaml`) and at the environment level (`ALLOW_CREATE_TICKET=false` vs `true`). + +4. **MCP (Model Context Protocol) support** — Freshdesk integration uses the MCP protocol (NeuraLegion/freshdesk_mcp, 41 tools), making it compatible with the broader MCP ecosystem. Other platforms use direct REST adapters. The MCP config (`mcp-config.yaml`) also defines granular permissions per agent mode (helpdesk vs admin). + +5. **n8n for workflows** — Rather than building a custom workflow engine, the system delegates to n8n, which provides a visual editor, scheduling (cron), and 400+ integrations. Five pre-built workflows cover the core automation needs: ticket routing, auto-escalation, daily digest, satisfaction surveys, and ticket-created notifications. + +6. **Redis for state** — Session state, rate limiting, and health metrics all live in Redis, making the agent services stateless and horizontally scalable. Redis is configured with password auth, LRU eviction (256MB max), and AOF persistence. + +7. **PostgreSQL for persistence** — Tickets, users, sessions, audit logs, and cost tracking are persisted in PostgreSQL with proper indexing (10+ indexes) and foreign keys. The schema includes UUID primary keys, JSONB metadata fields, and trigger-based `updated_at` timestamps. + +8. **Nginx as security gateway** — All external traffic goes through Nginx, which enforces rate limiting zones, security headers, IP whitelisting for admin routes, request size limits, and proxy caching. This keeps the application services isolated from direct external access. + +9. **Docker Compose-first deployment** — The entire stack (12+ containers) is defined in a single `docker-compose.yml` with a production override (`docker-compose.prod.yml`) that adds stricter health checks, resource limits, and restart policies. A `Makefile` provides convenience commands for common operations. + +10. **Agent bridge to Hermes** — The `config/agent-bridge.yaml` allows the main Hermes agent to delegate helpdesk/ticket/support intents to CommandDesk agents, making it a drop-in capability for the broader J1 ecosystem rather than a standalone tool. + +--- + +## Repository Structure + +``` +CommandDesk/ +├── admin/ # Admin dashboard (static HTML) +│ └── admin-dashboard.html +├── compose/ # Docker Compose overlay stacks +│ ├── docker-compose.yml # Hermes agent standalone +│ ├── docker-compose.automation.yml # n8n standalone +│ ├── docker-compose.ci.yml # CI compile-check container +│ ├── docker-compose.git.yml # Gitea +│ ├── docker-compose.knowledge.yml # SearXNG standalone +│ ├── docker-compose.mail.yml # Postal mail server +│ ├── docker-compose.monitoring.yml # Uptime Kuma +│ ├── docker-compose.plus.yml # Alternative STT/TTS/browser/Honcho +│ ├── docker-compose.selfhosted.yml # STT/TTS/browser/Honcho +│ ├── docker-compose.storage.yml # PostgreSQL (pgvector) + MinIO +│ ├── docker-compose.wiki.yml # Outline wiki +│ ├── Dockerfile # Hermes agent Dockerfile +│ └── requirements.txt # Python deps for compose +├── config/ # Application configuration +│ ├── admin-agent-config.yaml # Admin agent config (full access) +│ ├── agent-bridge.yaml # Hermes agent bridge config +│ ├── hermes-config.yaml # Helpdesk agent config (restricted) +│ ├── mcp-config.yaml # MCP server config + permissions +│ ├── mcp-registry.yaml # MCP server registry (6 platforms) +│ ├── nginx.conf # Nginx reverse proxy config +│ ├── searxng-settings.yml # SearXNG search engine config +│ └── system-prompt.md # Helpdesk agent system prompt +├── scripts/ # Utility scripts +│ ├── agent_server.py # FastAPI agent server (main entry point) +│ ├── analytics.py # Analytics report generator +│ ├── email_fetcher.py # IMAP email-to-ticket service +│ ├── health_monitor.py # Service health monitoring (30s loop) +│ ├── index_kb.py # Knowledge base ChromaDB indexer +│ ├── init-db.sql # PostgreSQL schema (7 tables, 15+ indexes) +│ ├── rate_limiter.py # Rate limiter middleware +│ ├── session_manager.py # Redis + PostgreSQL session manager +│ ├── setup.sh # One-time setup script +│ └── whatsapp_webhook.py # WhatsApp webhook receiver (639 lines) +├── skills/ # AI agent skills +│ ├── manifest.yaml # Skill manifest (j1-helpdesk + persona) +│ └── persona-customer-support/ # External skill (hazelugo/fav_gits) +│ ├── SKILL.md +│ └── .skillfish.json +├── ticket_platforms/ # Ticketing adapter library +│ ├── __init__.py # Package exports +│ ├── base.py # Ticket ABC (4 abstract methods) +│ ├── email.py # Email-to-ticket adapter +│ ├── freshdesk.py # Freshdesk REST API v2 adapter +│ ├── osticket.py # osTicket REST API v1 adapter +│ ├── registry.py # Adapter registry (register/get/available) +│ └── zammad.py # Zammad REST API adapter +├── tools-ui/ # Web UI components +│ ├── dashboard.html # Admin dashboard +│ ├── index.html # Chat widget +│ ├── mobile-app.html # Mobile PWA +│ ├── Dockerfile # Nginx-based UI container +│ └── manifest.json # PWA manifest +├── workflows/ # n8n workflow definitions +│ ├── auto-escalation.json # Escalation to admin + Slack +│ ├── daily-digest.json # Weekday 9AM stats digest +│ ├── satisfaction-survey.json # Every 4h survey for closed tickets +│ ├── ticket-created.json # New ticket notification +│ └── ticket-routing.json # Keyword-based ticket classification +├── .github/ # GitHub configuration +│ ├── dependabot.yml # pip + npm + docker + github-actions +│ ├── ISSUE_TEMPLATE/ # Bug report + feature request +│ ├── PULL_REQUEST_TEMPLATE.md +│ └── workflows/ +│ ├── ci.yml # Lint + config check + build + smoke test +│ └── codeql.yml # CodeQL security analysis (Python/JS/TS) +├── docker-compose.yml # Main deployment (12 services) +├── docker-compose.prod.yml # Production override (health checks, limits) +├── docker-compose.dev.yml # Development override (debug, exposed ports) +├── Dockerfile # Backend agent container +├── Dockerfile.email # Email fetcher container +├── Dockerfile.whatsapp # WhatsApp webhook container +├── Makefile # Build automation (20+ targets) +├── requirements.txt # Python dependencies (14 packages) +├── .env.example # Environment template (30+ vars) +├── .gitignore # 20 patterns +├── README.md # Project documentation +├── LICENSE # MIT +├── CODE_OF_CONDUCT.md # Contributor Covenant v2.1 +├── CONTRIBUTING.md # Contribution guidelines +├── SECURITY.md # Security policy (90-day disclosure) +├── hermes-llama-cpp.config.yaml # Standalone Hermes config (non-Docker) +├── llama-cpp-server.service # systemd unit for llama.cpp +├── memory_setup.py # SQLite memory setup (standalone mode) +├── osticket_tool.py # Standalone osTicket tool (pre-adapter) +├── helpdesk-agent-diagram-guide.html # Architecture diagram +├── dashboard-mockup.png # Dashboard screenshot +└── dashboard-realistic.png # Dashboard screenshot +``` + +### Notable Observations + +- **`knowledge-base/` directory does not exist** — Referenced in `docker-compose.yml` as a volume mount but absent from the repo. The `setup.sh` script creates a sample `welcome.md` file, but the directory itself is not tracked. +- **`certs/` directory is gitignored** — Referenced in `docker-compose.yml` and `nginx.conf` for HTTPS, but self-signed certs are generated by `setup.sh`. +- **`models/` directory is gitignored** — Expected; GGUF model files are large and downloaded by `setup.sh`. +- **No `tests/` directory** — No test files exist anywhere in the repo. The CI pipeline has a smoke test (PostgreSQL + Redis connectivity) but no unit tests or integration tests. +- **Dependabot configured for `npm` but no `package.json`** — The `dependabot.yml` lists `npm` as an ecosystem, but there is no `package.json` in the repo. This is a template vestige. +- **Two compose layers** — The root `docker-compose.yml` is the full production stack (12 services). The `compose/docker-compose.yml` is a simpler standalone Hermes agent. The `compose/` directory also contains 10 overlay stacks for a broader self-hosted infrastructure vision. +- **Two osTicket implementations** — `osticket_tool.py` (standalone, pre-adapter) and `ticket_platforms/osticket.py` (adapter-based). The standalone version appears to be the original approach, superseded by the adapter pattern. +- **Standalone mode** — `hermes-llama-cpp.config.yaml`, `llama-cpp-server.service`, and `memory_setup.py` suggest the system was originally designed to run outside Docker as a standalone Hermes agent with SQLite memory. + +--- + +## Notes + +- **Security audit in history**: Commit `5c7db2d` ("audit(CommandDesk): sanitize email references") and `68216b0` ("security: redact exposed tailscale IPs and demo emails") show active security maintenance. This is a positive maturity signal. +- **Very recent project**: All commits are from June-July 2026. The project is under active development. +- **Dependabot ecosystem mismatch**: Dependabot is configured for `npm` ecosystem, but no `package.json` exists. This is a template vestige from the J1 repo template. +- **No test files**: The repo has no `tests/` directory and no test files. The CI pipeline has a basic smoke test but no unit/integration tests. This is a gap for a production-classified repo. +- **Knowledge base directory missing**: `knowledge-base/` is referenced in compose but doesn't exist in the repo. The `setup.sh` creates a sample file, but the directory itself is not tracked. +- **Two osTicket implementations coexist**: `osticket_tool.py` (standalone) and `ticket_platforms/osticket.py` (adapter). The standalone version may be dead code. +- **Broad self-hosted vision**: The `compose/` directory contains 10 overlay stacks (STT, TTS, browser, Honcho, Gitea, Postal, Outline, MinIO, Uptime Kuma, SearXNG) that go well beyond helpdesk functionality. This suggests CommandDesk was envisioned as the **helpdesk component of a larger self-hosted enterprise platform**. From 6b046f752426d539b5106f0812c20d6a832ddd43 Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 21:33:04 -0400 Subject: [PATCH 11/13] docs(reports): add J1-PIPELINE audit, architecture, and security assessment reports --- reports/ARCHITECTURE_REPORT.md | 187 ++++++++++++++++++++++++++ reports/AUDIT_REPORT.md | 236 +++++++++++++++++++++++++++++++++ reports/SECURITY_REPORT.md | 165 +++++++++++++++++++++++ 3 files changed, 588 insertions(+) create mode 100644 reports/ARCHITECTURE_REPORT.md create mode 100644 reports/AUDIT_REPORT.md create mode 100644 reports/SECURITY_REPORT.md diff --git a/reports/ARCHITECTURE_REPORT.md b/reports/ARCHITECTURE_REPORT.md new file mode 100644 index 0000000..56fb523 --- /dev/null +++ b/reports/ARCHITECTURE_REPORT.md @@ -0,0 +1,187 @@ +# ARCHITECTURE_REPORT.md — J1-PIPELINE Phase 2 (ARCHITECT) + +**Repository:** `OneByJorah/CommandDesk` +**Analysis Date:** 2026-07-05 +**Analyst:** J1-PIPELINE ARCHITECT +**Status:** DEGRADED — 2 architectural concerns flagged + +--- + +## Architecture Overview + +CommandDesk uses a **microservices architecture** deployed via Docker Compose with 12+ containers. The architecture follows a layered pattern: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ External Channels │ +│ WhatsApp (webhook) · Email (IMAP) · Web (chat widget) │ +└──────────────────────────┬──────────────────────────────────┘ + │ +┌──────────────────────────▼──────────────────────────────────┐ +│ Nginx Reverse Proxy (Security Gateway) │ +│ Rate Limiting · Security Headers · IP Whitelisting · TLS │ +└────┬────────────┬──────────────┬──────────────┬─────────────┘ + │ │ │ │ +┌────▼───┐ ┌────▼───┐ ┌──────▼──────┐ ┌────▼──────────┐ +│Helpdesk│ │ Admin │ │ n8n │ │ SearXNG │ +│ Agent │ │ Agent │ │ Workflows │ │ Web Search │ +│ :8080 │ │ :8082 │ │ :5678 │ │ :8888 │ +└───┬────┘ └───┬────┘ └──────┬──────┘ └───────┬───────┘ + │ │ │ │ + └───────────┼──────────────┼──────────────────┘ + │ │ +┌───────────────▼──────────────▼────────────────────────────┐ +│ Core Services │ +│ ┌────────┐ ┌──────────┐ ┌────────┐ ┌──────────────┐ │ +│ │ llama │ │ ChromaDB │ │ Redis │ │ PostgreSQL │ │ +│ │ :8081 │ │ :8000 │ │ :6379 │ │ :5432 │ │ +│ └────────┘ └──────────┘ └────────┘ └──────────────┘ │ +└───────────────────────────────────────────────────────────┘ +``` + +--- + +## Architecture Score: 75/100 + +| Criterion | Score | Notes | +|-----------|-------|-------| +| Separation of Concerns | 85 | Good separation between agents, channels, and data | +| Service Boundaries | 80 | Well-defined service boundaries | +| Data Flow | 75 | Clear but some async patterns need work | +| Scalability | 60 | Redis-based sessions help, but agents are single-instance | +| Resilience | 65 | Health checks exist but no circuit breakers | +| Observability | 80 | Health monitor, analytics, audit logging | +| Security Architecture | 85 | Defense-in-depth: Nginx gateway, rate limiting, IP whitelist | +| **Overall** | **75** | **DEGRADED** | + +--- + +## ✅ Strengths + +### 1. Clean Adapter Pattern +The ticket platform adapter pattern (`ticket_platforms/`) is well-designed: +- Abstract base class (`Ticket` ABC) with 4 methods +- Decorator-based registry (`@register("name")`) +- Each adapter is self-contained (config, auth, API calls) +- Adding a new platform = write one class + register it + +### 2. Two-Agent Security Architecture +Separating the helpdesk agent (restricted, no ticket creation) from the admin agent (full access) is a strong security pattern. This is enforced at: +- Config level (different YAML files) +- Environment level (`ALLOW_CREATE_TICKET=false` vs `true`) +- MCP permissions level (different allowed/denied tool lists) +- Nginx level (admin routes IP-whitelisted to Docker subnet) + +### 3. Defense-in-Depth Security +- Nginx as security gateway (not just a reverse proxy) +- Rate limiting at 3 layers (Nginx zones, per-session, WhatsApp per-minute) +- Content filtering (PII patterns) +- Audit logging (all requests to PostgreSQL) +- Webhook signature verification (WhatsApp HMAC-SHA256) + +### 4. Multi-Channel Support +WhatsApp (webhook + intent detection), Email (IMAP polling), and Web (chat widget + PWA) — all feeding into the same agent backend. + +### 5. n8n Workflow Integration +Rather than building a custom workflow engine, the system delegates to n8n with 5 pre-built workflows covering the core automation needs. + +--- + +## 🔴 Architectural Concerns + +### A1. Single-Instance Agents (Scalability Risk) + +**Issue:** Both `helpdesk-agent` and `admin-agent` are single instances. The `agent_server.py` runs with `--workers 2` (2 Uvicorn workers), but the in-memory rate limiter (`RateLimiter` class) stores session state in a Python dict — not in Redis. This means: + +1. With multiple workers, each worker has its own in-memory rate limiter state +2. Rate limits are per-worker, not global +3. Session state is duplicated across workers + +**Impact:** Rate limiting is ineffective with multiple workers. A user could exhaust their rate limit on one worker and switch to another. + +**Recommendation:** Move rate limiter state to Redis (the `RateLimiter` class already has a `self.redis` field but doesn't use it for session storage — it uses `self._sessions` dict). The `SessionManager` class correctly uses Redis, but the `RateLimiter` does not. + +### A2. Missing Circuit Breakers and Retry Logic + +**Issue:** The system has no circuit breaker pattern for downstream dependencies: +- If `llama` (LLM) is down, the agent returns a generic error message +- If `chroma` (vector DB) is down, KB search fails silently +- If `postgres` is down, session persistence fails +- The `email_fetcher.py` has a basic retry loop but no exponential backoff + +**Impact:** Cascading failures are possible. A single service outage can degrade the entire system. + +**Recommendation:** Add circuit breakers (e.g., `pybreaker` or `aiobreaker`) for external service calls, with fallback behavior. + +### A3. In-Memory Rate Limiter Not Using Redis + +**Issue:** The `RateLimiter` class in `scripts/rate_limiter.py` accepts a `redis_client` parameter but only uses it for future persistence. All rate limit state is stored in `self._sessions: dict`. This means: +- Rate limits are lost on agent restart +- Rate limits don't work across multiple instances/workers +- No persistence of rate limit state + +**Recommendation:** Implement Redis-backed rate limiting in the `RateLimiter` class, using the existing `self.redis` field. + +--- + +## 🟡 Minor Concerns + +### M1. WhatsApp Webhook Exposes Port 8383 to 0.0.0.0 + +**File:** `docker-compose.yml:349` +**Code:** `"0.0.0.0:8383:8383"` +**Issue:** The WhatsApp webhook exposes port 8383 to all interfaces (`0.0.0.0`), while all other services bind to `127.0.0.1`. This is necessary for WhatsApp to reach the webhook, but it's a larger attack surface. + +**Note:** This is a necessary trade-off for webhook functionality. The webhook has signature verification (HMAC-SHA256) as a compensating control. + +### M2. No Health Endpoint for Email Fetcher + +**File:** `scripts/health_monitor.py:28` +**Code:** `"email-fetcher": None, # No health endpoint, check process` +**Issue:** The email fetcher has no health endpoint, so the health monitor cannot check its status. + +### M3. `docker-compose.yml` Uses Deprecated `version` Field + +**File:** `docker-compose.yml:1` +**Code:** `version: "3.9"` +**Issue:** The `version` field is deprecated in Docker Compose v2 and should be removed. + +--- + +## Data Flow Analysis + +### Request Flow (Helpdesk Agent) + +``` +User → WhatsApp/Email/Web → Nginx → helpdesk-agent:8080 + → Rate Limiter (in-memory, broken with workers) + → Session Manager (Redis) + → LLM (llama.cpp via HTTP) + → Response → Nginx → User +``` + +### Ticket Flow + +``` +External Ticketing System (osTicket/Freshdesk/Zammad) + ←→ ticket_platforms/adapter.py (REST API) + ←→ helpdesk-agent / admin-agent +``` + +### Knowledge Base Flow + +``` +knowledge-base/*.md → index_kb.py → ChromaDB (vector store) + → Agent queries ChromaDB for semantic search + → Returns relevant chunks +``` + +--- + +## Recommendations (Priority Order) + +1. **CRITICAL:** Fix `RateLimiter` to use Redis-backed storage instead of in-memory dict +2. **HIGH:** Add circuit breakers for downstream service calls +3. **MEDIUM:** Add health endpoint to email-fetcher +4. **LOW:** Remove deprecated `version` field from docker-compose.yml +5. **LOW:** Add exponential backoff to email fetcher retry loop diff --git a/reports/AUDIT_REPORT.md b/reports/AUDIT_REPORT.md new file mode 100644 index 0000000..e255433 --- /dev/null +++ b/reports/AUDIT_REPORT.md @@ -0,0 +1,236 @@ +# AUDIT_REPORT.md — J1-PIPELINE Phase 1 (AUDITOR) + +**Repository:** `OneByJorah/CommandDesk` +**Audit Date:** 2026-07-05 +**Analyst:** J1-PIPELINE AUDITOR +**Status:** CRITICAL — 4 critical, 8 degraded findings + +--- + +## Summary + +| Category | Score | Status | +|----------|-------|--------| +| Lint / Syntax | 60/100 | CRITICAL | +| Dead Code | 70/100 | DEGRADED | +| Dependencies | 85/100 | OK | +| Secrets | 90/100 | OK | +| README Compliance | 75/100 | DEGRADED | +| Tests | 20/100 | CRITICAL | +| Docker | 70/100 | DEGRADED | +| Folder Structure | 85/100 | OK | +| **Overall** | **69/100** | **CRITICAL** | + +--- + +## 🔴 CRITICAL Findings + +### C1. Syntax Error in `rate_limiter.py` (Line 39) + +**File:** `scripts/rate_limiter.py:39` +**Code:** `self._sessions: dict[str, SessionState] =()` +**Issue:** Empty tuple `()` used instead of empty dict `{}`. This is a valid Python syntax (tuple assignment) but the type annotation says `dict` — at runtime, `self._sessions` will be a tuple, and any method call like `self._sessions.get(session_id)` will raise `AttributeError: 'tuple' object has no attribute 'get'`. + +**Impact:** The rate limiter will crash on the first request. This is a **runtime error** that would take down the helpdesk agent immediately. + +**Fix:** Change `=()` to `={}`. + +### C2. Wrong Redis URL Default in `health_monitor.py` (Line 20) + +**File:** `scripts/health_monitor.py:20` +**Code:** `REDIS_URL = os.getenv("REDIS_URL", "redis://redis:***@postgres:5432/helpdesk")` +**Issue:** The default Redis URL references `postgres:5432` (PostgreSQL port) instead of `redis:6379`. This is a copy-paste error from the PostgreSQL URL pattern. + +**Impact:** If `REDIS_URL` env var is not set, the health monitor will try to connect to PostgreSQL as if it were Redis, which will fail. + +**Fix:** Change default to `redis://redis:6379/0`. + +### C3. Wrong Redis URL Default in `whatsapp_webhook.py` (Line 29) + +**File:** `scripts/whatsapp_webhook.py:29` +**Code:** `REDIS_URL = os.getenv("REDIS_URL", "redis://redis:***@postgres:5432/helpdesk")` +**Issue:** Same copy-paste error as C2 — references `postgres:5432` instead of `redis:6379`. + +**Impact:** Same as C2 — WhatsApp webhook will fail to connect to Redis if env var is not set. + +**Fix:** Change default to `redis://redis:6379/0`. + +### C4. Missing Import in `zammad.py` + +**File:** `ticket_platforms/zammad.py` +**Issue:** The file uses `requests.post()`, `requests.get()`, `requests.patch()` but never imports the `requests` module. This will cause a `NameError` at runtime. + +**Impact:** Any operation using the Zammad adapter will crash with `NameError: name 'requests' is not defined`. + +**Fix:** Add `import requests` at the top of the file. + +--- + +## 🟡 DEGRADED Findings + +### D1. No Test Files + +**Issue:** The repository has no `tests/` directory and no test files anywhere. The CI pipeline has a basic smoke test (PostgreSQL + Redis connectivity check) but no unit tests, integration tests, or test framework configured. + +**Impact:** No regression safety. Changes cannot be validated automatically. + +**Recommendation:** Add pytest-based unit tests for the ticket platform adapters, rate limiter, session manager, and API endpoints. + +### D2. Dependabot Ecosystem Mismatch (npm) + +**File:** `.github/dependabot.yml` +**Issue:** Dependabot is configured for `npm` ecosystem, but no `package.json` exists in the repository. This is a template vestige from the J1 repo template. + +**Impact:** Dependabot will fail silently or produce errors for the npm ecosystem check. + +**Fix:** Remove the `npm` ecosystem entry from `dependabot.yml`. + +### D3. `knowledge-base/` Directory Missing + +**Issue:** The `knowledge-base/` directory is referenced in `docker-compose.yml` as a volume mount (`./knowledge-base:/app/knowledge-base:ro`) but does not exist in the repository. The `setup.sh` script creates a sample `welcome.md` file, but the directory itself is not tracked in git. + +**Impact:** `docker compose up` will create an empty directory on first run, but the knowledge base indexer will fail if the directory doesn't exist. + +### D4. Dead Code: `osticket_tool.py` + +**File:** `osticket_tool.py` +**Issue:** This is a standalone osTicket wrapper that predates the adapter pattern. The adapter-based `ticket_platforms/osticket.py` supersedes it. The standalone version is not used by any Docker service or config. + +**Impact:** Code bloat. Maintenance burden. + +**Recommendation:** Remove `osticket_tool.py` or mark as deprecated. + +### D5. Incomplete Adapter: `ticket_platforms/email.py` + +**File:** `ticket_platforms/email.py` +**Issue:** The email adapter is only 24 lines and does not implement the `Ticket` ABC. It has a stub `__init__` and no methods. It is registered in `__init__.py` but cannot be used. + +**Impact:** If someone tries to use the email adapter, it will fail at runtime. + +### D6. README Architecture Diagram Inaccuracies + +**File:** `README.md` +**Issue:** The architecture diagram references "Ollama" and "Qdrant" as AI Engine and Knowledge Base options, but the actual stack uses **llama.cpp** (not Ollama) and **ChromaDB** (not Qdrant). The diagram is aspirational rather than accurate. + +**Impact:** Misleading documentation for new users. + +### D7. Unpinned Docker Image Tags + +**Files:** `docker-compose.yml` +**Issue:** Two services use `:latest` tags: +- `searxng/searxng:latest` +- `n8nio/n8n:latest` + +**Impact:** Non-reproducible builds. A `latest` tag update could break the stack without warning. + +**Recommendation:** Pin to specific versions. + +### D8. No `.dockerignore` File + +**Issue:** No `.dockerignore` exists. The Docker build context includes the entire repo, including `.git/`, `models/`, `certs/`, and other large/unnecessary files. + +**Impact:** Larger build context, slower Docker builds. + +--- + +## ✅ PASS Findings + +### P1. Python Syntax +All 14 Python files compile cleanly with `py_compile` and `ast.parse`. No syntax errors (except the runtime type error in C1). + +### P2. CI Pipeline +Comprehensive CI pipeline with: +- Docker Compose config validation +- Hadolint Dockerfile linting +- Flake8 Python linting +- YAML syntax checking +- Docker build +- Smoke test (PostgreSQL + Redis connectivity) + +### P3. CodeQL Security Scanning +CodeQL configured for Python, JavaScript, and TypeScript analysis on push and weekly schedule. + +### P4. Security Audit History +Two security audit commits in git history: +- `5c7db2d` — "audit(CommandDesk): sanitize email references" +- `68216b0` — "security: redact exposed tailscale IPs and demo emails" + +### P5. Community Files +All standard community files present: +- `LICENSE` (MIT) +- `CODE_OF_CONDUCT.md` (Contributor Covenant v2.1) +- `CONTRIBUTING.md` +- `SECURITY.md` (90-day disclosure policy) +- GitHub issue templates (bug report + feature request) +- Pull request template + +### P6. `.gitignore` +Comprehensive `.gitignore` covering secrets, models, data, IDE files, OS files, and logs. + +### P7. Environment Configuration +`.env.example` with 30+ documented environment variables covering all services. + +### P8. Makefile +Well-organized Makefile with 20+ targets for setup, start, stop, logs, maintenance, and development. + +--- + +## Dependency Review + +| Package | Version | Notes | +|---------|---------|-------| +| fastapi | 0.115.6 | Latest stable | +| uvicorn | 0.34.0 | Latest stable | +| pydantic | 2.10.3 | Latest stable | +| PyYAML | 6.0.2 | Latest stable | +| redis | 5.2.1 | Latest stable | +| asyncpg | 0.30.0 | Latest stable | +| psycopg2-binary | 2.9.10 | Latest stable | +| httpx | 0.28.1 | Latest stable | +| python-multipart | 0.0.31 | Latest stable | +| openai | 1.59.0 | Latest stable | +| chromadb | 0.5.23 | Pinned — newer 1.5.x available | +| imaplib2 | 3.6 | Latest stable | +| python-dotenv | 1.2.2 | Latest stable | +| uuid6 | 2024.7.10 | Latest stable | + +All dependencies are reasonably up-to-date. No known CVEs in the pinned versions. + +--- + +## Folder Structure + +``` +CommandDesk/ +├── admin/ ✅ Admin dashboard (1 file) +├── compose/ ✅ Docker Compose overlays (12 files) +├── config/ ✅ Application config (8 files) +├── reports/ ✅ Pipeline reports (this file) +├── scripts/ ✅ Utility scripts (10 files) +├── skills/ ✅ AI agent skills (2 files) +├── ticket_platforms/ ✅ Adapter library (6 files) +├── tools-ui/ ✅ Web UI components (5 files) +├── workflows/ ✅ n8n workflows (5 files) +├── .github/ ✅ CI/CD + templates +├── Root files ✅ 15 files (compose, Dockerfiles, configs, docs) +``` + +No empty directories. Structure is clean and well-organized. + +--- + +## Scoring + +| Category | Weight | Score | Weighted | +|----------|--------|-------|----------| +| Lint / Syntax | 10% | 60 | 6.0 | +| Dead Code | 10% | 70 | 7.0 | +| Dependencies | 10% | 85 | 8.5 | +| Secrets | 10% | 90 | 9.0 | +| README Compliance | 15% | 75 | 11.25 | +| Tests | 15% | 20 | 3.0 | +| Docker | 10% | 70 | 7.0 | +| Folder Structure | 10% | 85 | 8.5 | +| **Total** | **100%** | | **60.25** | + +**Status: CRITICAL** (below 70) diff --git a/reports/SECURITY_REPORT.md b/reports/SECURITY_REPORT.md new file mode 100644 index 0000000..5ad337c --- /dev/null +++ b/reports/SECURITY_REPORT.md @@ -0,0 +1,165 @@ +# SECURITY_REPORT.md — J1-PIPELINE Phase 3 (GUARDIAN) + +**Repository:** `OneByJorah/CommandDesk` +**Analysis Date:** 2026-07-05 +**Analyst:** J1-PIPELINE GUARDIAN +**Status:** DEGRADED — 2 critical, 3 degraded findings + +--- + +## Security Score: 72/100 + +| Category | Score | Status | +|----------|-------|--------| +| Authentication | 80 | OK | +| Authorization | 85 | OK | +| HTTPS/TLS | 60 | DEGRADED | +| CSP/Headers | 85 | OK | +| Docker Hardening | 65 | DEGRADED | +| Secrets Management | 80 | OK | +| Rate Limiting | 70 | DEGRADED | +| Input Validation | 75 | OK | +| Audit Logging | 85 | OK | +| Supply Chain | 70 | DEGRADED | +| **Overall** | **72** | **DEGRADED** | + +--- + +## 🔴 CRITICAL Security Findings + +### S1. Rate Limiter Runtime Crash (C1 from AUDITOR) + +**File:** `scripts/rate_limiter.py:39` +**Issue:** `self._sessions: dict[str, SessionState] =()` — empty tuple instead of empty dict. The rate limiter will crash on the first request with `AttributeError: 'tuple' object has no attribute 'get'`. + +**Security Impact:** If the rate limiter crashes, the agent server returns a 500 error for all requests. This is a denial-of-service vulnerability — any request triggers the crash, taking down the entire helpdesk agent. + +**Severity:** CRITICAL +**Fix:** Change `=()` to `={}`. + +### S2. Missing `import requests` in Zammad Adapter (C4 from AUDITOR) + +**File:** `ticket_platforms/zammad.py` +**Issue:** The Zammad adapter uses `requests.post()`, `requests.get()`, `requests.patch()` but never imports `requests`. Any ticket operation on Zammad will crash with `NameError`. + +**Security Impact:** If Zammad is configured as the ticketing backend, all ticket operations fail silently or crash the agent. This is a reliability issue that could prevent legitimate support requests from being processed. + +**Severity:** CRITICAL +**Fix:** Add `import requests` at the top of the file. + +--- + +## 🟡 DEGRADED Security Findings + +### S3. No HTTPS in Default Configuration + +**Issue:** The Nginx config (`config/nginx.conf`) only listens on port 80 (HTTP). While the `setup.sh` script generates self-signed certificates, the Nginx config does not include an HTTPS server block. The `docker-compose.yml` maps ports 80 and 443, but only port 80 is configured. + +**Impact:** All traffic between users and the helpdesk is unencrypted by default. Credentials, ticket data, and PII are transmitted in plaintext. + +**Recommendation:** Add an HTTPS server block to `nginx.conf` that redirects HTTP to HTTPS and serves the self-signed certs from `certs/`. + +### S4. Containers Run as Root + +**Issue:** None of the Dockerfiles or docker-compose services specify a non-root user. All containers run as root by default. + +**Impact:** If any container is compromised, the attacker has root access within that container. This increases the blast radius of a container breakout. + +**Recommendation:** Add `USER` directives to Dockerfiles and `user:` directives to docker-compose services. + +### S5. Unpinned Base Images + +**Issue:** The Dockerfiles use `python:3.11-slim` without a specific patch version (e.g., `python:3.11.11-slim`). The `docker-compose.yml` uses `postgres:16-alpine` and `redis:7-alpine` without specific patch versions. + +**Impact:** Base image updates could introduce breaking changes or security vulnerabilities without warning. + +**Recommendation:** Pin all base images to specific SHA256 digests or at minimum specific patch versions. + +### S6. ChromaDB Auth Token Default + +**File:** `docker-compose.yml:143` +**Code:** `CHROMA_AUTH_TOKEN=${CHROMA_AUTH_TOKEN:-chromadb_token_change_me}` +**Issue:** The default ChromaDB auth token is `chromadb_token_change_me`, which is a weak default. If the user doesn't set `CHROMA_AUTH_TOKEN` in `.env`, the token is trivially guessable. + +**Impact:** Anyone with network access to ChromaDB (port 8000) can query or modify the knowledge base. + +**Recommendation:** Generate a random default token in `setup.sh` instead of using a hardcoded string. + +### S7. n8n JWT Secret Default + +**File:** `docker-compose.yml:202` +**Code:** `N8N_USER_MANAGEMENT_JWT_SECRET=${JWT_SECRET:-jwt_secret_change_me_please}` +**Issue:** Same pattern as S6 — hardcoded weak default. + +**Impact:** If the JWT secret is not changed, an attacker could forge n8n authentication tokens. + +--- + +## ✅ Security Strengths + +### P1. Defense-in-Depth Architecture +- Nginx as security gateway (all external traffic goes through it) +- Rate limiting at 3 layers (Nginx zones, per-session, WhatsApp per-minute) +- IP whitelisting for admin routes (Docker subnet only) +- Security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy) + +### P2. Content Filtering +PII detection and blocking for: passwords, credit card numbers, SSNs. + +### P3. Audit Logging +All requests logged to PostgreSQL with session_id, user_id, action, details (JSONB), IP address, and timestamp. + +### P4. Webhook Signature Verification +WhatsApp webhook uses HMAC-SHA256 signature verification (`X-Hub-Signature-256` header). + +### P5. Secrets Management +- `.env` never committed (in `.gitignore`) +- Secrets directory (`.secrets/`) gitignored +- API keys passed via environment variables, not hardcoded +- Admin agent requires API key authentication + +### P6. Security Audit History +Two dedicated security audit commits in git history: +- `5c7db2d` — sanitize email references +- `68216b0` — redact exposed tailscale IPs and demo emails + +### P7. CodeQL Scanning +CodeQL configured for Python, JavaScript, and TypeScript analysis on every push and weekly. + +### P8. Request Size Limits +Nginx configured with `client_max_body_size 4k` to prevent large payload attacks. + +### P9. Service Isolation +Each service runs in its own container with Docker networks isolating them from the host. + +--- + +## Security Checklist + +| Control | Status | Notes | +|---------|--------|-------| +| Authentication | ✅ | API keys for admin, webhook signature verification | +| Authorization | ✅ | Two-agent architecture, MCP permissions | +| HTTPS | ❌ | No HTTPS server block in Nginx config | +| CSP Headers | ✅ | CSP, HSTS, X-Frame-Options, X-Content-Type-Options | +| Docker Hardening | ❌ | Containers run as root, no `.dockerignore` | +| Secrets Management | ✅ | `.env` gitignored, env vars for secrets | +| Rate Limiting | ⚠️ | Broken in-memory rate limiter (C1) | +| Input Validation | ✅ | Content filtering, request size limits | +| Audit Logging | ✅ | All requests logged to PostgreSQL | +| Supply Chain | ⚠️ | Dependabot configured but npm ecosystem mismatch | +| SBOM | ❌ | No SBOM generation | +| AppArmor/SELinux | ❌ | Not configured | +| Container Non-Root | ❌ | All containers run as root | + +--- + +## Recommendations (Priority Order) + +1. **CRITICAL:** Fix rate limiter tuple→dict bug (S1) +2. **CRITICAL:** Add `import requests` to Zammad adapter (S2) +3. **HIGH:** Add HTTPS server block to Nginx config (S3) +4. **HIGH:** Add non-root users to Dockerfiles (S4) +5. **MEDIUM:** Pin base images to specific versions (S5) +6. **MEDIUM:** Generate random ChromaDB auth token in setup.sh (S6) +7. **LOW:** Generate random n8n JWT secret in setup.sh (S7) From 92c56d903ba457859eb4322fd6a18d51c13a37d5 Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Sun, 5 Jul 2026 21:33:08 -0400 Subject: [PATCH 12/13] chore(meta): add j1.yaml project metadata file --- j1.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 j1.yaml diff --git a/j1.yaml b/j1.yaml new file mode 100644 index 0000000..d4ff072 --- /dev/null +++ b/j1.yaml @@ -0,0 +1,15 @@ +repo: CommandDesk +class: AI, LLM, Agent, Docker, Python, Web, Security, Automation, Monitoring +org: OneByJorah +owner: Jhonattan L. Jimenez +license: MIT +production_score: 0 +last_audit: 2026-07-05T00:00:00Z +last_publish: null +standards_version: "2.1" +dependencies: [] +deploy_target: production +tailscale_only: false +public_facing: false +community_sla_hours: 48 +adoption_tracked: false From 53eb969e998d46b14403b726cbfef250bd2eb870 Mon Sep 17 00:00:00 2001 From: J1-PIPELINE Date: Fri, 10 Jul 2026 10:53:25 -0400 Subject: [PATCH 13/13] feat: add docker-compose.deploy.yml for production deployment --- docker-compose.deploy.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docker-compose.deploy.yml diff --git a/docker-compose.deploy.yml b/docker-compose.deploy.yml new file mode 100644 index 0000000..0cf50db --- /dev/null +++ b/docker-compose.deploy.yml @@ -0,0 +1,28 @@ +# CommandDesk - Complex deployment +# Note: This requires LLM models which are too heavy for current deployment +# Creating minimal deployment for core services only + +services: + n8n: + image: n8nio/n8n:1.80.0 + container_name: commanddesk-n8n + ports: + - "8083:5678" + volumes: + - n8n-data:/home/node/.n8n + environment: + - N8N_HOST=localhost + - N8N_PORT=5678 + - N8N_PROTOCOL=http + - WEBHOOK_URL=http://localhost:8083/ + - GENERIC_TIMEZONE=UTC + networks: + - shared-infrastructure_default + restart: unless-stopped + +networks: + shared-infrastructure_default: + external: true + +volumes: + n8n-data: