# PentAGI Documentation

> Self-hosted autonomous penetration testing platform: Docker deployment, multi-agent flows, BYOK LLM providers, REST and GraphQL APIs, sandbox tools, and optional observability stacks for security engineers and operators.

## Context Links

- [Agent index](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5)
- [GitHub repository](https://github.com/vxcontrol/pentagi)

## Repository Metadata

- Repository: vxcontrol/pentagi

- Generated: 2026-07-10T07:13:57.068Z
- Updated: 2026-07-10T07:37:54.306Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 26

## Page Index

- 01. [Overview](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/01-overview.md) - What PentAGI exposes: autonomous multi-agent pentests in Docker, BYOK LLM providers, web UI, REST and GraphQL under /api/v1, and the shortest path from deploy to first flow.
- 02. [Installation](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/02-installation.md) - Prerequisites, Docker Compose core stack, .env from .env.example, SSL and data volumes, and compose overlays for observability, Langfuse, and Graphiti.
- 03. [Quickstart](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/03-quickstart.md) - First successful run: set at least one LLM key, docker compose up, open https://localhost:8443, change admin@pentagi.com defaults, create a flow, and verify provider and UI health.
- 04. [Interactive installer](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/04-interactive-installer.md) - TUI installer wizard: system checks, .env generation, LLM and search setup, SSL hardening, compose deploy, and admin password reset maintenance paths.
- 05. [Flows, tasks, and subtasks](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/05-flows-tasks-and-subtasks.md) - Execution hierarchy: Flow lifecycle and StatusType states, Task objectives, Generator and Refiner subtask plans, Assistant mode, and putUserInput, stopFlow, finishFlow boundaries.
- 06. [Agents and supervision](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/06-agents-and-supervision.md) - Specialist agents (primary, pentester, coder, installer, searcher, memorist, adviser, reflector, reporter), execution monitor, planning step, and tool-call hard limits.
- 07. [Tools and sandbox execution](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/07-tools-and-sandbox-execution.md) - Tool categories, Docker-isolated terminal and file tools, network search tools, barrier tools done and ask, timeouts, and default images for general vs pentest work.
- 08. [Memory and knowledge](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/08-memory-and-knowledge.md) - pgvector memory tools, embeddings config, chain summarizer budgets, user knowledge documents API, flow files and resources under /work, and optional Graphiti graph search.
- 09. [Configure LLM providers](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/09-configure-llm-providers.md) - Wire OpenAI, Anthropic, Gemini, Bedrock, DeepSeek, GLM, Kimi, Qwen, and Ollama via env keys and server URLs; UI provider profiles for per-agent models; test providers before flows.
- 10. [Local and custom providers](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/10-local-and-custom-providers.md) - OpenAI-compatible custom endpoints (LLM_SERVER_*), Ollama local or cloud, config path YAML, legacy and preserve reasoning flags, and aggregator endpoints such as OpenRouter and DeepInfra.
- 11. [Search engines](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/11-search-engines.md) - Enable and configure DuckDuckGo, Google CSE, Tavily, Traversaal, Perplexity, Sploitus, and Searxng; scraper URLs; proxy and timeout constraints that gate network search tools.
- 12. [Authentication and API tokens](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/12-authentication-and-api-tokens.md) - Local session login, OAuth Google and GitHub, default admin account, password change, Bearer API tokens for REST and GraphQL, and permission-scoped token lifecycle.
- 13. [Observability and Langfuse](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/13-observability-and-langfuse.md) - Optional OpenTelemetry path to Grafana, VictoriaMetrics, Jaeger, and Loki; Langfuse LLM analytics compose stack; OTEL_HOST and LANGFUSE_* keys; what each stack measures.
- 14. [Knowledge graph](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/14-knowledge-graph.md) - Enable Graphiti with Neo4j via docker-compose-graphiti.yml, GRAPHITI_* settings, graphiti_search tool behavior, and failure modes when the graph backend is down.
- 15. [Docker sandbox and worker nodes](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/15-docker-sandbox-and-worker-nodes.md) - Docker socket and network options, DOCKER_INSIDE and NET_ADMIN, default and pentest images, custom OpenVAS image, and multi-host worker_node deployment constraints.
- 16. [Environment variables](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/16-environment-variables.md) - Authoritative Config struct and .env.example: core, Docker, server, providers, embeddings, summarizer, search, OAuth, proxy, supervision, Graphiti, Langfuse, and DB pool keys with defaults.
- 17. [REST API](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/17-rest-api.md) - Gin routes under /api/v1: auth, flows, tasks, subtasks, files, resources, containers, toolcalls, assistants, logs, knowledge, providers, settings, users, tokens; Swagger at /api/v1/swagger.
- 18. [GraphQL API](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/18-graphql-api.md) - schema.graphqls surface: Query, Mutation, and Subscription operations for flows, assistants, providers, prompts, API tokens, knowledge, usage stats, and real-time log streams over WebSocket.
- 19. [Provider configuration schema](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/19-provider-configuration-schema.md) - Per-agent YAML and AgentsConfig fields: model, temperature, top_p, top_k, max_tokens, json mode, reasoning, price, extra_body; built-in provider config.yml baselines and UI testAgent/testProvider.
- 20. [Tools reference](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/20-tools-reference.md) - Named tool registry: terminal, file, browser, search engines, memory store/search, agent delegation tools, result tools, barrier tools, and assistant flow-control tools with argument shapes.
- 21. [Prompts and templates](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/21-prompts-and-templates.md) - PromptType enum, embedded .tmpl templates, validation via validatePrompt, user prompt CRUD, and how agent templates bind to execution context variables.
- 22. [Example provider configs](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/22-example-provider-configs.md) - Copy-paste YAML from examples/configs for vLLM, Ollama, OpenRouter, DeepInfra, Azure, and cloud-compatible endpoints mapped to agent config keys.
- 23. [Sample pentest prompts](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/23-sample-pentest-prompts.md) - Ready flow inputs from examples/prompts: base web pentest checklist and scope-of-work style engagement prompts with expected report-oriented outcomes.
- 24. [Deploy with vLLM and Qwen](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/24-deploy-with-vllm-and-qwen.md) - Air-gapped style local inference: hardware matrix, vLLM serve flags, LLM_SERVER_* wiring, thinking vs non-thinking provider YAML, and supervision flags recommended for sub-32B models.
- 25. [Development and testing](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/25-development-and-testing.md) - Backend go build and test, frontend pnpm scripts, gqlgen and graphql:generate, ctester container tests, etester embeddings, ftester tool calling, and local compose for contributors.
- 26. [Contributing](https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/26-contributing.md) - License-compatible dependency policy, generate-licenses.sh, PR expectations, and contributor workflow boundaries for backend and frontend changes.

## Source File Index

- `.env.example`
- `.github/PULL_REQUEST_TEMPLATE.md`
- `backend/cmd/ctester/main.go`
- `backend/cmd/etester/main.go`
- `backend/cmd/ftester/main.go`
- `backend/cmd/installer/checker`
- `backend/cmd/installer/hardening`
- `backend/cmd/installer/main.go`
- `backend/cmd/installer/processor`
- `backend/cmd/installer/wizard`
- `backend/cmd/pentagi/main.go`
- `backend/docs/config.md`
- `backend/docs/docker.md`
- `backend/docs/flow_execution.md`
- `backend/docs/installer.md`
- `backend/docs/langfuse.md`
- `backend/docs/observability.md`
- `backend/docs/ollama.md`
- `backend/docs/prompt_engineering_pentagi.md`
- `backend/go.mod`
- `backend/gqlgen/gqlgen.yml`
- `backend/pkg/config/config_test.go`
- `backend/pkg/config/config.go`
- `backend/pkg/controller/assistant.go`
- `backend/pkg/controller/flow.go`
- `backend/pkg/controller/prompter.go`
- `backend/pkg/controller/subtask.go`
- `backend/pkg/controller/task.go`
- `backend/pkg/csum/chain_summary.go`
- `backend/pkg/docker/client.go`
- `backend/pkg/flowfiles/files.go`
- `backend/pkg/graph/schema.graphqls`
- `backend/pkg/graph/schema.resolvers.go`
- `backend/pkg/graph/subscriptions/controller.go`
- `backend/pkg/graphiti/client.go`
- `backend/pkg/providers/anthropic/config.yml`
- `backend/pkg/providers/custom/custom.go`
- `backend/pkg/providers/embeddings/embedder.go`
- `backend/pkg/providers/ollama/config.yml`
- `backend/pkg/providers/ollama/ollama.go`
- `backend/pkg/providers/openai/config.yml`
- `backend/pkg/providers/pconfig/config.go`
- `backend/pkg/providers/performer.go`
- `backend/pkg/providers/performers.go`
- `backend/pkg/providers/provider/agents.go`
- `backend/pkg/providers/provider/provider.go`
- `backend/pkg/providers/providers.go`
- `backend/pkg/resources/resources.go`
- `backend/pkg/server/auth/api_token_jwt.go`
- `backend/pkg/server/auth/auth_middleware.go`
- `backend/pkg/server/docs/swagger.yaml`
- `backend/pkg/server/models/providers.go`
- `backend/pkg/server/router.go`
- `backend/pkg/server/services/api_tokens.go`
- `backend/pkg/server/services/auth.go`
- `backend/pkg/server/services/flows.go`
- `backend/pkg/server/services/graphql.go`
- `backend/pkg/server/services/knowledge.go`
- `backend/pkg/server/services/prompts.go`
- `backend/pkg/server/services/providers.go`
- `backend/pkg/templates/prompts`
- `backend/pkg/templates/templates.go`
- `backend/pkg/tools/args.go`
- `backend/pkg/tools/duckduckgo.go`
- `backend/pkg/tools/executor.go`
- `backend/pkg/tools/flow_manager.go`
- `backend/pkg/tools/google.go`
- `backend/pkg/tools/graphiti_search.go`
- `backend/pkg/tools/memory.go`
- `backend/pkg/tools/perplexity.go`
- `backend/pkg/tools/registry_test.go`
- `backend/pkg/tools/registry.go`
- `backend/pkg/tools/searxng.go`
- `backend/pkg/tools/sploitus.go`
- `backend/pkg/tools/tavily.go`
- `backend/pkg/tools/terminal.go`
- `backend/pkg/tools/tools.go`
- `CLAUDE.md`
- `CONTRIBUTING.md`
- `CONTRIBUTORS.md`
- `docker-compose-graphiti.yml`
- `docker-compose-langfuse.yml`
- `docker-compose-observability.yml`
- `docker-compose.yml`
- `Dockerfile`
- `examples/configs/azure-openai.provider.yml`
- `examples/configs/custom-openai.provider.yml`
- `examples/configs/deepinfra.provider.yml`
- `examples/configs/ollama-cloud.provider.yml`
- `examples/configs/ollama-qwen332b-fp16-tc.provider.yml`
- `examples/configs/openrouter.provider.yml`
- `examples/configs/vllm-qwen3.5-27b-fp8-no-think.provider.yml`
- `examples/configs/vllm-qwen3.5-27b-fp8.provider.yml`
- `examples/configs/vllm-qwen3.6-27b-fp8.provider.yml`
- `examples/guides/openvas-custom-image.md`
- `examples/guides/vllm-qwen35-27b-fp8.md`
- `examples/guides/worker_node.md`
- `examples/prompts/base_web_pentest.md`
- `examples/prompts/scope_of_work_pentest.md`
- `examples/reports/ollama_qwen3_32b_fp16_base_web_pentest.md`
- `examples/reports/openai_base_web_pentest.md`
- `examples/tests/openai-report.md`
- `frontend/graphql-schema.graphql`
- `frontend/package.json`
- `frontend/src/app.tsx`
- `frontend/src/lib`
- `frontend/src/pages/flows/new-flow.tsx`
- `frontend/src/pages/login.tsx`
- `frontend/src/pages/settings/settings-api-tokens.tsx`
- `frontend/src/pages/settings/settings-prompts.tsx`
- `frontend/src/pages/settings/settings-providers.tsx`
- `LICENSE`
- `licenses/README.md`
- `observability/otel/config.yml`
- `README.md`
- `scripts/generate-licenses.sh`

---

## 01. Overview

> What PentAGI exposes: autonomous multi-agent pentests in Docker, BYOK LLM providers, web UI, REST and GraphQL under /api/v1, and the shortest path from deploy to first flow.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/01-overview.md
- Generated: 2026-07-10T07:05:26.585Z

### Source Files

- `README.md`
- `docker-compose.yml`
- `backend/cmd/pentagi/main.go`
- `backend/pkg/server/router.go`
- `backend/pkg/graph/schema.graphqls`
- `frontend/src/app.tsx`
- `CLAUDE.md`

---
title: "Overview"
description: "What PentAGI exposes: autonomous multi-agent pentests in Docker, BYOK LLM providers, web UI, REST and GraphQL under /api/v1, and the shortest path from deploy to first flow."
---

PentAGI is a self-hosted, BYOK autonomous penetration testing platform. A Go API process (`backend/cmd/pentagi`) loads config from the environment, migrates PostgreSQL + pgvector, attaches to the Docker runtime, restores active flows, and serves HTTPS (default port `8443`) with a React SPA plus REST and GraphQL under the single base path `/api/v1`. Users and automation create **flows**; specialist LLM agents decompose work into **tasks** and **subtasks**, execute tools inside Docker-isolated containers, and persist results, logs, and vector memory.

<Info>
PentAGI is an autonomous and assistant-guided pentest runner, not a CALDERA-style BAS product with predefined adversary campaigns. Provider choice is yours: cloud keys, Ollama, or OpenAI-compatible custom endpoints (`LLM_SERVER_*`).
</Info>

## What you get

| Surface | Default location | Role |
|---|---|---|
| Web UI | `https://localhost:8443` | Flows, dashboard, settings, knowledge, resources, templates |
| REST + GraphQL | `/api/v1/*` | Auth, flows, logs, knowledge, providers, tokens, analytics |
| Swagger UI | `/api/v1/swagger/*` | OpenAPI exploration |
| GraphQL playground | `/api/v1/graphql/playground` | Schema exploration (auth still required for operations) |
| GraphQL endpoint | `Any /api/v1/graphql` | Queries, mutations, WebSocket subscriptions |
| Core compose stack | `docker-compose.yml` | `pentagi`, `pgvector`, `pgexporter`, `scraper` |
| Optional overlays | `docker-compose-observability.yml`, `docker-compose-langfuse.yml`, `docker-compose-graphiti.yml` | OTEL/Grafana stack, LLM analytics, Neo4j knowledge graph |
| TUI installer | `backend/cmd/installer` | Guided `.env`, SSL, deploy, admin password reset |

Default listen bind is `127.0.0.1:8443` via `PENTAGI_LISTEN_IP` / `PENTAGI_LISTEN_PORT`. SSL is on by default (`SERVER_USE_SSL=true`) when certificate paths are present.

## Runtime architecture

```mermaid
flowchart TB
  subgraph clients [Clients]
    UI[React SPA frontend/src]
    REST[REST clients Bearer or session]
    GQL[GraphQL clients + WS]
  end

  subgraph api [API process cmd/pentagi]
    Router[Gin router /api/v1]
    Auth[Session cookie / OAuth / API tokens]
    FC[FlowController]
    PC[ProviderController]
    Subs[SubscriptionsController]
  end

  subgraph workers [Flow workers]
    FW[FlowWorker]
    TW[TaskWorker]
    SW[SubtaskWorker]
    AW[AssistantWorker]
    Agents[Specialist agents]
  end

  subgraph sandbox [Docker sandbox]
    Term[terminal / file tools]
    Img[Default and pentest images]
  end

  subgraph data [Data plane]
    PG[(PostgreSQL + pgvector)]
    Scrap[scraper browser]
    Graphiti[Graphiti + Neo4j optional]
  end

  subgraph external [External BYOK]
    LLM[LLM providers]
    Search[Search engines]
  end

  UI --> Router
  REST --> Router
  GQL --> Router
  Router --> Auth
  Auth --> FC
  Auth --> PC
  FC --> FW
  FW --> TW
  TW --> SW
  FW --> AW
  SW --> Agents
  Agents --> Term
  Agents --> LLM
  Agents --> Search
  Agents --> Scrap
  Agents --> PG
  Agents --> Graphiti
  FC --> Subs
  Subs --> GQL
```

Startup sequence in `backend/cmd/pentagi/main.go`:

1. Load `config.Config` from environment.
2. Optionally attach Langfuse and OpenTelemetry clients (fail only if misconfigured, not if absent).
3. Open PostgreSQL (`DATABASE_URL`), share one pool with sqlc + GORM, create a pgxpool for vector ops.
4. Run goose migrations from embedded `backend/migrations/sql/`.
5. Initialize Docker client, LLM provider controller, subscription hub, and flow controller.
6. `LoadFlows` restores in-flight work after restart.
7. Bind `SERVER_HOST`:`SERVER_PORT` with TLS or plain HTTP.

## Execution model

Hierarchy used across REST, GraphQL, and the UI:

| Unit | Meaning |
|---|---|
| **Flow** | Top-level pentest session (persistent) |
| **Task** | User objective inside a flow |
| **Subtask** | System-generated sequential step (Generator / Refiner) |
| **Assistant** | Interactive chat mode on a flow; optional agent delegation |

Lifecycle states (`StatusType`):

| State | Meaning |
|---|---|
| `created` | Record exists, not running |
| `running` | Agents executing |
| `waiting` | Blocked on user input (`ask` barrier or `putUserInput`) |
| `finished` | Completed successfully |
| `failed` | Unrecoverable error |

Primary GraphQL control surface:

| Operation | Behavior |
|---|---|
| `createFlow(modelProvider, input, resourceIds?)` | Start autonomous work |
| `putUserInput(flowId, input, …)` | Answer `waiting` or add follow-up |
| `stopFlow(flowId)` | Stop running work |
| `finishFlow(flowId)` | Mark flow finished |
| `createAssistant` / `callAssistant` / `stopAssistant` | Assistant mode |

### Specialist agents

Per-agent model settings use `AgentConfigType` and embedded prompt templates (`PromptType`):

| Agent | Role |
|---|---|
| `primary_agent` | Orchestrates a subtask |
| `generator` / `refiner` | Plan and revise subtask lists |
| `pentester` | Exploit / vulnerability work |
| `coder` | Write and maintain code |
| `installer` | Environment and tool setup |
| `searcher` / `enricher` | Research and multi-source enrichment |
| `memorist` | Long-term memory store/search |
| `adviser` | Expert guidance |
| `reflector` | Correct non-tool unstructured replies |
| `assistant` | Interactive flow-scoped chat |
| reporter (prompt/report path) | Final task reporting |

Optional supervision env flags include `EXECUTION_MONITOR_ENABLED`, same-tool and total tool-call limits, `MAX_GENERAL_AGENT_TOOL_CALLS`, `MAX_LIMITED_AGENT_TOOL_CALLS`, and `AGENT_PLANNING_STEP_ENABLED` (useful for smaller local models).

### Tools and isolation

Agents do not run host shell commands directly. Terminal and file tools execute inside Docker containers (`DOCKER_HOST`, socket mount, optional `DOCKER_INSIDE` / `DOCKER_NET_ADMIN`). User uploads and resources appear under `/work` paths inside the worker. Network tools (browser via scraper, DuckDuckGo, Google CSE, Tavily, Traversaal, Perplexity, Sploitus, Searxng) activate only when corresponding env keys or URLs are set. Barrier tools `done` and `ask` control subtask completion and human checkpoints (`ASK_USER`).

## API surface under `/api/v1`

Base path is fixed as `const baseURL = "/api/v1"` in the Gin router.

### Auth

| Method | Path | Notes |
|---|---|---|
| `POST` | `/auth/login` | Local session login |
| `GET` | `/auth/logout` | End session |
| `GET` | `/auth/authorize` | OAuth start |
| `GET`/`POST` | `/auth/login-callback` | OAuth callback |
| `GET` | `/info` | Session / auth info (try-auth) |
| `PUT` | `/user/password` | Local user password change |

Session cookies use the `auth` store and `COOKIE_SIGNING_SALT`. Programmatic access uses Bearer API tokens (`Authorization: Bearer …`) with privilege scopes; token CRUD is session-user gated under `/tokens`. OAuth Google and GitHub register only when `PUBLIC_URL` and client credentials are set.

### Resource groups (authenticated)

| Group | Examples |
|---|---|
| Flows | `POST/GET/PUT/DELETE /flows`, `/flows/:id/graph` |
| Tasks / subtasks | `/flows/:id/tasks`, nested subtasks |
| Files / resources | `/flows/:id/files`, `/resources` |
| Containers / toolcalls | `/containers`, `/toolcalls` |
| Assistants | `/flows/:id/assistants` |
| Logs | agent, assistant, msg, term, search, vecstore, screenshots |
| Knowledge | `/knowledge` CRUD + search |
| Providers / settings | `GET /providers`, `GET /settings` |
| Prompts / users / roles | admin and settings management |
| Analytics | `/usage` |
| GraphQL | `Any /graphql` |

Frontend SPA routes (served from static files or reverse proxy): `/dashboard`, `/flows`, `/settings`, `/templates`, `/resources`, `/knowledges`, `/login`, `/oauth`, `/chat`.

## BYOK LLM providers

No hosted model is required. Wire one or more providers via environment variables or UI provider profiles.

| Type (`ProviderType`) | Typical env keys |
|---|---|
| `openai` | `OPEN_AI_KEY`, `OPEN_AI_SERVER_URL` |
| `anthropic` | `ANTHROPIC_API_KEY`, `ANTHROPIC_SERVER_URL` |
| `gemini` | `GEMINI_API_KEY`, `GEMINI_SERVER_URL` |
| `bedrock` | `BEDROCK_*` region and credentials |
| `ollama` | `OLLAMA_SERVER_URL`, optional API key and config path |
| `custom` | `LLM_SERVER_URL`, `LLM_SERVER_KEY`, model, YAML config path |
| `deepseek` / `glm` / `kimi` / `qwen` | respective `*_API_KEY` and server URL |

OpenAI-compatible aggregators (OpenRouter, DeepInfra, vLLM, Azure-style endpoints) map through `custom` / `LLM_SERVER_*` plus YAML under `examples/configs/`. GraphQL exposes `testAgent` and `testProvider` before committing flows. Embeddings (`EMBEDDING_*`) power knowledge and memory search when configured; without an embedder, embedding-dependent knowledge ops error while the server still starts.

## Data and optional integrations

| Component | Required? | Purpose |
|---|---|---|
| PostgreSQL + pgvector (`vxcontrol/pgvector`) | Yes | Flows, users, logs, vector memory |
| Docker socket or remote Docker API | Yes | Sandbox workers |
| Scraper (`vxcontrol/scraper`) | Core compose | Browser tool / screenshots |
| Observability overlay | No | OTEL → VictoriaMetrics, Jaeger, Loki, Grafana (`OTEL_HOST`) |
| Langfuse overlay | No | LLM analytics (`LANGFUSE_*`) |
| Graphiti overlay | No | Neo4j knowledge graph (`GRAPHITI_*`, `graphiti_search`) |

Compose networks: `pentagi-network` (core), plus `observability-network` and `langfuse-network` for overlays. Create core networks first if overlays fail with missing-network errors.

## Deploy to first flow

<Steps>
  <Step title="Prerequisites">
    Docker and Docker Compose. Host access to the Docker API (default socket mount). At least one LLM provider credential or reachable local endpoint.
  </Step>
  <Step title="Configure environment">
    Copy `.env.example` to `.env`. Set at least one provider key (for example `OPEN_AI_KEY` or `OLLAMA_SERVER_URL`). Change security-sensitive values (`COOKIE_SIGNING_SALT`, DB password, SSL materials) before production use.
  </Step>
  <Step title="Start the core stack">
```bash
docker compose up -d
```
    Core services: `pentagi`, `pgvector`, `pgexporter`, `scraper`. Optional:

```bash
docker compose -f docker-compose.yml -f docker-compose-observability.yml up -d
docker compose -f docker-compose.yml -f docker-compose-langfuse.yml up -d
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml up -d
```
  </Step>
  <Step title="Open the UI and secure the admin account">
    Open `https://localhost:8443`. Fresh install seeds:

    - Email: `admin@pentagi.com`
    - Password: `admin`
    - `password_change_required = true`

    Change the password immediately. There is no public self-service signup; admins manage users via `/api/v1/users/`. Lost admin password: installer maintenance reset path.
  </Step>
  <Step title="Create a flow">
    In the UI: **Flows → New**, pick a configured provider, enter an objective (see sample prompts under `examples/prompts/`). Or mutate GraphQL `createFlow` / REST `POST /api/v1/flows` with a Bearer token from **Settings → API tokens**.
  </Step>
  <Step title="Verify health">
    - UI loads dashboard and providers list.
    - Swagger: `https://localhost:8443/api/v1/swagger/index.html`
    - Flow status transitions `created` → `running` (or `waiting` if `ask` fires).
    - Toolcalls and terminal logs appear under the flow; reports support web view, clipboard, Markdown, and PDF download (not JSON export).
  </Step>
</Steps>

<Tip>
For guided setup (system checks, `.env` generation, SSL hardening, compose deploy), use the interactive installer (`backend/cmd/installer`) instead of hand-editing `.env`.
</Tip>

## Repository layout

:::files
pentagi/
├── backend/
│   ├── cmd/pentagi/          # API server entrypoint
│   ├── cmd/installer/        # TUI deployment wizard
│   ├── cmd/{c,e,f}tester/    # Container, embedding, function testers
│   ├── pkg/server/           # Gin router, auth, REST services
│   ├── pkg/graph/            # schema.graphqls + gqlgen resolvers
│   ├── pkg/providers/        # LLM adapters + config.yml baselines
│   ├── pkg/tools/            # Tool registry and executors
│   ├── pkg/controller/       # Flow/task/subtask workers
│   ├── pkg/docker/           # Sandbox Docker client
│   └── migrations/sql/       # goose schema + seed admin
├── frontend/src/             # React + Apollo SPA
├── observability/            # Grafana, OTEL, Jaeger, Loki configs
├── examples/                 # Provider YAML, prompts, guides
├── docker-compose*.yml       # Core + optional stacks
└── .env.example              # Authoritative env catalog
:::

## Capability boundaries

- Not a predefined-campaign BAS or adversary-emulation product.
- JSON flow-report export is not a supported UI output format today.
- `LLM_SERVER_*` custom endpoints work but are marked experimental in project docs and may evolve.
- The `pentagi` compose service runs as `root` when using the Docker socket; TCP Docker endpoints can drop socket root if you reconfigure access.
- Default bind is localhost-only; external access needs `PENTAGI_LISTEN_IP`, `PUBLIC_URL`, and `CORS_ORIGINS` aligned to real hostnames/IPs (never `0.0.0.0` in URL/CORS lists).

## Next

<CardGroup cols={2}>
  <Card title="Installation" href="/installation">
    Core compose, volumes, SSL, and optional observability / Langfuse / Graphiti overlays.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    First successful run: LLM key, compose up, admin password, create a flow.
  </Card>
  <Card title="Interactive installer" href="/installer">
    TUI wizard for system checks, `.env`, deploy, and admin password reset.
  </Card>
  <Card title="Flows, tasks, and subtasks" href="/flows-tasks-subtasks">
    Lifecycle states, Generator/Refiner plans, assistant mode, stop/finish boundaries.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    Wire OpenAI, Anthropic, Gemini, Bedrock, DeepSeek, GLM, Kimi, Qwen, Ollama.
  </Card>
  <Card title="REST API" href="/rest-api">
    Gin routes under `/api/v1` and Swagger at `/api/v1/swagger`.
  </Card>
  <Card title="GraphQL API" href="/graphql-api">
    Query, Mutation, Subscription surface and WebSocket log streams.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full Config struct and `.env.example` reference with defaults.
  </Card>
</CardGroup>

---

## 02. Installation

> Prerequisites, Docker Compose core stack, .env from .env.example, SSL and data volumes, and compose overlays for observability, Langfuse, and Graphiti.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/02-installation.md
- Generated: 2026-07-10T07:05:34.232Z

### Source Files

- `README.md`
- `.env.example`
- `docker-compose.yml`
- `docker-compose-observability.yml`
- `docker-compose-langfuse.yml`
- `docker-compose-graphiti.yml`
- `Dockerfile`

---
title: "Installation"
description: "Prerequisites, Docker Compose core stack, .env from .env.example, SSL and data volumes, and compose overlays for observability, Langfuse, and Graphiti."
---

PentAGI deploys as a Docker Compose stack: the `vxcontrol/pentagi` application container, `vxcontrol/pgvector` (PostgreSQL + pgvector), optional `postgres-exporter`, and `vxcontrol/scraper`. Configuration is driven by a host `.env` file cloned from `.env.example`. Optional stacks layer on with additional compose files for OpenTelemetry/Grafana observability, Langfuse LLM analytics, and Graphiti/Neo4j knowledge graph.

## Prerequisites

| Requirement | Detail |
|---|---|
| Runtime | Docker Engine + Docker Compose v2, or Podman (rootless needs scraper port changes) |
| CPU | Minimum 2 vCPU |
| Memory | Minimum 4 GB RAM |
| Disk | Minimum 20 GB free |
| Network | Pull images (`vxcontrol/pentagi`, `vxcontrol/pgvector`, `vxcontrol/scraper`, and optional overlays) |
| LLM | At least one provider key or local endpoint (OpenAI, Anthropic, Gemini, Bedrock, Ollama, DeepSeek, GLM, Kimi, Qwen, or custom `LLM_SERVER_*`) |
| Docker access | Host Docker socket (default `/var/run/docker.sock`) so the app can spawn sandbox worker containers |

<Warning>
The core `pentagi` service runs as `root:root` and mounts the Docker socket so it can create isolated tool containers. Treat host Docker access as root-equivalent. For stronger isolation, use a separate worker node (see [Docker sandbox and worker nodes](/docker-sandbox-workers)).
</Warning>

## Deployment architecture

```text
Host
├── .env                          # secrets + wiring (from .env.example)
├── example.custom.provider.yml   # mounted → /opt/pentagi/conf/custom.provider.yml
├── example.ollama.provider.yml   # mounted → /opt/pentagi/conf/ollama.provider.yml
├── docker-compose.yml            # core: pentagi, pgvector, pgexporter, scraper
├── docker-compose-observability.yml   # optional overlay (needs core networks first)
├── docker-compose-langfuse.yml        # optional overlay
└── docker-compose-graphiti.yml        # optional overlay

Compose networks (created by core file):
  pentagi-network | observability-network | langfuse-network

Named volumes (core):
  pentagi-data → /opt/pentagi/data
  pentagi-ssl  → /opt/pentagi/ssl
  pentagi-ollama → /root/.ollama
  scraper-ssl
  pentagi-postgres-data
```

| Service | Image (default) | Host bind (default) | Role |
|---|---|---|---|
| `pentagi` | `vxcontrol/pentagi:latest` (`PENTAGI_IMAGE`) | `127.0.0.1:8443` → `8443` | API, UI, agents, Docker sandbox control |
| `pgvector` | `vxcontrol/pgvector:latest` | `127.0.0.1:5432` → `5432` | App DB + vector memory |
| `pgexporter` | `quay.io/prometheuscommunity/postgres-exporter:v0.16.0` | `127.0.0.1:9187` → `9187` | Postgres metrics |
| `scraper` | `vxcontrol/scraper:latest` | `127.0.0.1:9443` → `443` | Isolated browser/scrape sessions |

`pentagi` joins all three networks so optional overlays can reach it without republishing core ports.

## Install paths

### Interactive installer (recommended)

Prebuilt installer binaries (Linux/Windows/macOS, amd64/arm64) walk through system checks, `.env` generation, LLM and search setup, SSL hardening, and compose deploy. Docker socket access requires either `sudo ./installer` or membership in the `docker` group.

Installer downloads and full wizard behavior: [Interactive installer](/installer).

### Manual Compose install

<Steps>
<Step title="Create a working directory">
```bash
mkdir pentagi && cd pentagi
```
Clone the repo, or download only the compose and env artifacts you need.
</Step>

<Step title="Create .env from .env.example">
```bash
curl -o .env https://raw.githubusercontent.com/vxcontrol/pentagi/master/.env.example
```
Or, in a full checkout: `cp .env.example .env`.
</Step>

<Step title="Provide provider YAML stubs for compose mounts">
Compose always mounts host files for custom and Ollama agent configs. Create them before `up`:

```bash
curl -o example.custom.provider.yml \
  https://raw.githubusercontent.com/vxcontrol/pentagi/master/examples/configs/custom-openai.provider.yml
curl -o example.ollama.provider.yml \
  https://raw.githubusercontent.com/vxcontrol/pentagi/master/examples/configs/ollama-llama318b.provider.yml
```

Override host paths with `PENTAGI_LLM_SERVER_CONFIG_PATH` and `PENTAGI_OLLAMA_SERVER_CONFIG_PATH` if needed.
</Step>

<Step title="Set at least one LLM provider">
Minimum examples (edit `.env`):

```bash
# Cloud — pick one or more
OPEN_AI_KEY=...
ANTHROPIC_API_KEY=...
GEMINI_API_KEY=...

# Local Ollama
OLLAMA_SERVER_URL=http://host.docker.internal:11434
# or a reachable LAN URL from the container network

# Custom OpenAI-compatible (vLLM, OpenRouter, etc.)
LLM_SERVER_URL=http://...
LLM_SERVER_KEY=...
LLM_SERVER_MODEL=...
# LLM_SERVER_CONFIG_PATH is set inside the container by compose when the host file is mounted
```

Search engines (`DUCKDUCKGO_ENABLED`, `TAVILY_API_KEY`, `GOOGLE_*`, `SEARXNG_URL`, etc.) are optional but recommended for richer recon. Full key list: [Environment variables](/environment-variables).
</Step>

<Step title="Harden security defaults">
Change at least:

| Variable | Default intent | Action |
|---|---|---|
| `COOKIE_SIGNING_SALT` | Cookie signing salt | Set a long random string |
| `PENTAGI_POSTGRES_PASSWORD` | `postgres` | Strong password; keep `DATABASE_URL` consistent via compose interpolation |
| `PUBLIC_URL` | `https://localhost:8443` | Real public origin for OAuth redirects and UI |
| `CORS_ORIGINS` | `https://localhost:8443` | Exact browser origins that will call the UI/API |

Optional: strip inline comments if a tool treats them as values:

```bash
perl -i -pe 's/\s+#.*$//' .env
```
</Step>

<Step title="Start the core stack">
```bash
curl -O https://raw.githubusercontent.com/vxcontrol/pentagi/master/docker-compose.yml
docker compose up -d
```

Verification:

```bash
docker compose ps
# pentagi healthy dependency: pgvector healthcheck (pg_isready)
curl -kI https://localhost:8443
```

Open `https://localhost:8443`. Default local admin:

- Email: `admin@pentagi.com`
- Password: `admin`

Change the password on first login. There is no public self-service sign-up from the login page.
</Step>
</Steps>

<Check>
Core install is successful when `pgvector` is healthy, `pentagi` is running, HTTPS responds on `8443`, and the login page accepts the admin account.
</Check>

## Core stack details

### Environment file

Compose interpolates host `.env` into service `environment` and port/volume overrides. Important groups:

| Area | Keys (selection) |
|---|---|
| Listen binds | `PENTAGI_LISTEN_IP` (default `127.0.0.1`), `PENTAGI_LISTEN_PORT` (`8443`), scraper/Postgres exporter bind vars |
| Data paths | `PENTAGI_DATA_DIR`, `PENTAGI_SSL_DIR`, `PENTAGI_OLLAMA_DIR`, `PENTAGI_DOCKER_SOCKET` |
| App server | `SERVER_HOST` (`0.0.0.0`), `SERVER_PORT` (`8443`), `SERVER_USE_SSL` (`true`), `SERVER_SSL_CRT` / `SERVER_SSL_KEY` |
| Database | `PENTAGI_POSTGRES_USER`, `PENTAGI_POSTGRES_PASSWORD`, `PENTAGI_POSTGRES_DB` (`pentagidb`); compose builds `DATABASE_URL` to `pgvector:5432` |
| Docker sandbox | `DOCKER_HOST`, `DOCKER_INSIDE`, `DOCKER_NET_ADMIN`, `DOCKER_SOCKET`, `DOCKER_DEFAULT_IMAGE`, `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` |
| Scraper | `SCRAPER_PRIVATE_URL` (default `https://someuser:somepass@scraper/`), `LOCAL_SCRAPER_USERNAME` / `PASSWORD` |
| Integrations | `OTEL_HOST`, `LANGFUSE_*`, `GRAPHITI_*` |

Inside the container, the app image entrypoint is `/opt/pentagi/bin/entrypoint.sh` → `/opt/pentagi/bin/pentagi` (multi-stage `Dockerfile`: Node frontend build + Go API build → Alpine runtime).

### SSL certificates

With `SERVER_USE_SSL=true` (compose default):

1. Entrypoint defaults paths to `ssl/server.key` and `ssl/server.crt` under the working directory (`/opt/pentagi`), which is volume-backed by `pentagi-ssl` → `/opt/pentagi/ssl`.
2. If both files exist, they are reused.
3. If missing, entrypoint generates a local CA and a `localhost` server cert (RSA 4096, multi-year validity), appends the CA to the cert chain, and removes temporary CA private key material after signing.
4. To supply your own certs, place them on the SSL volume (or host dir bound via `PENTAGI_SSL_DIR`) and set `SERVER_SSL_CRT` / `SERVER_SSL_KEY` to the in-container paths.

Browsers will warn on the auto-generated certificate. External CA bundles for outbound LLM/tool TLS use `EXTERNAL_SSL_CA_PATH` / `EXTERNAL_SSL_INSECURE` (paths inside the container, typically under `/opt/pentagi/ssl/`).

### Data volumes

| Volume / bind | Container path | Persistence |
|---|---|---|
| `pentagi-data` or `PENTAGI_DATA_DIR` | `/opt/pentagi/data` | App data |
| `pentagi-ssl` or `PENTAGI_SSL_DIR` | `/opt/pentagi/ssl` | TLS material |
| `pentagi-ollama` or `PENTAGI_OLLAMA_DIR` | `/root/.ollama` | Ollama keys/models path in-container |
| `pentagi-postgres-data` | `/var/lib/postgresql/data` | DB + embeddings store |
| `scraper-ssl` | `/usr/src/app/ssl` | Scraper TLS |
| Host Docker socket | `/var/run/docker.sock` | Runtime only (not a named volume) |
| Host provider YAMLs | `/opt/pentagi/conf/*.provider.yml` | Config mounts |

Destroying named volumes deletes DB and SSL state. Prefer `docker compose down` without `-v` for restarts.

### Image build (optional)

Production image is multi-stage:

1. `node:23-slim` — `pnpm install` + frontend production build into `/opt/pentagi/fe`
2. `golang:1.24-bookworm` — static `pentagi`, `ctester`, `ftester`, `etester` binaries
3. `alpine:3.23.3` — runtime user `pentagi` (group includes docker GID 998); compose still forces `user: root:root` for socket access

```bash
docker build -t local/pentagi:latest .
# then set PENTAGI_IMAGE=local/pentagi:latest in .env
```

## Optional compose overlays

Overlay files declare `external: true` for `pentagi-network`, `observability-network`, and/or `langfuse-network`. **Start core `docker-compose.yml` first** so those networks exist, then attach overlays.

### Observability (OpenTelemetry → Grafana)

```bash
# .env
OTEL_HOST=otelcol:8148
```

```bash
curl -O https://raw.githubusercontent.com/vxcontrol/pentagi/master/docker-compose-observability.yml
docker compose -f docker-compose.yml -f docker-compose-observability.yml up -d
```

| Component | Default host access | Purpose |
|---|---|---|
| Grafana | `127.0.0.1:3000` | Dashboards (`./observability/grafana/...`) |
| OTEL collector | gRPC `8148`, HTTP `4318` | Ingest from PentAGI (`OTEL_HOST=otelcol:8148`) |
| VictoriaMetrics, Loki, Jaeger, node-exporter, cAdvisor, ClickHouse | Internal to `observability-network` | Metrics, logs, traces |

Repo-local configs under `observability/` are bind-mounted into the stack.

### Langfuse (LLM analytics)

```bash
# .env — wire PentAGI → Langfuse
LANGFUSE_BASE_URL=http://langfuse-web:3000
LANGFUSE_PROJECT_ID=   # match LANGFUSE_INIT_PROJECT_ID
LANGFUSE_PUBLIC_KEY=   # match LANGFUSE_INIT_PROJECT_PUBLIC_KEY
LANGFUSE_SECRET_KEY=   # match LANGFUSE_INIT_PROJECT_SECRET_KEY
```

```bash
curl -O https://raw.githubusercontent.com/vxcontrol/pentagi/master/docker-compose-langfuse.yml
docker compose -f docker-compose.yml -f docker-compose-langfuse.yml up -d
```

Langfuse UI defaults to `127.0.0.1:4000` (`LANGFUSE_LISTEN_PORT`). Init admin and project keys live under `LANGFUSE_INIT_*` / `LANGFUSE_NEXTAUTH_*` in `.env.example`. Change salts, encryption key, Redis/Postgres/MinIO credentials before production use.

To also ship Langfuse telemetry into the observability stack:

```bash
LANGFUSE_OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4318
```

### Graphiti knowledge graph (beta)

```bash
# .env
GRAPHITI_ENABLED=true
GRAPHITI_TIMEOUT=30
GRAPHITI_URL=http://graphiti:8000
GRAPHITI_MODEL_NAME=gpt-5-mini
NEO4J_USER=neo4j
NEO4J_PASSWORD=devpassword   # change in production
NEO4J_URI=bolt://neo4j:7687
OPEN_AI_KEY=...              # required by Graphiti entity extraction
```

```bash
curl -O https://raw.githubusercontent.com/vxcontrol/pentagi/master/docker-compose-graphiti.yml
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml up -d
```

| Service | Host ports | Notes |
|---|---|---|
| `neo4j` | `127.0.0.1:7474`, `7687` | Browser + Bolt; volume `neo4j_data` |
| `graphiti` | `127.0.0.1:8000` | Healthcheck `/healthcheck`; API docs `/docs` |

Graphiti uses **only** the OpenAI-compatible endpoint from `OPEN_AI_KEY` / `OPEN_AI_SERVER_URL` (mapped as `OPENAI_API_KEY` / `OPENAI_BASE_URL`). Anthropic, Gemini, Bedrock, and other PentAGI providers are not used for graph extraction. Leave `GRAPHITI_ENABLED=false` if that endpoint is unavailable.

### All overlays together

```bash
docker compose \
  -f docker-compose.yml \
  -f docker-compose-langfuse.yml \
  -f docker-compose-graphiti.yml \
  -f docker-compose-observability.yml \
  up -d
```

Optional shell aliases:

```bash
alias pentagi="docker compose -f docker-compose.yml -f docker-compose-langfuse.yml -f docker-compose-graphiti.yml -f docker-compose-observability.yml"
alias pentagi-up="pentagi up -d"
alias pentagi-down="pentagi down"
```

## Network exposure

Default binds are localhost-only. For LAN access:

```bash
PENTAGI_LISTEN_IP=0.0.0.0
PENTAGI_LISTEN_PORT=8443
PUBLIC_URL=https://192.168.1.100:8443
CORS_ORIGINS=https://localhost:8443,https://192.168.1.100:8443
```

Then:

```bash
docker compose down
docker compose up -d --force-recreate
docker ps | grep pentagi   # expect 0.0.0.0:8443->8443/tcp
```

Do not put `0.0.0.0` in `PUBLIC_URL` or `CORS_ORIGINS`. Open host firewall port `8443/tcp` as needed.

## Podman notes

| Mode | Scraper config |
|---|---|
| Rootful | Same as Docker (`SCRAPER_PRIVATE_URL=https://user:pass@scraper/`) |
| Rootless | Privileged port 443 is unavailable; use non-privileged HTTP, e.g. `SCRAPER_PRIVATE_URL=http://someuser:somepass@scraper:3000/` (see comments in `.env.example`) |

## Operations and troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| Missing `pentagi-network` / `observability-network` / `langfuse-network` | Overlay started without core | `docker compose -f docker-compose.yml up -d` first, then overlays |
| `pentagi` waits / restarts on DB | `pgvector` not healthy or wrong password | Check `PENTAGI_POSTGRES_*`; `docker compose logs pgvector` |
| Port still `127.0.0.1` after edit | Env not reloaded | `docker compose up -d --force-recreate` |
| No LLM / empty provider list | No keys or unreachable local URL | Set at least one provider; verify URLs from inside the container network |
| Docker sandbox failures | Socket not mounted or permission denied | Confirm `PENTAGI_DOCKER_SOCKET` / compose volume; root user in compose |
| TLS browser warning | Auto-generated cert | Expected; replace with real certs on `pentagi-ssl` |
| Graphiti unhealthy / no graph tool value | Missing `OPEN_AI_KEY` or stack not attached | Enable overlay + OpenAI-compatible key, or set `GRAPHITI_ENABLED=false` |
| IDE loads bad env values | Inline comments in `.env` | Strip comments with the `perl` one-liner above |

Useful commands:

```bash
docker compose logs -f pentagi
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml ps graphiti neo4j
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml logs -f graphiti
```

Swagger (after UI is up): `https://localhost:8443/api/v1/swagger/index.html`.

## What stays out of the web UI

After install, the console manages provider *profiles*, prompts, and API tokens. These remain server-side via `.env` / compose:

- Provider API keys and base URLs
- Search engine credentials
- Langfuse, OTEL, Graphiti wiring
- Docker sandbox and network policy (`DOCKER_*`)
- Listen addresses and SSL material

## Next

<CardGroup>
<Card title="Quickstart" href="/quickstart">
First login, change admin defaults, create a flow, and verify provider health.
</Card>
<Card title="Interactive installer" href="/installer">
TUI wizard: system checks, .env generation, SSL hardening, and admin password reset.
</Card>
<Card title="Environment variables" href="/environment-variables">
Full Config / .env.example reference with defaults.
</Card>
<Card title="Configure LLM providers" href="/configure-llm-providers">
Wire cloud and local providers after the stack is running.
</Card>
<Card title="Observability and Langfuse" href="/observability-and-langfuse">
What OTEL_HOST and LANGFUSE_* measure and how the optional stacks connect.
</Card>
<Card title="Knowledge graph" href="/knowledge-graph">
Graphiti enablement, GRAPHITI_* settings, and failure modes.
</Card>
<Card title="Docker sandbox and worker nodes" href="/docker-sandbox-workers">
Socket options, images, and multi-host worker isolation.
</Card>
</CardGroup>

---

## 03. Quickstart

> First successful run: set at least one LLM key, docker compose up, open https://localhost:8443, change admin@pentagi.com defaults, create a flow, and verify provider and UI health.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/03-quickstart.md
- Generated: 2026-07-10T07:06:01.480Z

### Source Files

- `README.md`
- `.env.example`
- `docker-compose.yml`
- `frontend/src/pages/login.tsx`
- `frontend/src/pages/flows/new-flow.tsx`
- `backend/pkg/server/services/auth.go`
- `backend/pkg/server/services/flows.go`

---
title: "Quickstart"
description: "First successful run: set at least one LLM key, docker compose up, open https://localhost:8443, change admin@pentagi.com defaults, create a flow, and verify provider and UI health."
---

PentAGI boots as a Docker Compose stack (`pentagi`, `pgvector`, `scraper`, and optional exporters) that serves the web UI and REST/GraphQL APIs on HTTPS port **8443**. A first successful run needs: at least one LLM provider key (or local endpoint) in `.env`, a healthy core stack, sign-in as the seeded local admin, a password change when `password_change_required` is set, and a flow created with a ready provider.

<Warning>
Only assess systems you own or are explicitly authorized to test. Acceptable use is defined in the repository `EULA.md`.
</Warning>

## Prerequisites

| Requirement | Notes |
|---|---|
| Docker and Docker Compose | Or Podman (scraper needs non-privileged port config in rootless mode) |
| CPU / memory / disk | Minimum 2 vCPU, 4 GB RAM, ~20 GB free |
| Network | Pull images and call LLM/search endpoints (unless fully local inference) |
| BYOK credentials | At least one provider: OpenAI, Anthropic, Gemini, Bedrock, DeepSeek, GLM, Kimi, Qwen, Ollama, or custom `LLM_SERVER_*` |

Optional later: search engines, OAuth, Langfuse, OpenTelemetry, Graphiti. They are not required for first UI login and a minimal flow.

## Path overview

```text
.env (LLM key) ──► docker compose up -d
        │
        ▼
https://localhost:8443 ──► login admin@pentagi.com / admin
        │
        ▼
password change (password_change_required) ──► /flows/new
        │
        ▼
Automation or Assistant flow ──► monitor status / report
```

For guided `.env` generation and deploy, use the interactive installer instead of the manual path below.

## Step-by-step: first run

<Steps>
<Step title="Prepare working directory and env file">

From a clean directory (or a clone of the repo):

```bash
mkdir -p pentagi && cd pentagi
curl -o .env https://raw.githubusercontent.com/vxcontrol/pentagi/master/.env.example
curl -O https://raw.githubusercontent.com/vxcontrol/pentagi/master/docker-compose.yml
curl -o example.custom.provider.yml \
  https://raw.githubusercontent.com/vxcontrol/pentagi/master/examples/configs/custom-openai.provider.yml
curl -o example.ollama.provider.yml \
  https://raw.githubusercontent.com/vxcontrol/pentagi/master/examples/configs/ollama-llama318b.provider.yml
```

Compose mounts `example.custom.provider.yml` and `example.ollama.provider.yml` by default. Touching or downloading them avoids missing bind-mount paths.

</Step>
<Step title="Set at least one LLM provider">

Edit `.env` and set **one or more** of the following (empty keys leave that provider disabled in `ProvidersReadinessStatus`):

| Provider | Primary env keys |
|---|---|
| OpenAI | `OPEN_AI_KEY`, optional `OPEN_AI_SERVER_URL` |
| Anthropic | `ANTHROPIC_API_KEY`, optional `ANTHROPIC_SERVER_URL` |
| Gemini | `GEMINI_API_KEY`, optional `GEMINI_SERVER_URL` |
| AWS Bedrock | `BEDROCK_REGION` + one of default auth / bearer / access keys |
| DeepSeek / GLM / Kimi / Qwen | `DEEPSEEK_API_KEY`, `GLM_API_KEY`, `KIMI_API_KEY`, `QWEN_API_KEY` |
| Ollama (local or cloud) | `OLLAMA_SERVER_URL`, optional `OLLAMA_SERVER_API_KEY`, `OLLAMA_SERVER_MODEL` |
| Custom OpenAI-compatible | `LLM_SERVER_URL`, `LLM_SERVER_KEY`, optional `LLM_SERVER_MODEL`, `LLM_SERVER_CONFIG_PATH` |

Minimal cloud example:

```bash
OPEN_AI_KEY=sk-...
# or
ANTHROPIC_API_KEY=sk-ant-...
# or
GEMINI_API_KEY=...
```

Minimal local Ollama example:

```bash
OLLAMA_SERVER_URL=http://host.docker.internal:11434
OLLAMA_SERVER_MODEL=llama3.1:8b
```

<Tip>
Provider API keys and base URLs are server-side (`.env` / compose). After the stack is up, **Settings → Providers** manages user-defined profiles (per-agent models, reasoning, pricing) and can run `testProvider` / `testAgent` checks. Env keys still gate whether a built-in type is marked enabled.
</Tip>

</Step>
<Step title="Harden defaults before production use">

For anything beyond a local lab, change at least:

| Variable | Default / note |
|---|---|
| `COOKIE_SIGNING_SALT` | Example value `salt` — replace with a random secret |
| `PUBLIC_URL` / `CORS_ORIGINS` | Default `https://localhost:8443` |
| `PENTAGI_POSTGRES_PASSWORD` | Default `postgres` |
| Scraper `LOCAL_SCRAPER_*` | Defaults `someuser` / `somepass` |

Optional for first run: `DUCKDUCKGO_ENABLED=true`, `SPLOITUS_ENABLED=true`, or other search keys. They improve agent research tools but are not required to start the UI.

</Step>
<Step title="Start the core stack">

```bash
docker compose up -d
```

Core services from `docker-compose.yml`:

| Service | Role |
|---|---|
| `pentagi` | API + UI on host `127.0.0.1:8443` → container `8443` |
| `pgvector` | PostgreSQL + pgvector (`pentagidb`); healthchecked before `pentagi` starts |
| `scraper` | Isolated browser fetch (default host `127.0.0.1:9443` → container `443`) |
| `pgexporter` | Postgres metrics exporter (optional for first run) |

Default bind is **localhost only** (`PENTAGI_LISTEN_IP=127.0.0.1`). Docker socket is mounted so agent containers can be spawned.

</Step>
<Step title="Verify containers and open the UI">

```bash
docker compose ps
docker compose logs -f pentagi
```

Expected: `pgvector` healthy, `pentagi` running, port mapping similar to `127.0.0.1:8443->8443/tcp`.

Open **https://localhost:8443**. Accept the self-signed certificate warning if you did not mount custom `SERVER_SSL_CRT` / `SERVER_SSL_KEY`.

</Step>
<Step title="Sign in as the default admin">

There is no public self-service sign-up on the login page. Migration seed creates:

| Field | Value |
|---|---|
| Email | `admin@pentagi.com` |
| Password | `admin` |
| Status | `active` |
| Role | Admin (`role_id` 1) |
| `password_change_required` | `true` |

Login uses `POST /api/v1/auth/login` with session cookies (`HttpOnly`, `Secure` when TLS is present). Successful login loads privileges from the admin role (including `flows.create`, settings, providers, and so on).

Default post-login destination when no return URL is set: **`/flows/new`**.

</Step>
<Step title="Change the default password">

On first login, local users with `password_change_required` see **Update Password** (UI allows skip, but changing is required before real work).

- Endpoint: `PUT /api/v1/user/password`
- Body fields: `current_password`, `password`, `confirm_password`
- Backend validator (`stpass`): password must be **longer than 15 characters**, **or** at least **8** characters with number, lowercase, uppercase, and one of `!@#$&*`
- Success clears `password_change_required`

If the admin password is lost later, use the installer maintenance path to reset `admin@pentagi.com`.

</Step>
<Step title="Confirm providers are ready">

In the UI, open **Settings → Providers** (or the provider picker on the new-flow form).

GraphQL exposes readiness as booleans per type under `ProvidersConfig.enabled` (`openai`, `anthropic`, `gemini`, `bedrock`, `ollama`, `custom`, `deepseek`, `glm`, `kimi`, `qwen`). At least one type you configured should be available for selection.

Optional health check before a real engagement:

1. Create or edit a user-defined provider profile under **Settings → Providers**.
2. Run the UI test actions backed by GraphQL `testProvider` / `testAgent`.
3. Prefer a cheap agent/model for smoke tests.

</Step>
<Step title="Create the first flow">

Route: **`/flows/new`** (sidebar **Flows → New Flow**).

1. Choose mode:
   - **Automation** — autonomous end-to-end run (`createFlow`)
   - **Assistant** — interactive session (`createFlowWithAssistant`); optional **Use Agents** (default from `ASSISTANT_USE_AGENTS` / system settings)
2. Select a provider name (built-in or user-defined).
3. Optionally attach resources / use a flow template.
4. Enter a natural-language objective (target, scope, expected outcome).

Example first prompt:

```text
Assess https://target.example for common web application vulnerabilities.
Focus on authentication, file handling, and injection issues.
Stay within the provided target only and summarize confirmed findings
with reproduction steps.
```

Longer checklists live under `examples/prompts/` (for example `base_web_pentest.md`).

Submit creates a flow via GraphQL/REST (`createFlow` / `POST /api/v1/flows/`) with required `input` and `provider`. The UI navigates to `/flows/{id}?tab=automation|assistant`.

Flow lifecycle statuses: `created`, `running`, `waiting`, `finished`, `failed`. Patch actions include `stop`, `finish`, `input`, and `rename`.

</Step>
<Step title="Confirm the run is healthy">

On the flow page:

- Messages and agent activity stream (GraphQL subscriptions over WebSocket when the UI is connected)
- Tasks/subtasks appear as the planner executes
- Terminal/tool logs show Docker-sandbox command activity when tools run
- **Report** menu: web view, clipboard copy, Markdown download, PDF download

Stack-level checks:

```bash
docker compose ps
docker compose logs --tail=100 pentagi
curl -k -s -o /dev/null -w "%{http_code}\n" https://localhost:8443/
```

Swagger (after login or with a token): `https://localhost:8443/api/v1/swagger/index.html`.

</Step>
</Steps>

## Core compose surface

Default host ports (override with `*_LISTEN_IP` / `*_LISTEN_PORT`):

| Port (host) | Service |
|---|---|
| `8443` | PentAGI UI + API (`SERVER_USE_SSL=true` by default) |
| `5432` | pgvector (localhost-bound by default) |
| `9443` | scraper HTTPS |
| `9187` | postgres-exporter |

Data volumes: `pentagi-data`, `pentagi-ssl`, `pentagi-ollama`, `pentagi-postgres-data`, `scraper-ssl`. Networks: `pentagi-network` (plus empty `observability-network` / `langfuse-network` for optional overlays).

Start overlays only after the base compose file has created shared networks:

```bash
docker compose -f docker-compose.yml -f docker-compose-observability.yml up -d
docker compose -f docker-compose.yml -f docker-compose-langfuse.yml up -d
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml up -d
```

## External access (optional)

Default bind is loopback. For LAN/server access, set in `.env` then recreate:

```bash
PENTAGI_LISTEN_IP=0.0.0.0
PENTAGI_LISTEN_PORT=8443
PUBLIC_URL=https://192.168.1.100:8443
CORS_ORIGINS=https://localhost:8443,https://192.168.1.100:8443
```

```bash
docker compose down
docker compose up -d --force-recreate
```

Use a real IP/hostname in `PUBLIC_URL` and `CORS_ORIGINS` (not `0.0.0.0`).

## Troubleshooting

| Symptom | Likely cause | Action |
|---|---|---|
| No providers in the new-flow picker | No LLM keys / endpoints in `.env` | Set at least one provider env, recreate `pentagi` |
| Login fails for admin | Wrong credentials or inactive user | Use `admin@pentagi.com` / `admin`; check DB seed; installer reset if needed |
| Browser certificate warning | Self-signed TLS | Expected locally; mount custom certs for production |
| `pentagi` waits on DB | `pgvector` not healthy | `docker compose logs pgvector`; check Postgres password/volume |
| Flow create 403 / not permitted | Missing `flows.create` privilege | Use Admin role account |
| Flow create fails with provider not found | Name/type mismatch or disabled provider | Align picker name with enabled provider; recheck env |
| Agent tools cannot reach Docker | Socket not mounted / permission | Confirm `/var/run/docker.sock` mount; compose runs `pentagi` as root for socket access |
| Overlay compose network errors | Overlay started without base networks | Start `docker-compose.yml` first |
| Cannot reach from another host | Bound to `127.0.0.1` | Set `PENTAGI_LISTEN_IP` / `PUBLIC_URL` / `CORS_ORIGINS`, recreate |

## Minimal API smoke test (after UI token)

Create a Bearer token under **Settings → API Tokens** (shown once). Example flow create:

:::endpoint POST /api/v1/flows/
Create a flow for the authenticated user (session cookie or Bearer token). Requires `flows.create`.

**Body**

- `input` (string, required) — first task objective  
- `provider` (string, required) — provider name known to the controller  
- `functions` (object, optional) — tool allowlist overrides  
- `resource_ids` (uint64[], optional) — attach library resources  

**Responses**

- `201` — flow created  
- `400` — invalid payload  
- `403` — not permitted  
- `500` — provider missing or internal create error  
:::

## What not to expect on day one

- Public registration from the login page (admin-created local users or OAuth only)
- MCP management UI (not a live settings surface today)
- Search/Langfuse/Graphiti without extra env and compose overlays
- JSON report export from the Report menu (web / copy / Markdown / PDF only)

## Next

<CardGroup>
  <Card title="Installation" href="/installation">
    Full prerequisites, volumes, SSL, and compose overlays for observability, Langfuse, and Graphiti.
  </Card>
  <Card title="Interactive installer" href="/installer">
    TUI wizard for system checks, .env generation, SSL hardening, deploy, and admin password reset.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    Wire cloud and built-in providers, UI profiles, and pre-flow tests.
  </Card>
  <Card title="Local and custom providers" href="/local-and-custom-providers">
    Ollama, LLM_SERVER_*, OpenRouter/DeepInfra-style endpoints, and config YAML mounts.
  </Card>
  <Card title="Flows, tasks, and subtasks" href="/flows-tasks-subtasks">
    Lifecycle states and control boundaries after the first flow is running.
  </Card>
  <Card title="Sample pentest prompts" href="/sample-pentest-prompts">
    Ready-made automation inputs from examples/prompts.
  </Card>
  <Card title="Authentication and API tokens" href="/auth-and-api-tokens">
    Session login, OAuth, password change, and Bearer tokens for REST/GraphQL.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Authoritative Config and .env.example reference.
  </Card>
</CardGroup>

---

## 04. Interactive installer

> TUI installer wizard: system checks, .env generation, LLM and search setup, SSL hardening, compose deploy, and admin password reset maintenance paths.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/04-interactive-installer.md
- Generated: 2026-07-10T07:06:04.735Z

### Source Files

- `backend/cmd/installer/main.go`
- `backend/docs/installer.md`
- `backend/cmd/installer/wizard`
- `backend/cmd/installer/processor`
- `backend/cmd/installer/checker`
- `backend/cmd/installer/hardening`
- `README.md`

---
title: Interactive installer
description: TUI installer wizard — system checks, .env generation, LLM and search setup, SSL hardening, compose deploy, and admin password reset maintenance paths.
---

The interactive installer is a Terminal User Interface (TUI) wizard that configures and deploys PentAGI on a host with Docker. It creates or updates `.env`, runs system readiness checks, hardens default secrets, extracts compose overlays, applies Docker Compose stacks, and exposes maintenance operations (start/stop/update/purge and admin password reset).

Use this path when you want guided setup instead of hand-editing `.env` and composing services manually. Manual compose remains valid; the installer is the recommended operator path for first install and day-2 stack control.

## What the installer does

| Phase | Outcome |
| --- | --- |
| Bootstrap | Resolve `-e` env path, create `.env` from embedded template if missing, load dirty state under `.state/` |
| Migrate / sync | Rewrite legacy path vars; pull `HTTP(S)_PROXY` and Docker client env into `.env` when unset |
| System checks | Docker API/compose, worker Docker, CPU/memory/disk, network (including Docker Hub DNS), stack presence |
| Hardening | Replace known default secrets (Postgres, scraper, Langfuse, Neo4j, cookie salt) when volumes are not already present |
| Wizard | EULA, main menu configuration (LLM, search, tools, monitoring, server), commit via **Apply changes** or **Install** |
| Processor | Extract compose files, ensure Docker networks, `docker compose` lifecycle, worker image ops, password reset |

Stacks the processor can manage:

| Stack | Compose / role |
| --- | --- |
| `pentagi` | Core app (`docker-compose.yml`) |
| `graphiti` | Knowledge graph (`docker-compose-graphiti.yml`) |
| `langfuse` | LLM analytics (`docker-compose-langfuse.yml`) |
| `observability` | OTEL/Grafana stack (`docker-compose-observability.yml`) |
| `worker` | Worker / pentest images |
| `installer` | Self-update of the installer binary |
| `all` / `compose` | Aggregate operations |

## Prerequisites

- Docker Engine and Docker Compose available to the process (socket or remote `DOCKER_HOST`)
- Writable directory for the env file (default `.env` next to the installer)
- Network access for image pulls and optional update checks (`https://update.pentagi.com`); Docker Hub reachability is part of readiness
- Privilege to talk to Docker: run as root (`sudo ./installer`) or as a user in the `docker` group
- Rough resource floors used by readiness checks (scale with enabled optional stacks):
  - Memory base ~0.5 GB plus stack add-ons (Graphiti ~2 GB, Langfuse ~1.5 GB, Observability ~1.5 GB)
  - Disk from ~5 GB baseline up to ~25 GB when worker images are required

<Note>
Adding a user to the `docker` group is root-equivalent. Prefer `sudo ./installer` for production hosts.
</Note>

## Get and run

### Prebuilt binary

Platform archives (amd64/arm64) are published under `https://pentagi.com/downloads/{linux\|windows\|darwin}/{arch}/installer-latest.zip`.

<Steps>
  <Step title="Create a work directory">
    ```bash
    mkdir -p pentagi && cd pentagi
    ```
  </Step>
  <Step title="Download and extract">
    ```bash
    wget -O installer.zip https://pentagi.com/downloads/linux/amd64/installer-latest.zip
    unzip installer.zip
    ```
  </Step>
  <Step title="Run with Docker access">
    ```bash
    sudo ./installer
    # or: ./installer -e /path/to/.env
    # or: ./installer -v
    ```
  </Step>
</Steps>

### Build from source

From `backend/`:

```bash
go build -o ../build/installer ./cmd/installer
../build/installer -e ../.env
```

Do not `go run` the TUI in the same session you develop in; build a binary and run it separately so the terminal is not corrupted.

### CLI flags

<ParamField body="-e" type="string" default=".env">
Path to the environment file. Absolute path is resolved; missing parent dirs are created; a missing file is seeded from the embedded `.env` template with mode `0600`.
</ParamField>

<ParamField body="-v" type="boolean" default="false">
Print binary version and exit.
</ParamField>

<RequestExample>
```bash
./installer
./installer -e config/.env
./installer -v
```
</RequestExample>

## Startup pipeline

On launch the binary:

1. Parses flags and cancels on `SIGINT` / `SIGTERM`
2. Validates or creates the env file
3. Loads `state` (env vars + optional `.state/<envname>.state` dirty overlay and EULA consent)
4. Runs `DoMigrateSettings` (legacy host paths → container mount paths)
5. Runs `DoSyncNetworkSettings` (OS `HTTP_PROXY` / `HTTPS_PROXY` → `PROXY_URL`; OS Docker client vars when `.env` Docker connection keys are empty)
6. Gathers `CheckResult` facts
7. Runs `DoHardening` (secrets only when matching containers/volumes do not already exist)
8. Enters the Bubble Tea wizard

If the state is still dirty when you exit, the process reminds you to re-run and commit.

```mermaid
flowchart TD
  A[Parse flags -e / -v] --> B[Load or create .env]
  B --> C[State + migrations + proxy/Docker sync]
  C --> D[System checks]
  D --> E[Hardening defaults]
  E --> F[Welcome screen]
  F -->|Ready + Enter| G{EULA consent?}
  G -->|No| H[EULA screen]
  G -->|Yes| I[Main menu]
  H --> I
  I --> J[Config forms]
  J --> K[Apply changes or Install]
  K --> L[Processor: networks + compose]
  I --> M[Maintenance]
  M --> N[Start / stop / update / purge / reset password]
```

## System checks

The Welcome screen shows check results. **Enter** is accepted only when `IsReadyToContinue()` is true:

| Gate | Meaning |
| --- | --- |
| Env file exists and dir writable | Can persist configuration |
| Docker API accessible | Host Docker client works |
| Worker env API accessible | Worker Docker endpoint (local or remote) works |
| Docker Compose installed + version OK | Compose CLI usable |
| Docker version OK | Engine meets installer expectations |
| Network OK | DNS/connectivity including Docker Hub-related checks |
| CPU / memory / disk OK | Host resources vs enabled stack footprint |

The main menu also surfaces live status for Docker, PentAGI, Langfuse, and Observability.

<Warning>
If Docker Hub DNS or HTTPS is blocked, installer network checks fail even when PentAGI HTTP proxy vars are set. Configure the Docker daemon/registry mirror separately; `PROXY_URL` does not proxy image pulls.
</Warning>

## Wizard navigation

| Control | Behavior |
| --- | --- |
| Enter | Advance from Welcome when ready; select menu items; submit forms |
| Tab / Shift+Tab | Move between form fields |
| Ctrl+H | Toggle masked secrets |
| Ctrl+S | Save without always closing (where supported, e.g. password reset) |
| ESC | Return to Welcome (clears nested stack) |
| Ctrl+C | Exit |

Screens use composite IDs (`screen§arg…`) so parameterized forms (provider type, processor op) restore selection after navigation.

### Main menu

Always available configuration entries:

- **LLM Providers** — OpenAI, Anthropic, Gemini, Bedrock, Ollama, DeepSeek, GLM, Kimi, Qwen, Custom
- **Embedder** — embedding provider settings
- **Summarizer** — general and assistant context budgets
- **Tools** — AI agent limits, search engines, scraper, Graphiti, Docker sandbox
- **Monitoring** — Langfuse and Observability (embedded / external / disabled patterns)
- **Server settings** — listen host/port, public URL, CORS, proxy, timeouts, SSL paths, license key, data dirs

Conditionally shown:

| Item | When |
| --- | --- |
| **Apply changes** | Dirty env state (`IsDirty()`) |
| **Install PentAGI** | No pending dirty state and core stack not installed |
| **Maintenance** | At least one lifecycle op applies (start/stop/update/reset/etc.) |

## Configuration surfaces

Forms write through a controller into dirty state; values land in `.env` only after **Apply changes** or an operation that commits.

### LLM providers (BYOK)

Each provider form maps to the corresponding env keys (API keys, base URLs, optional config paths). At least one working provider is required for real flows after deploy; the installer does not force a single vendor.

| Provider ID | Typical keys |
| --- | --- |
| `openai` | Base URL + API key |
| `anthropic` | Base URL + API key |
| `gemini` | Base URL + API key |
| `bedrock` | Region, auth modes (default / bearer / access+secret), optional session token and base URL |
| `ollama` | Base URL, optional API key, model, config path, pull/load options |
| `deepseek` / `glm` / `kimi` / `qwen` | Base URL + API key (+ optional provider name for gateways) |
| `custom` | OpenAI-compatible `LLM_SERVER_*` style endpoint, config path, legacy reasoning flags |

Embedded example YAMLs ship with the installer (`example.custom.provider.yml`, `example.ollama.provider.yml`).

### Search engines and tools

Under **Tools → Search engines**: DuckDuckGo (enable, region, safe search, time range), Sploitus, Google CSE, Tavily, Traversaal, Perplexity, Searxng — matching the server-side search env surface.

Also under Tools: AI agent supervision settings, scraper credentials/URLs, Graphiti URL mode, Docker socket/network/image options.

### Monitoring and Graphiti

| Product | Modes | Default embedded endpoints |
| --- | --- | --- |
| Observability | Embedded when `OTEL_HOST` is the in-compose collector; else external/disabled | `otelcol:8148` |
| Langfuse | Embedded when `LANGFUSE_BASE_URL` is in-compose web; else external/disabled | `http://langfuse-web:3000` |
| Graphiti | Embedded when `GRAPHITI_URL` is in-compose; else external/disabled | `http://graphiti:8000` |

Embedded stacks pull the matching compose overlay and start containers; switching away removes containers but keeps extracted files for re-enable.

### Server settings and SSL

Server form fields include license key, listen IP/port, public URL, CORS, proxy URL/user/password, HTTP and terminal timeouts, and SSL-related paths:

- `PENTAGI_SSL_DIR` — directory with `server.crt` / `server.key` (PEM; cert may be full chain); overrides default generated cert behavior
- `EXTERNAL_SSL_CA_PATH` — custom CA inside the container (typically under `/opt/pentagi/ssl/`)
- `EXTERNAL_SSL_INSECURE` — skip TLS verify for outbound provider calls (testing only)

Default compose data includes volumes such as `pentagi-ssl` and `scraper-ssl`. Hardening covers cookie signing, DB passwords, and scraper auth used with HTTPS scraper URLs—not a separate “generate cert” menu step.

## Hardening and migrations

### Automatic secret hardening

When PentAGI / Langfuse / Graphiti are **not** already installed and their data volumes do **not** exist, the installer replaces documented defaults with random values and commits:

| Area | Examples |
| --- | --- |
| PentAGI | `COOKIE_SIGNING_SALT`, `PENTAGI_POSTGRES_PASSWORD`, scraper user/password, synced `SCRAPER_PRIVATE_URL` |
| Graphiti | `NEO4J_PASSWORD` |
| Langfuse | DB/ClickHouse/Redis/S3 secrets, salt, encryption key, NextAuth secret, project public/secret keys; signup disabled by policy |

Policies use hex, UUID-with-prefix, or alphanumeric lengths. Existing volumes are left alone so credentials stay consistent with data already on disk.

Also set: `INSTALLATION_ID` from the host installation identity; invalid `LICENSE_KEY` values are cleared after introspection.

### Path migrations

| Legacy | Migrated |
| --- | --- |
| Host `DOCKER_CERT_PATH` (real dir) | `PENTAGI_DOCKER_CERT_PATH` host path; `DOCKER_CERT_PATH` reset to container default |
| Host custom `LLM_SERVER_CONFIG_PATH` | `PENTAGI_LLM_SERVER_CONFIG_PATH` + container default path |
| Host custom `OLLAMA_SERVER_CONFIG_PATH` | `PENTAGI_OLLAMA_SERVER_CONFIG_PATH` + container default path |

## Apply changes vs install

Both operations go through the processor with an embedded terminal for compose output.

### Apply changes

Shown when state is dirty.

1. Commit dirty vars to `.env` and clear `.state` overlay  
2. Re-gather checker facts  
3. Ensure Docker networks (`pentagi-network`, `observability-network`, `langfuse-network` as needed)  
4. Phase order: Observability → Langfuse → Graphiti → PentAGI  
5. Per stack: extract/verify compose files, then update/start or remove if switched to external/disabled  

### Install

Shown when core is not installed and there is no dirty buffer.

Same network ensure + phased stack apply, but only for stacks not already installed. Prefer finishing configuration first, then Install; use Apply changes for later env edits on a live install.

## Maintenance

Menu items appear only when the corresponding checker helper is true.

| Operation | Condition (summary) |
| --- | --- |
| Start / Stop / Restart | Installed-not-running / any stack running |
| Download worker image | Worker image missing |
| Update worker / stacks / installer | Image or stack outdated; installer needs update server access |
| Factory reset | Any compose stack installed |
| Remove / Purge | Any stack installed (purge is more destructive) |
| Reset admin password | `PentagiRunning` |

### Reset admin password

Resets `admin@pentagi.com` in PostgreSQL while the stack is up.

<Steps>
  <Step title="Confirm PentAGI is running">
    Maintenance only lists **Reset Admin Password** when the core container is running and port `5432` is reachable on `127.0.0.1` with credentials from `.env` (`PENTAGI_POSTGRES_USER` / `PASSWORD` / `DB`, defaults `postgres` / `postgres` / `pentagidb`).
  </Step>
  <Step title="Enter and confirm password">
    Installer validation: non-empty, minimum **5** characters, both fields match. Passwords are masked; Ctrl+H toggles visibility.
  </Step>
  <Step title="Apply">
    Enter runs reset and returns on success; Ctrl+S runs reset and stays on the form. The processor bcrypt-hashes the password and runs  
    `UPDATE users SET password = $1, status = 'active' WHERE mail = 'admin@pentagi.com'`.
  </Step>
</Steps>

<Note>
Default first-login credentials after a fresh deploy remain `admin@pentagi.com` / `password` until changed in the UI or via this maintenance path. Prefer a strong password in production; UI registration rules may be stricter than the installer’s 5-character floor.
</Note>

## State and files on disk

| Path | Role |
| --- | --- |
| `.env` (or `-e` path) | Authoritative env after commit; mode `0600` on create |
| `.state/<env-basename>.state` | Dirty vars + navigation stack before commit |
| `.state` EULA consent marker | Skips EULA after acceptance |
| Working dir compose files | Extracted from embedded FS (`docker-compose*.yml`, provider examples) |
| Docker volumes | e.g. `pentagi-postgres-data`, `pentagi-data`, `pentagi-ssl`, `scraper-ssl` |

## Troubleshooting

| Symptom | What to check |
| --- | --- |
| Welcome blocks Enter | Docker socket permissions; compose install; disk/memory; Docker Hub DNS; env dir writability |
| “System is not ready” at startup | Same gates as Welcome; fix host before relying on the menu |
| Compose commands fail in TUI | Narrow terminals force `COMPOSE_ANSI=never`; widen if output is unreadable |
| Apply/install fails mid-stack | Read terminal pane errors; fix registry auth/mirror; re-run after partial extract |
| Password reset fails | Stack not running; Postgres not published on localhost:5432; wrong `PENTAGI_POSTGRES_*`; admin row missing |
| Secrets “reset” unexpectedly | Hardening only when volumes absent—do not delete volumes if you need stable credentials |
| Proxy works for app but not pulls | Configure Docker daemon proxy/mirror, not only `PROXY_URL` |
| Dirty state after quit | Re-run installer and **Apply changes** or discard by resetting state carefully |

Debug during development: write logs via the wizard logger to `log.json` and `tail -f log.json` in another terminal—never `fmt.Printf` inside the TUI.

## Verify a successful install

1. Welcome checks all green; EULA accepted  
2. At least one LLM provider configured and applied  
3. Install or Apply completes without processor errors  
4. Open `https://localhost:8443` (or your public URL/port)  
5. Sign in, change default admin password if still default  
6. Create a test flow and confirm provider health  

## Related pages

<CardGroup cols={2}>
  <Card title="Installation" href="/installation">
    Manual compose, SSL volumes, and optional overlays without the TUI.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    First login, change defaults, and create a flow after deploy.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    Env keys and UI profiles for each BYOK provider.
  </Card>
  <Card title="Search engines" href="/search-engines">
    Search provider env and tool gating details.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full Config and `.env.example` reference.
  </Card>
  <Card title="Observability and Langfuse" href="/observability-and-langfuse">
    Optional monitoring and LLM analytics stacks.
  </Card>
  <Card title="Knowledge graph" href="/knowledge-graph">
    Graphiti and Neo4j compose enablement.
  </Card>
  <Card title="Authentication and API tokens" href="/auth-and-api-tokens">
    Default admin account and session/API auth after install.
  </Card>
</CardGroup>

---

## 05. Flows, tasks, and subtasks

> Execution hierarchy: Flow lifecycle and StatusType states, Task objectives, Generator and Refiner subtask plans, Assistant mode, and putUserInput, stopFlow, finishFlow boundaries.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/05-flows-tasks-and-subtasks.md
- Generated: 2026-07-10T07:06:28.344Z

### Source Files

- `backend/docs/flow_execution.md`
- `backend/pkg/controller/flow.go`
- `backend/pkg/controller/task.go`
- `backend/pkg/controller/subtask.go`
- `backend/pkg/controller/assistant.go`
- `backend/pkg/graph/schema.graphqls`
- `backend/pkg/server/services/flows.go`

---
title: "Flows, tasks, and subtasks"
description: "Execution hierarchy: Flow lifecycle and StatusType states, Task objectives, Generator and Refiner subtask plans, Assistant mode, and putUserInput, stopFlow, finishFlow boundaries."
---

PentAGI runs autonomous pentests as a **Flow → Task → Subtask** hierarchy under `backend/pkg/controller`. A `FlowWorker` owns a Docker-backed session and queues user input; each user objective becomes a `TaskWorker`; the Generator and Refiner agents decompose and replan ordered `SubtaskWorker` steps that the Primary Agent executes until barrier tools `done` or `ask` fire. Shared lifecycle values use GraphQL `StatusType` and Postgres enums `FLOW_STATUS`, `TASK_STATUS`, and `SUBTASK_STATUS`.

## Execution hierarchy

| Level | Runtime type | Persisted as | Role |
| --- | --- | --- | --- |
| Flow | `FlowWorker` | `flows` | Session boundary: provider, primary container, assistants, task queue, data under the flow work dir |
| Task | `TaskWorker` | `tasks` | One user objective (`input`, LLM-derived `title`, final `result`) |
| Subtask | `SubtaskWorker` | `subtasks` | One planned step (`title`, `description`, `result`) run via a Primary Agent message chain |
| Assistant | `AssistantWorker` | `assistants` | Interactive side channel on a flow; not in the Task/Subtask stack |

```text
FlowWorker  ── owns ──► TaskController
    │                      │
    │                      ├── TaskWorker (user objective)
    │                      │      └── SubtaskController
    │                      │             ├── GenerateSubtasks  (Generator agent)
    │                      │             ├── PopSubtask → SubtaskWorker.Run
    │                      │             └── RefineSubtasks   (Refiner agent)
    │
    └── AssistantWorker[]  (optional; independent msg chain)
```

Workers coordinate status **upward**: subtask → task → flow. Completing the last task does not finish the flow; it returns the flow to `waiting` so another task can start.

## StatusType lifecycle

GraphQL and the API expose one status enum for flows, tasks, subtasks, and assistants:

```graphql
enum StatusType {
  created
  running
  waiting
  finished
  failed
}
```

Database enums match (`FLOW_STATUS`, `TASK_STATUS`, `SUBTASK_STATUS`).

```mermaid
stateDiagram-v2
  [*] --> created: row inserted
  created --> running: worker starts work
  running --> waiting: ask / stop / interrupt / await input
  waiting --> running: putUserInput or resume
  running --> finished: success / Finish
  running --> failed: unrecoverable error
  waiting --> finished: Finish (force complete)
  finished --> [*]
  failed --> [*]
```

### Status meaning by level

| Status | Flow | Task | Subtask |
| --- | --- | --- | --- |
| `created` | Row just inserted; title/model may still be filled by provider setup | Row after `CreateTask`, before run | Planned by Generator/Refiner; not yet popped |
| `running` | Active task or generation in progress; UI should treat input as busy | Subtask loop executing | Primary Agent chain in `PerformAgentChain` |
| `waiting` | Ready for new task or answer to `ask` | Paused on a waiting subtask, or ready after prior completion | Barrier `ask`, cancel, or chain error reset |
| `finished` | Session closed via `finishFlow` / `Finish` | Reporter path marked success, or forced finish | Barrier `done` success |
| `failed` | Terminal failure path on flow row | Reporter `Success=false` | `PerformResultError` |

### Status back-propagation

- **Subtask → task**: `SubtaskStatusRunning` sets task `running`; `waiting` sets task `waiting`; terminal subtask statuses do not auto-finish the task (the task loop continues or reports).
- **Task → flow**: task `running` / `waiting` map to flow `running` / `waiting`; task `finished` or `failed` sets flow to **`waiting`** (accept next input), not flow `finished`.
- **Interrupt** (`context.Canceled` / `DeadlineExceeded`): incomplete task/subtask reset to `waiting` so the session can resume.

### Load and restart rules

| Entity | Loadable statuses | Notes |
| --- | --- | --- |
| Flow | `running`, `waiting` only | Other statuses return `ErrNothingToLoad` |
| Task | not `created` | Finished/failed load as completed; waiting/running reload subtasks |
| Subtask | finished, failed, waiting; running demoted | Mid-run subtasks are forced back to `created` then reloaded; pure `created` without chains is skipped |

On process start, `FlowController.LoadFlows` restores only active flows and resumes incomplete non-waiting tasks.

## Flow lifecycle

### Create

1. GraphQL `createFlow(modelProvider, input, resourceIds?)` or REST `POST /api/v1/flows/` with `CreateFlow` (`input`, `provider`, optional `functions`, `resource_ids`).
2. `FlowController.CreateFlow` → `NewFlowWorker`: insert flow (`status=created`, title `untitled`), bind provider, build tools executor, prepare Docker primary container, start `worker()` goroutine.
3. Unless assistant-only `dryRun`, first `PutInput` runs immediately and creates the first task.

Provider choice is BYOK: any configured provider name for the user (OpenAI, Anthropic, Gemini, Bedrock, Ollama, custom, DeepSeek, GLM, Kimi, Qwen, and so on). No hosted connector is required beyond keys and optional server URLs.

### Worker input loop

`flowWorker.worker` continues incomplete tasks after load, then reads from channel `input` (`flowInputTimeout` = 1s for early error reporting).

`processInput`:

1. If any task is incomplete and `waiting` → `task.PutInput` (answers `ask`) and `runTask`.
2. Else set flow `running`, cancel-safe context for generation, `CreateTask` (includes Generator), then `execTask` / `Run`.

Only one task cancel scope is active at a time (`taskST`, `taskWG`). `Stop` cancels that scope; stop during GenerateSubtasks returns the flow to `waiting` without fatal error.

### Finish vs stop

| Operation | GraphQL | REST `PUT /flows/{id}` | Effect |
| --- | --- | --- | --- |
| Stop current work | `stopFlow(flowId)` | `{ "action": "stop" }` | Cancels task context; waits up to `stopTaskTimeout` (5s); worker stays registered; status tends to `waiting` |
| End session | `finishFlow(flowId)` | `{ "action": "finish" }` | Cancels flow, closes input channel, finishes incomplete tasks/assistants, releases executor, sets flow `finished`, removes from controller map |
| Delete | `deleteFlow` | `DELETE /flows/{id}` | Attempts `Finish` if worker exists, deletes DB row and flow-scoped memory docs |

`Stop` does **not** set `finished`. After stop, use `putUserInput` / REST `action: input` for a new or resumed objective. `Finish` is terminal for the live worker.

## Tasks

A task is one user objective inside a flow.

### Creation path

1. `TaskWorker` via `NewTaskWorker`: `Provider.GetTaskTitle` → `CreateTask` (`status=created`) → publish `taskCreated` → log `MsglogTypeInput` → **`GenerateSubtasks`** (must return at least one subtask) → publish `taskUpdated`.
2. `Run` loops: `PopSubtask` → `SubtaskWorker.Run` → unless waiting, `RefineSubtasks` → repeat until no planned subtasks or soft cap.
3. `Provider.GetTaskResult` (Reporter agent) writes `result` and sets `finished` or `failed`; message log type `report`.

### Soft cap

`providers.TasksNumberLimit = 15`. The run loop continues while `len(ListSubtasks) < TasksNumberLimit+3` (in-memory workers for completed/refined steps). Generator/Refiner prompts receive `N` as max planned slots (refiner: `max(15 - completedCount, 0)`). Empty refine plan means “objectives met; stop planning.”

### Input while waiting

`TaskWorker.PutInput` requires `IsWaiting()` and forwards input to the first incomplete waiting subtask. Calling put input when the task is running returns an error at the task layer; flow-level input while no task is waiting **creates a new task**.

## Subtasks: Generator and Refiner

`SubtaskController` owns the plan.

| Method | Agent | Behavior |
| --- | --- | --- |
| `GenerateSubtasks` | Generator | LLM plan → insert all rows as `created`; empty plan is an error |
| `RefineSubtasks` | Refiner | After each successful subtask: delete remaining `created` rows, insert new plan; empty plan = no change / done |
| `PopSubtask` | — | First planned DB row; create `SubtaskWorker` + `PrepareAgentChain` if needed |
| `ListSubtasks` / `GetSubtask` | — | In-memory workers only |

### Subtask run results

`Provider.PerformAgentChain` returns:

| Result | Status | Cause |
| --- | --- | --- |
| `PerformResultDone` | `finished` | Primary Agent called barrier `done` |
| `PerformResultWaiting` | `waiting` | Primary Agent called barrier `ask` |
| `PerformResultError` | `failed` | Unrecoverable agent/tool failure |

`PutInput` on a subtask requires waiting, injects into the msg chain via `PutInputToAgentChain`, logs input, clears waiting, then a new `Run` continues the same chain (`EnsureChainConsistency` first).

### Planned vs completed

- **Planned**: `status=created` (and queue order from DB planned query).
- **Active**: `running` or `waiting` with a live worker and msg chain.
- **Done**: `finished` / `failed` with `result` text.

Assistant tool `patch_flow_subtasks` can replace the planned list via delta ops (add/remove/modify/reorder) while the flow is not mid-run in a conflicting way; obtain `task_id` from `get_flow_status`.

## Assistant mode

Assistants attach to a **flow**, not to a task/subtask. They use a separate message chain and log stream (`AssistantLog`).

### Create and call

| GraphQL | Behavior |
| --- | --- |
| `createAssistant(flowId, modelProvider, input, useAgents, resourceIds?)` | Create assistant; may create/load flow worker with `dryRun` if needed; inject `FlowWorker` into provider |
| `callAssistant(...)` | Queue input (`assistantInputTimeout` = 2s) |
| `stopAssistant` / `deleteAssistant` | Cancel run or remove assistant |

`useAgents`:

- `true` — delegation tools (`search`, `pentester`, `coder`, `advice`, `memorist`, `maintenance`) plus direct tools.
- `false` — direct tools and search/memory only; no specialist agent delegation.

Assistants use `nil` task/subtask IDs for many agent handlers and return natural language to the user (not Primary-Agent barrier semantics).

### Flow-management tools (assistant only)

When `AssistantProvider.SetFlowWorker` is set:

| Tool | Purpose |
| --- | --- |
| `get_flow_status` | Flow/tasks/subtasks snapshot; optional verbose agent messages |
| `stop_flow` | Cancel running task (handler timeout ~15s); reports post-stop state |
| `submit_flow_input` | Deliver text when flow is `waiting` (answer `ask` or start new task); rejects when still running |
| `patch_flow_subtasks` | Rewrite planned subtasks for a task ID |

These mirror user control surfaces but run from the assistant tool loop.

## Control boundaries: putUserInput, stopFlow, finishFlow

### putUserInput / input

**GraphQL:** `putUserInput(flowId, input, modelProvider?, resourceIds?)`  
**REST:** `PUT /api/v1/flows/{flowID}` with `"action":"input"`, `"input":"..."`, optional provider and `resource_ids`.

Requires live worker (`GetFlow`); optional mid-session provider switch; copies user resources into flow FS and container when permitted.

| Flow state | Effect |
| --- | --- |
| Incomplete task `waiting` | Resume that task (answer `ask`) |
| No waiting incomplete task | Create new task from input (Generator runs first) |
| Worker finished / not in map | Error `flow not found` / not editable |

Permission: `flows.edit` (scoped by flow ownership unless admin).

### stopFlow

Cancels the **current task context** only. Does not finish assistants, does not release Docker, does not set flow `finished`. Use when the agent is looping or you need to change direction; then `putUserInput` or assistant `submit_flow_input`.

### finishFlow

Terminal session close: stop input loop, complete unfinished tasks/assistants as finished, release executor (containers), persist `FlowStatusFinished`, drop worker from controller. Further automation needs a new flow (or reload paths that only accept `running`/`waiting` will not attach).

### Permission summary

| Mutation | Privilege |
| --- | --- |
| `createFlow` | `flows.create` |
| `putUserInput`, `stopFlow`, `finishFlow`, `renameFlow` | `flows.edit` |
| `deleteFlow` | `flows.delete` |
| Assistants create/call/stop | assistant/flow edit privileges (see auth docs) |

## Human-in-the-loop: `ask`

Barrier tool `ask` is available on the Primary Agent when `ASK_USER=true` (`config.AskUser`, default **false**). Flow:

1. Agent calls `ask` → `PerformResultWaiting`.
2. Subtask/task/flow become `waiting`.
3. User answers via `putUserInput` / REST `input` / assistant `submit_flow_input`.
4. Input is applied to the agent chain; subtask `Run` continues.

With `ASK_USER=false`, agents cannot pause for user Q&A; they must complete or fail without that barrier.

## API surface

### GraphQL (primary control plane)

| Operation | Kind | Notes |
| --- | --- | --- |
| `createFlow` | Mutation | Returns `Flow` |
| `putUserInput` | Mutation | `ResultType` success/error |
| `stopFlow` / `finishFlow` / `deleteFlow` / `renameFlow` | Mutation | |
| `flow` / `flows` / `tasks(flowId)` | Query | Tasks include nested `subtasks` |
| `flowCreated` / `flowUpdated` / `taskCreated` / `taskUpdated` | Subscription | Real-time UI |

### REST under `/api/v1`

| Method | Path | Role |
| --- | --- | --- |
| `GET` | `/flows/`, `/flows/{id}`, `/flows/{id}/graph` | List, detail, tasks+subtasks graph |
| `POST` | `/flows/` | Create (`CreateFlow`) |
| `PUT` | `/flows/{id}` | Patch: `stop` \| `finish` \| `input` \| `rename` |
| `DELETE` | `/flows/{id}` | Delete |
| `GET` | `/flows/{id}/tasks/`, `.../tasks/{taskID}`, `.../graph` | Task views |
| `GET` | `/flows/{id}/subtasks/`, `.../tasks/{taskID}/subtasks/` | Subtask views |

Swagger: `/api/v1/swagger`.

### Example: create and drive a flow (GraphQL)

```graphql
mutation Create($provider: String!, $input: String!) {
  createFlow(modelProvider: $provider, input: $input) {
    id
    status
    title
  }
}

mutation Input($flowId: ID!, $input: String!) {
  putUserInput(flowId: $flowId, input: $input)
}

mutation Stop($flowId: ID!) {
  stopFlow(flowId: $flowId)
}

mutation Finish($flowId: ID!) {
  finishFlow(flowId: $flowId)
}
```

### Example: REST patch input

```json
{
  "action": "input",
  "input": "Continue with credential stuffing on the staging API only.",
  "provider": "openai"
}
```

## Timing and operational limits

| Constant / env | Value | Where |
| --- | --- | --- |
| `TasksNumberLimit` | 15 | Max planned subtask budget per task |
| `flowInputTimeout` | 1s | Early error wait after queueing flow input |
| `assistantInputTimeout` | 2s | Assistant input queue |
| `stopTaskTimeout` | 5s | `FlowWorker.Stop` wait for task WG |
| Flow mgmt tool ops | ~15s | Assistant `stop_flow` / `submit_flow_input` handlers |
| `ASK_USER` | default `false` | Enables Primary Agent `ask` barrier |

Provider/model selection is per flow (and optionally switched on later input). Specialist agent models come from provider YAML / UI profiles; see provider configuration docs.

## Troubleshooting

| Symptom | Likely cause | Action |
| --- | --- | --- |
| `putUserInput` fails with flow not found | Worker finished, never created, or process restarted without loadable status | Check flow status; only `running`/`waiting` reload; create a new flow if `finished` |
| Input ignored / creates extra task | No task in `waiting`; input starts a new objective | Use input only when status is `waiting` if you mean to answer `ask` |
| Stuck `running` | Agent loop, long terminal tool, or hung LLM | `stopFlow`, inspect toolcall/msg logs, then new input |
| `stop` timeout | Task did not exit within 5s | Retry stop; check container/terminal; finish only if abandoning session |
| No subtasks generated | Generator returned empty / provider error | Fix provider keys/models; inspect agent logs for generator chain |
| `ask` never appears | `ASK_USER` false | Set `ASK_USER=true` and recreate flow |
| Subtask restarts after process crash | Running subtask demoted to `created` on load | Expected; chain may re-prepare |
| Assistant cannot `submit_flow_input` | Flow still `running` | `stop_flow` first, then submit |

## Next

<CardGroup>
  <Card title="Agents and supervision" href="/agents-and-supervision">
    Primary, Generator, Refiner, Reporter, specialists, execution monitor, and tool-call limits.
  </Card>
  <Card title="Tools and sandbox" href="/tools-and-sandbox">
    Barrier tools done/ask, Docker terminal and file tools, timeouts, and default images.
  </Card>
  <Card title="GraphQL API" href="/graphql-api">
    Full Query, Mutation, and Subscription surface for flows, tasks, and live logs.
  </Card>
  <Card title="REST API" href="/rest-api">
    Gin routes under /api/v1 for flows, tasks, subtasks, and patch actions.
  </Card>
  <Card title="Tools reference" href="/tools-reference">
    Named registry including assistant flow-management tools and argument shapes.
  </Card>
  <Card title="Sample pentest prompts" href="/sample-pentest-prompts">
    Ready flow inputs from examples/prompts for first successful engagements.
  </Card>
</CardGroup>

---

## 06. Agents and supervision

> Specialist agents (primary, pentester, coder, installer, searcher, memorist, adviser, reflector, reporter), execution monitor, planning step, and tool-call hard limits.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/06-agents-and-supervision.md
- Generated: 2026-07-10T07:06:18.554Z

### Source Files

- `backend/docs/flow_execution.md`
- `backend/pkg/providers/performer.go`
- `backend/pkg/providers/performers.go`
- `backend/pkg/providers/provider/agents.go`
- `backend/pkg/config/config.go`
- `backend/pkg/graph/schema.graphqls`
- `README.md`

---
title: "Agents and supervision"
description: "Specialist agents (primary, pentester, coder, installer, searcher, memorist, adviser, reflector, reporter), execution monitor, planning step, and tool-call hard limits."
---

PentAGI runs each Flow subtask through a multi-agent chain on `flowProvider`: the **primary agent** orchestrates work, specialist agents execute delegated tools inside a Docker sandbox, and layered **supervision** (execution monitor, planning step, hard tool-call limits, reflector, repeating detector) bounds cost and recovers stuck loops. LLM calls are BYOK—each agent role maps to a provider config key (`primary_agent`, `pentester`, `coder`, and so on) so any wired OpenAI-compatible, cloud, or local endpoint can back that role.

## Runtime model

Workers call into `flowProvider` performers. The shared loop is `performAgentChain` in `backend/pkg/providers/performer.go`:

1. Load execution context (completed and planned subtasks).
2. Call the LLM with the agent’s tool set (`callWithRetries`).
3. Execute each tool (`execToolCall`), with optional mentor injection.
4. Stop on barrier tools (`done`, `ask`), or continue until a hard iteration limit.

Message chains are typed (`MsgchainTypePrimaryAgent`, `MsgchainTypePentester`, …), persisted in PostgreSQL, and summarized when the context window grows. Parent/current agent context (`PutAgentContext` / `GetAgentContext`) tags logs and vector-store writes with the delegation path.

```mermaid
flowchart TB
  subgraph orchestration [Task / subtask orchestration]
    GEN[Generator]
    REF[Refiner]
    PRI[Primary agent]
    REP[Reporter]
  end

  subgraph specialists [Specialists]
    PEN[Pentester]
    COD[Coder]
    INS[Installer]
    SEA[Searcher]
    MEM[Memorist]
    ADV[Adviser]
  end

  subgraph supervision [Supervision]
    MON[Execution monitor → mentor]
    PLN[Planning step → planner]
    LIM[Hard tool-call limits]
    RFL[Reflector]
    RPT[Repeating detector]
  end

  GEN --> PRI
  PRI --> PEN
  PRI --> COD
  PRI --> INS
  PRI --> SEA
  PRI --> MEM
  PRI --> ADV
  PRI --> REF
  REF --> PRI
  PRI --> REP

  PEN --> PLN
  COD --> PLN
  INS --> PLN
  PRI --> MON
  PEN --> MON
  COD --> MON
  INS --> MON
  PRI --> LIM
  PRI --> RFL
  PRI --> RPT
```

## Agent roster

GraphQL exposes runtime agent identity as `AgentType` (`backend/pkg/graph/schema.graphqls`). Provider YAML and UI test paths use `AgentConfigType` / `ProviderOptionsType` keys in `backend/pkg/providers/pconfig`.

| Role | Config key / options type | Performer / entry | Typical job | Result / barrier tools |
| --- | --- | --- | --- | --- |
| Primary | `primary_agent` | Primary chain in `provider.go` | Orchestrate a subtask; delegate specialists; finish or ask user | Delegation tools + `done` / `ask` |
| Pentester | `pentester` | `performPentester` | Vulnerability assessment, exploit paths | `hack_result` |
| Coder | `coder` | `performCoder` | Scripts, exploits, tooling code | `code_result` |
| Installer | `installer` | `performInstaller` | Env setup, package/tool install | `maintenance_result` |
| Searcher | `searcher` | `performSearcher` | Network + memory research | `search_result` |
| Memorist | chain type `memorist` (LLM opts reuse `searcher`) | `performMemorist` | Long-term / execution memory retrieval | `memorist_result` |
| Adviser | `adviser` | via `advice` / mentor / planner | Expert guidance; also mentor and planner modes | Natural answer through `advice` handler |
| Reflector | `reflector` | `performReflector` | Correct free-text answers into tool use | Guidance as user-style message |
| Reporter | uses `simple` options; chain type `reporter` | `performTaskResultReporter` | Final task report | `report_result` |
| Generator | `generator` | `performSubtasksGenerator` | Decompose task → subtask list (cap 15) | `subtask_list` |
| Refiner | `refiner` | `performSubtasksRefiner` | Patch planned subtasks after each run | `subtask_patch` |
| Enricher | `enricher` | `performEnricher` | Context enrichment for advice/planning | `enricher_result` |
| Assistant | `assistant` | Assistant provider | Interactive chat; optional agent tools + flow control | Text to user; optional flow tools |
| Tool-call fixer | (internal) | arg repair path | Fix invalid JSON tool arguments | Regenerated args |
| Summarizer | (internal / config) | chain summarizer | Shrink long chains | Summary messages |

### Primary agent

Runs once per active subtask with a `PrimaryExecutor` that wires:

- **Delegation tools**: `pentester`, `coder`, `maintenance` (installer), `search`, `memorist`, `advice`
- **Environment tools**: terminal, file, browser (and network search when enabled)
- **Barriers**: `done` (`FinalyToolName`), `ask` (gated by `ASK_USER`)

Barrier outcomes map to `PerformResultDone`, `PerformResultWaiting`, or `PerformResultError`.

### Specialists

Specialists receive a focused prompt and a restricted executor. Planning (when enabled) applies only to **pentester**, **coder**, and **installer** before `performAgentChain` starts. Searcher and memorist stay on the limited tool-call budget and do not run the planning step.

### Meta agents: generator, refiner, reporter

| Agent | When | Output |
| --- | --- | --- |
| Generator | New task | Ordered subtasks (max 15) |
| Refiner | After each finished subtask | Patch of remaining plan (add/remove/modify/reorder); empty list when task complete |
| Reporter | Task completion | Structured final report |

### Reflector

Invoked when a non-assistant agent returns content **without** tool calls, or when `callWithRetries` exhausts LLM attempts (`maxRetriesToCallAgentChain = 3`). Caps at `maxReflectorCallsPerChain` (3). Guides the agent toward structured tools or barriers. Assistant agents are exempt: free-text is the user-facing answer.

### Adviser as mentor and planner

The same `advice` tool / adviser handler is reused with different observation names and templates:

| Mode | Trigger | Template | Effect |
| --- | --- | --- | --- |
| Explicit advice | Agent calls `advice` | Adviser system/question prompts | Optional consultation |
| Mentor | Execution monitor thresholds | `question_execution_monitor` | Injects `<mentor_analysis>` next to tool output |
| Planner | `AGENT_PLANNING_STEP_ENABLED` on pentester/coder/installer start | `question_task_planner` + `task_assignment` wrapper | Prepends a 3–7 step plan to the human message |

## Supervision layers

Hard limits and reflector are always on. Execution monitoring and planning are **opt-in** (default off).

### Execution monitor (mentor)

`executionMonitor` in `backend/pkg/providers/helpers.go` tracks tool calls inside each `performAgentChain` run:

| Condition | Default | Env |
| --- | --- | --- |
| Same tool name consecutively | 5 | `EXECUTION_MONITOR_SAME_TOOL_LIMIT` |
| Total tool calls in the chain | 10 | `EXECUTION_MONITOR_TOTAL_TOOL_LIMIT` |
| Feature gate | `false` | `EXECUTION_MONITOR_ENABLED` |

When a threshold hits **and** the executor has `advice`, `performMentor` runs the adviser as `"mentor"`. On success, counters reset and the tool response becomes:

```xml
<enhanced_response>
<original_result>
…tool output…
</original_result>

<mentor_analysis>
…progress review…
</mentor_analysis>
</enhanced_response>
```

Mentor failure is non-fatal: the original tool result is returned and the chain continues.

### Planning step

When `AGENT_PLANNING_STEP_ENABLED=true`, `performPlanner` runs before pentester/coder/installer chains. Failure logs a warning and proceeds without a plan. The planner wraps the original assignment:

```xml
<task_assignment>
  <original_request>…</original_request>
  <execution_plan>
  1. …
  2. …
  </execution_plan>
  <instructions>
  Follow the execution plan… You may deviate if you discover better approaches.
  </instructions>
</task_assignment>
```

### Hard tool-call limits

Always enforced in `performAgentChain`:

| Cohort | Agents | Default max iterations | Env |
| --- | --- | --- | --- |
| General | `assistant`, `primary_agent`, `pentester`, `coder`, `installer` | 100 | `MAX_GENERAL_AGENT_TOOL_CALLS` |
| Limited | all other options types (searcher, enricher, generator, refiner, adviser, reflector, simple, …) | 20 | `MAX_LIMITED_AGENT_TOOL_CALLS` |

Behavior:

- Within the last `maxAgentShutdownIterations` (3) of the limit, the loop injects a synthetic “approaching iteration limit” path and drives reflector-style graceful exit (barrier tools).
- After the hard ceiling, the chain errors: `agent chain exceeded maximum iterations (N)`.
- Config values ≤ 0 fall back to built-in defaults (100 / 20). Positive overrides are floored at `maxAgentShutdownIterations * 2` so shutdown guidance still has room.

### Repeating detector

Independent of the mentor: after `RepeatingToolCallThreshold` (3) identical tool calls (args normalized), further repeats get a soft “please try another tool” response. Escalation aborts the chain at threshold + `maxSoftDetectionsBeforeAbort` (4), i.e. 7 consecutive identical calls.

### Retry and fixer stack

| Layer | Limit | Behavior |
| --- | --- | --- |
| LLM chain call | 3 retries, 5s delay | Then `performCallerReflector` |
| Tool execution | 3 retries | Invalid args → tool-call fixer schema repair |
| Reflector depth | 3 per chain | Then hard fail |
| Barrier tools | `done` / `ask` | End or wait for user |

## Configuration reference

Set in `.env` (see `.env.example`) and passed through `docker-compose.yml` into the pentagi service. Parsed on `config.Config` in `backend/pkg/config/config.go`.

<ParamField body="EXECUTION_MONITOR_ENABLED" type="bool" default="false">
Enable automatic mentor reviews on tool-call pattern thresholds.
</ParamField>

<ParamField body="EXECUTION_MONITOR_SAME_TOOL_LIMIT" type="int" default="5">
Consecutive same-tool calls before mentor invocation.
</ParamField>

<ParamField body="EXECUTION_MONITOR_TOTAL_TOOL_LIMIT" type="int" default="10">
Total tool calls in an agent chain before mentor invocation.
</ParamField>

<ParamField body="MAX_GENERAL_AGENT_TOOL_CALLS" type="int" default="100">
Hard iteration ceiling for primary, assistant, pentester, coder, installer.
</ParamField>

<ParamField body="MAX_LIMITED_AGENT_TOOL_CALLS" type="int" default="20">
Hard iteration ceiling for limited/meta agents.
</ParamField>

<ParamField body="AGENT_PLANNING_STEP_ENABLED" type="bool" default="false">
Run planner (adviser) before pentester, coder, and installer specialist runs.
</ParamField>

Related always-on knobs that interact with agents:

| Variable | Role |
| --- | --- |
| `ASK_USER` | Exposes `ask` barrier on primary |
| Provider keys / `LLM_SERVER_*` | BYOK models per agent config key |
| Summarizer `*_SUMMARIZER_*` | Chain compression during long agent runs |
| `TERMINAL_TOOL_TIMEOUT` | Cap sandbox command runtime |

### Enable supervision for smaller models

<Steps>
  <Step title="Configure per-agent models">
    Point each agent key in provider YAML or the Settings UI at your BYOK endpoint. Prefer a stronger or higher-reasoning profile for `adviser` if the same base model backs primary and specialists.
  </Step>
  <Step title="Turn on monitoring and planning">
    ```bash
    EXECUTION_MONITOR_ENABLED=true
    EXECUTION_MONITOR_SAME_TOOL_LIMIT=5
    EXECUTION_MONITOR_TOTAL_TOOL_LIMIT=10
    AGENT_PLANNING_STEP_ENABLED=true
    ```
  </Step>
  <Step title="Keep hard limits realistic">
    Leave defaults (100 / 20) unless you need tighter cost caps; do not set limits below the graceful-shutdown window.
  </Step>
  <Step title="Verify">
    Restart the stack, start a Flow, and watch agent logs / tool-call logs for mentor wraps (`mentor_analysis`) and planner-prefixed human messages. Use provider test endpoints for `primary_agent`, `pentester`, and `adviser` before long runs.
  </Step>
</Steps>

<Warning>
Execution monitoring and planning increase token use and wall time (mentor and planner each call the adviser). They are recommended for weaker or sub-32B local models; large commercial models often run well with defaults (`false` / hard limits only).
</Warning>

## Agent ↔ tool access (summary)

| Agent | Can call specialists? | Typical tools |
| --- | --- | --- |
| Primary | Yes: pentester, coder, installer, search, memorist, advice | terminal, file, browser, barriers |
| Pentester | coder, installer, search, memorist, advice | terminal, file, browser, guides |
| Coder | installer, search, memorist, advice | terminal, file, code store/search |
| Installer | search, memorist, advice | terminal, file, browser, guides |
| Searcher | memorist | search engines, browser, memory |
| Memorist | — | memory / vector tools |
| Adviser | enricher → searcher/memorist | advice path |
| Assistant (`UseAgents=true`) | same specialists as primary (no task barriers by default) | + optional flow management tools when `FlowWorker` is injected |

Full tool schemas and sandbox images: tools reference and sandbox docs.

## Failure modes

| Symptom | Likely cause | Mitigation |
| --- | --- | --- |
| `agent chain exceeded maximum iterations` | Hit hard limit without `done`/`ask` | Raise `MAX_*` carefully; enable monitor/planning; check stuck loops in toolcall logs |
| Mentor never appears | Monitor off or no `advice` on executor | Set `EXECUTION_MONITOR_ENABLED=true`; ensure agent has adviser handler |
| Plan missing on specialist | Planning off or planner error | Enable `AGENT_PLANNING_STEP_ENABLED`; check logs for “proceeding without plan” |
| Free-text loop / reflector storms | Model not emitting tools | Stronger model for that agent; check tool-call ID support; max 3 reflector attempts then fail |
| Identical tool spam then abort | Repeating detector | Agent must change tools/args; mentor may redirect if monitor on |
| High cost with supervision | Mentor/planner on every threshold | Raise thresholds or disable monitor on large models |

## Observability hooks

- **Agent logs**: initiator → executor (`AgentType`) for each delegation
- **Tool call logs**: received / running / finished / failed
- **Message logs**: thinking, content, barriers (`done`, `ask`, `advice`)
- **Langfuse / OTEL**: agent spans named for primary, mentor (`performMentor`), planner (`performPlanner`), reflector
- **GraphQL**: real-time subscriptions on agent, message, and tool-call logs; usage aggregates by agent type

## Implementation map

:::files
backend/pkg/providers/
  performer.go      # performAgentChain, execToolCall, reflector, retries
  performers.go     # specialists, planner, mentor, generator, refiner, reporter
  handlers.go       # Get*Handler closures for agent tools
  provider.go       # primary subtask entry, PrimaryExecutor barriers
  helpers.go        # executionMonitor, repeatingDetector, response wrap
  pconfig/config.go # ProviderOptionsType / AllAgentTypes
backend/pkg/config/config.go
backend/pkg/tools/registry.go
backend/pkg/graph/schema.graphqls  # AgentType, AgentConfigType
backend/docs/flow_execution.md
:::

## Next

<CardGroup cols={2}>
  <Card title="Flows, tasks, and subtasks" href="/flows-tasks-subtasks">
    Lifecycle states, generator/refiner plans, assistant boundaries, and user input barriers.
  </Card>
  <Card title="Tools and sandbox execution" href="/tools-and-sandbox">
    Tool categories, Docker isolation, timeouts, and barrier tools `done` / `ask`.
  </Card>
  <Card title="Tools reference" href="/tools-reference">
    Named tools and argument shapes for delegation, results, and flow control.
  </Card>
  <Card title="Provider configuration schema" href="/provider-config-schema">
    Per-agent model, temperature, reasoning, and UI testAgent/testProvider.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full Config struct defaults including supervision keys.
  </Card>
  <Card title="Deploy with vLLM and Qwen" href="/vllm-qwen-deployment">
    Local sub-32B setups where execution monitor and planning are strongly recommended.
  </Card>
</CardGroup>

---

## 07. Tools and sandbox execution

> Tool categories, Docker-isolated terminal and file tools, network search tools, barrier tools done and ask, timeouts, and default images for general vs pentest work.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/07-tools-and-sandbox-execution.md
- Generated: 2026-07-10T07:05:59.096Z

### Source Files

- `backend/pkg/tools/registry.go`
- `backend/pkg/tools/executor.go`
- `backend/pkg/tools/terminal.go`
- `backend/pkg/docker/client.go`
- `backend/docs/docker.md`
- `backend/pkg/config/config.go`
- `backend/docs/flow_execution.md`

---
title: "Tools and sandbox execution"
description: "Tool categories, Docker-isolated terminal and file tools, network search tools, barrier tools done and ask, timeouts, and default images for general vs pentest work."
---

Agents act through a named tool registry (`backend/pkg/tools`) executed by a per-flow `customExecutor`. Environment tools (`terminal`, `file`) run inside a primary Docker container named `pentagi-terminal-<flowID>`; network search and memory tools call host-side integrations when configured; barrier tools (`done`, `ask`) stop the agent loop with a success or wait outcome.

## Runtime architecture

```mermaid
flowchart TB
  subgraph agents [Agent executors]
    PA[Primary]
    PT[Pentester / Coder / Installer]
    SR[Searcher]
  end

  subgraph tools_pkg [backend/pkg/tools]
    REG[registry definitions]
    CE[customExecutor.Execute]
    TERM[terminal + file handlers]
    NET[search_network handlers]
    BAR[barrier handlers]
  end

  subgraph docker_pkg [backend/pkg/docker]
    DC[DockerClient.RunContainer]
    EXEC[ContainerExecCreate / Attach]
  end

  subgraph sandbox [Primary container]
    WORK["/work volume"]
    SH["sh -c command"]
  end

  PA --> CE
  PT --> CE
  SR --> CE
  REG --> CE
  CE --> TERM
  CE --> NET
  CE --> BAR
  TERM --> EXEC
  CE --> DC
  DC --> sandbox
  EXEC --> SH
  WORK --- SH
```

On flow prepare, `flowToolsExecutor.Prepare` reuses a running primary container or creates one with the flow-selected image, `NET_RAW` (and optional `NET_ADMIN`), and a bind/volume at `/work`. User uploads and resources are synced into `/work/uploads` and `/work/resources`.

## Tool categories

`GetToolType` maps every registry name to a `ToolType`. The executor uses the type for Langfuse observation kind and for barrier detection.

| `ToolType` | String | Tools |
|---|---|---|
| `EnvironmentToolType` | `environment` | `terminal`, `file`, assistant flow-control (`get_flow_status`, `stop_flow`, `submit_flow_input`, `patch_flow_subtasks`, `wait_flow_completion`) |
| `SearchNetworkToolType` | `search_network` | `browser`, `google`, `duckduckgo`, `tavily`, `traversaal`, `perplexity`, `searxng`, `sploitus` |
| `SearchVectorDbToolType` | `search_vector_db` | `search_in_memory`, `search_guide`, `search_answer`, `search_code`, `graphiti_search` |
| `StoreVectorDbToolType` | `store_vector_db` | `store_guide`, `store_answer`, `store_code` |
| `AgentToolType` | `agent` | `search`, `maintenance`, `coder`, `pentester`, `advice`, `memorist` |
| `StoreAgentResultToolType` | `store_agent_result` | `search_result`, `maintenance_result`, `code_result`, `hack_result`, `memorist_result`, `enricher_result`, `report_result`, `subtask_list`, `subtask_patch` |
| `BarrierToolType` | `barrier` | `done`, `ask` |

Optional tools are registered only when `IsAvailable()` is true (API keys, scraper URLs, embedder, Graphiti client, and related flags).

## Which agents get which tools

Executors are built in `flowToolsExecutor` with different definition sets:

| Executor | Direct sandbox | Network search | Barriers / terminal results |
|---|---|---|---|
| **Primary** | No `terminal`/`file` | Via `search` agent tool | `done` always; `ask` if `ASK_USER=true` |
| **Installer** | `terminal`, `file` | `browser` (+ guides) if available | `maintenance_result` |
| **Coder** | `terminal`, `file` | optional `browser` | `code_result` |
| **Pentester** | `terminal`, `file` | optional `browser`, `sploitus` | `hack_result` |
| **Searcher** | No sandbox | Engines + `browser` when configured | `search_result` |
| **Assistant** | `terminal`, `file` | optional `browser`; optional agent tools if `UseAgents` | Flow-management tools when handlers injected |

Primary orchestrates; specialists hold the Docker shell and search engines.

## Docker sandbox

### Primary container

- **Name:** `pentagi-terminal-<flowID>` (`PrimaryTerminalNamePrefix`)
- **Type:** `ContainerTypePrimary`
- **Entrypoint:** `tail -f /dev/null` (idle shell host for `docker exec`)
- **Working directory:** `/work` (`WorkFolderPathInContainer`)
- **Capabilities:** always `NET_RAW`; add `NET_ADMIN` when `DOCKER_NET_ADMIN=true`
- **Data:** host path under `DATA_DIR/flow-<id>` (or a named volume) bind-mounted to `/work`
- **Ports (bridge mode):** two TCP ports per flow from base `28000` via `GetPrimaryContainerPorts(flowID)`
- **DinD:** when `DOCKER_INSIDE=true`, the host Docker socket is mounted into the container

### Image selection and fallbacks

| Config key | Default | Role |
|---|---|---|
| `DOCKER_DEFAULT_IMAGE` | `debian:latest` | General default / pull-create fallback (`GetDefaultImage`) |
| `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` | `vxcontrol/kali-linux` | Prompted to the image chooser as the pentest baseline |

Flow start selects an image (prompt type `image_chooser`, with both defaults in template context). `RunContainer` pulls the chosen image; on pull or create failure it falls back to `DOCKER_DEFAULT_IMAGE` and retries.

### Related Docker settings

| Variable | Default | Effect |
|---|---|---|
| `DOCKER_HOST` | From Docker env / socket | Daemon connection |
| `DOCKER_SOCKET` | Auto-detected or `/var/run/docker.sock` | Socket path when `DOCKER_INSIDE` |
| `DOCKER_NETWORK` | empty | Custom bridge name, or `host` for host network mode |
| `DOCKER_PUBLIC_IP` | `0.0.0.0` | Host IP for published ports |
| `DOCKER_WORK_DIR` | empty | Override host work root for binds |
| `DATA_DIR` | `./data` | Local flow file root |

## Environment tools

### `terminal`

Runs one blocking (or detached) shell command in the primary container via `ContainerExecCreate` / `Attach`:

```text
sh -c <input>
```

| Argument | Required | Behavior |
|---|---|---|
| `input` | yes | Command string |
| `cwd` | yes | Working directory; empty defaults to `/work` |
| `detach` | yes | `true` for interactive/long-running (listeners, shells, servers); returns quickly, little or no captured output |
| `timeout` | yes | Seconds; see [Timeouts](#timeouts) |
| `message` | yes | Engagement-log commentary |

Only one command is executed per call. Stdin/stdout are logged through `TermLogProvider`. Handler errors are swallowed into a result string so the agent loop can continue.

### `file`

| `action` | Behavior |
|---|---|
| `read_file` | `CopyFromContainer` + tar extract; max file size **100 MB** |
| `write_file` | Tar stream + `CopyToContainer`; mode `0600` |

`path` must be absolute inside the container. User materials appear under `/work/uploads` and `/work/resources` after prepare-time sync.

## Network search tools

Registered on Searcher (and partially on other agents) when available:

| Tool | Typical gate |
|---|---|
| `browser` | `SCRAPER_PRIVATE_URL` / `SCRAPER_PUBLIC_URL` |
| `google` | Google CSE credentials |
| `duckduckgo` | `DUCKDUCKGO_ENABLED` (default true) |
| `tavily` | `TAVILY_API_KEY` |
| `traversaal` | Traversaal key |
| `perplexity` | Perplexity key |
| `searxng` | Searxng URL |
| `sploitus` | Always constructible (HTTP exploit search) |

Shared search args: `query` (prefer English for indexed web), `max_results` (1–10), `message`. External HTTP calls also respect proxy and global client timeout settings. Full enablement matrix: [Search engines](/search-engines).

## Barrier tools

Barriers end the agent tool-call loop. `IsBarrierFunction` checks the executor’s barrier set; Primary wires:

| Tool | When registered | Payload | Effect |
|---|---|---|---|
| `done` | Always on Primary | `success`, `result`, `message` | Subtask finished (`PerformResultDone`) |
| `ask` | Only if `ASK_USER=true` | `message` (question) | Wait for user input (`PerformResultWaiting`) |

Specialist “result” tools (`hack_result`, `code_result`, `search_result`, …) are barriers for those executors and return control to the caller.

```text
ASK_USER=false  →  Primary barriers: { done }
ASK_USER=true   →  Primary barriers: { done, ask }
```

Resume after `ask`: user input via UI / API, or Assistant `submit_flow_input` when flow-manager handlers are injected.

## Timeouts

### Terminal execution

| Constant / config | Value | Meaning |
|---|---|---|
| `TERMINAL_TOOL_TIMEOUT` | **1200** s default | Server default for `terminal` |
| Explicit max | **10800** s (3 h) | Upper bound for agent-requested timeouts |
| Extra slack | **+5** s | Added around configured/requested limits for cleanup |
| Detach quick-check | **500** ms | How long detach mode waits before returning “still running” |

Rules implemented in `terminal.normalizeExecTimeout` / `configuredExecTimeout`:

1. Configured default ≤ 0 or > 3 h is treated as **3 h** (commands are never unbounded).
2. Agent `timeout` of `0`, negative, or above the allowed ceiling falls back to the configured default (+ extra).
3. Positive values within range are used (+ extra).
4. On timeout, the handler returns partial output and hints: use `detach=true` for interactive work, or wrap long batch jobs with the shell `timeout` utility.

### Other tool timeouts

| Surface | Bound |
|---|---|
| Result summarization | Large `terminal` / `browser` results over **16 KB** may be summarized or truncated |
| `wait_flow_completion` | Default **60** s; max **3600** s |
| Global HTTP client | Configured separately for LLM and search providers |

## Execution path (single tool call)

```text
LLM tool_call
  → customExecutor.Execute
      → log message + toolcall row
      → handler (terminal / search / barrier / …)
      → optional summarize (terminal, browser)
      → optional storeToolResult for allow-listed tools
      → update msg + toolcall success/failure
  → if IsBarrierFunction → stop agent loop
```

Allow-listed for long-term memory storage include `terminal`, `file`, major search engines, and several agent tools.

## Failure modes

| Symptom | Likely cause |
|---|---|
| `terminal is not available` / container not operational | Primary container missing, stopped, or prepare failed |
| Pull/create falls back to `debian:latest` | Chosen or pentest image unavailable; check registry and disk |
| Network tools missing from tool list | `IsAvailable()` false — keys/URLs not set |
| `ask` never offered | `ASK_USER` left at default `false` |
| Advanced nmap / raw networking weak | `DOCKER_NET_ADMIN=false` (only `NET_RAW`) |
| Host network isolation reduced | `DOCKER_NETWORK=host` |
| Command timeout with partial output | Timeout too low for batch job; use shell `timeout` or `detach=true` |
| File read error on large artifact | File exceeds **100 MB** read limit |
| Missing `/work/uploads` content | Prepare sync failed or files added after last prepare without re-sync |

## Configuration quick reference

<ParamField body="ASK_USER" type="boolean">
Enable Primary `ask` barrier. Default `false`.
</ParamField>

<ParamField body="TERMINAL_TOOL_TIMEOUT" type="integer">
Default terminal timeout in seconds. Default `1200`.
</ParamField>

<ParamField body="DOCKER_DEFAULT_IMAGE" type="string">
Fallback / general image. Default `debian:latest`.
</ParamField>

<ParamField body="DOCKER_DEFAULT_IMAGE_FOR_PENTEST" type="string">
Pentest baseline image. Default `vxcontrol/kali-linux`.
</ParamField>

<ParamField body="DOCKER_NET_ADMIN" type="boolean">
Grant `NET_ADMIN` on the primary container. Default `false`.
</ParamField>

<ParamField body="DOCKER_INSIDE" type="boolean">
Mount Docker socket for Docker-in-Docker style management. Default `false`.
</ParamField>

## Next

<CardGroup>
  <Card title="Tools reference" href="/tools-reference">
    Full named registry, argument shapes, and assistant flow-control tools.
  </Card>
  <Card title="Docker sandbox and workers" href="/docker-sandbox-workers">
    Socket, network, multi-host worker nodes, and custom images.
  </Card>
  <Card title="Search engines" href="/search-engines">
    Keys, scraper URLs, proxy, and which network tools light up.
  </Card>
  <Card title="Agents and supervision" href="/agents-and-supervision">
    Specialist roles, monitoring, and tool-call hard limits.
  </Card>
  <Card title="Memory and knowledge" href="/memory-and-knowledge">
    Vector memory tools, `/work` files, and Graphiti search.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full Config struct and .env defaults for Docker and tools.
  </Card>
</CardGroup>

---

## 08. Memory and knowledge

> pgvector memory tools, embeddings config, chain summarizer budgets, user knowledge documents API, flow files and resources under /work, and optional Graphiti graph search.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/08-memory-and-knowledge.md
- Generated: 2026-07-10T07:06:39.884Z

### Source Files

- `backend/pkg/tools/memory.go`
- `backend/pkg/providers/embeddings/embedder.go`
- `backend/pkg/csum/chain_summary.go`
- `backend/pkg/server/services/knowledge.go`
- `backend/pkg/graphiti/client.go`
- `backend/pkg/resources/resources.go`
- `backend/pkg/flowfiles/files.go`

---
title: "Memory and knowledge"
description: "pgvector memory tools, embeddings config, chain summarizer budgets, user knowledge documents API, flow files and resources under /work, and optional Graphiti graph search."
---

PentAGI persists engagement knowledge in PostgreSQL with **pgvector** (collection `langchain`), keeps agent message chains within byte budgets via the **chain summarizer** (`csum`), exposes curated **answer / guide / code** documents through REST and GraphQL, mounts flow **uploads** and **resources** under the sandbox path `/work`, and optionally records episodic memory in **Graphiti** for `graphiti_search`. Embeddings and Graphiti are provider-neutral BYOK/BYOC surfaces: configure any supported embedding backend and your own Graphiti/Neo4j stack without locking to a single cloud vendor.

## Architecture

```mermaid
flowchart TB
  subgraph agents [Agent runtime]
    Tools["Tool handlers<br/>search_in_memory / store_* / graphiti_search"]
    Memorist["memorist agent"]
    Executor["customExecutor.storeToolResult"]
    Csum["csum.SummarizeChain"]
  end

  subgraph api [API /api/v1]
    REST["REST KnowledgeService"]
    GQL["GraphQL knowledge*"]
    Files["Flow files + resources APIs"]
  end

  subgraph storage [Persistence]
    PG[("PostgreSQL + pgvector<br/>langchain_pg_embedding")]
    DataDir["DATA_DIR<br/>flow-N / flow-N-data<br/>resources/*.blob"]
    Neo[("Optional Neo4j via Graphiti")]
  end

  subgraph embed [Embeddings]
    Emb["embeddings.New<br/>EMBEDDING_PROVIDER"]
  end

  Tools --> PG
  Memorist --> Tools
  Executor --> PG
  REST --> PG
  GQL --> PG
  Emb --> PG
  Tools --> Neo
  Files --> DataDir
  DataDir -->|"CopyToContainer /work"| Sandbox["Docker sandbox WorkingDir=/work"]
  Csum -->|"LLM summarize handler"| Chain["Message chain in context"]
```

| Surface | Backing store | Agent tools | User/API surface |
|---|---|---|---|
| Ephemeral flow memory (`doc_type=memory`) | pgvector | `search_in_memory`; auto-store from selected tools | Vecstore logs only |
| Curated knowledge (`answer`, `guide`, `code`) | pgvector | `search_*` / `store_*` | REST `/api/v1/knowledge`, GraphQL `knowledge*` |
| Chain context compression | In-memory chain rewrite | Implicit during agent turns | `SUMMARIZER_*` / `ASSISTANT_SUMMARIZER_*` env |
| Flow files | Host `flow-{id}-data/` → container `/work` | `file`, `terminal` | Flow file APIs; prompt `<task_files>` listing |
| Global resources | Host `resources/{md5}.blob` | Copied into flow `/work/resources` | Resource upload/list APIs |
| Temporal knowledge graph | Graphiti + Neo4j | `graphiti_search` | Compose overlay + `GRAPHITI_*` |

## Embeddings

Vector store and knowledge CRUD require a configured embedder. `embeddings.New` selects a constructor from `EMBEDDING_PROVIDER` and wraps it with strip-newlines and batch options.

| Provider value | Default model (when `EMBEDDING_MODEL` empty) | Notes |
|---|---|---|
| `openai` (default) | `text-embedding-ada-002` | Falls back to `OPENAI_KEY` / `OPENAI_SERVER_URL` if embedding-specific key/URL unset |
| `ollama` | (model required via env) | Uses `EMBEDDING_URL`; key not used |
| `mistral` | `mistral-embed` | Model override not supported in client |
| `jina` | `jina-embeddings-v2-small-en` | |
| `huggingface` | `BAAI/bge-small-en-v1.5` | |
| `googleai` | `embedding-001` | `EMBEDDING_URL` not used |
| `voyageai` | `voyage-4` | `EMBEDDING_URL` not used |
| `none` | — | Embedder unavailable; create/search return errors / HTTP 503 |

<ParamField body="EMBEDDING_PROVIDER" type="string" default="openai">
Backend selector: `openai`, `ollama`, `mistral`, `jina`, `huggingface`, `googleai`, `voyageai`, or `none`.
</ParamField>

<ParamField body="EMBEDDING_URL" type="string">
Optional base URL override for providers that support custom endpoints.
</ParamField>

<ParamField body="EMBEDDING_KEY" type="string">
API key for the embedding backend (OpenAI may use `OPENAI_KEY` instead).
</ParamField>

<ParamField body="EMBEDDING_MODEL" type="string">
Model name; provider-specific defaults apply when empty.
</ParamField>

<ParamField body="EMBEDDING_BATCH_SIZE" type="int" default="512">
Batch size passed to the embedder wrapper.
</ParamField>

<ParamField body="EMBEDDING_STRIP_NEW_LINES" type="bool" default="true">
Strip newlines before embedding.
</ParamField>

<ParamField body="EMBEDDING_MAX_TEXT_BYTES" type="int" default="8192">
Max bytes sent to the embedding model on knowledge create/update; full document text is still stored in the DB.
</ParamField>

pgvector store construction uses collection name `langchain` and either the shared `PgxPool` or `DATABASE_URL`.

<Warning>
Without a working embedder, list/get/delete of knowledge documents can still work from SQL, but create, update (re-embed), and semantic search return “embedding provider is not configured/available” (REST maps this to **503**). Agent vector tools that need the store report as unavailable or fail at handle time.
</Warning>

## pgvector memory tools

### Automatic memory write

After successful tool execution, `customExecutor.storeToolResult` may index results into pgvector when the tool is in `allowedStoringInMemoryTools`:

`terminal`, `file`, `search`, `google`, `duckduckgo`, `tavily`, `traversaal`, `perplexity`, `searxng`, `sploitus`, `maintenance`, `coder`, `pentester`, `advice`

Write path:

1. Format args (JSON) + result as markdown.
2. Split with recursive character splitter (chunk size **2000**, overlap **100**, code blocks and heading hierarchy on).
3. Attach metadata: `user_id`, `flow_id`, optional `task_id` / `subtask_id`, `tool_name`, `tool_description`, `doc_type=memory`, `part_size`, `total_size`.
4. `store.AddDocuments`.

### `search_in_memory`

| Constant | Value |
|---|---|
| Score threshold | `0.2` |
| Result limit (post-merge) | `3` |
| Filter `doc_type` | `memory` |
| Always filter | `flow_id` (current flow) |
| Optional hard filters | `task_id`, `subtask_id` |

**Arguments (`SearchInMemoryAction`):**

| Field | Required | Constraints |
|---|---|---|
| `questions` | yes | 1–5 English semantic queries |
| `task_id` | no | Integer hard filter |
| `subtask_id` | no | Integer hard filter |
| `message` | yes | Engagement-log commentary |

Behavior:

- Runs similarity search per question; continues if one query fails.
- If task/subtask filters yield zero hits, **falls back** to flow-level filters (drops task/subtask).
- Merges, deduplicates, sorts by score, caps at 3 documents.
- Empty result returns: `nothing found in memory store by this question` (not an error).
- Response markdown includes match score, task/subtask IDs, tool name/description, and content.

### Curated knowledge tools (guide / answer / code)

Same similarity constants (**threshold 0.2**, **limit 3**, multi-query merge). Document types and hard filters:

| Tool | Direction | `doc_type` | Required type filter |
|---|---|---|---|
| `search_guide` / `store_guide` | R/W | `guide` | `type`: `install`, `configure`, `use`, `pentest`, `development`, `other` |
| `search_answer` / `store_answer` | R/W | `answer` | `type`: `guide`, `vulnerability`, `code`, `tool`, `other` |
| `search_code` / `store_code` | R/W | `code` | `lang` (markdown language name) |

Store tools expect English content for the shared English index, require co-indexed `question` text, and instruct agents to anonymize IPs, domains, credentials, and paths. Empty guide search returns a message telling the agent to store a guide after solving the case.

### Memorist

`memorist` is an **agent** tool that delegates complex long-term retrieval; with assistant agents disabled, prompts expose direct `search_in_memory` / `search_guide` / `search_answer` / `search_code` instead.

<Info>
Technical-channel fields (`questions`, `question`, store payloads) are specified as **English-only** so multi-engagement retrieval stays aligned with the English-indexed store. Engagement-log `message` fields use the engagement language.
</Info>

## Chain summarizer budgets

`csum.SummarizeChain` rewrites `[]llms.MessageContent` via `ChainAST` before context overflow. Strategies run in order:

1. **Section collapse** — all sections except the last `KeepQASections` become a single completion/summarization body pair.
2. **Last-section preserve** (if `PreserveLast`) — keep recent sections under `LastSecBytes`, with max body-pair size `MaxBPBytes` and a **25%** reserve for future messages.
3. **QA rotation** (if `UseQA`) — cap older QA sections by count (`MaxQASections`) and total bytes (`MaxQABytes`); optional human-message summarization in QA pairs.

Summarized content is marked with prefix `**summarized content:**\n`.

### Flow summarizer (`SUMMARIZER_*`)

| Env | Config field | Default |
|---|---|---|
| `SUMMARIZER_PRESERVE_LAST` | `PreserveLast` | `true` |
| `SUMMARIZER_USE_QA` | `UseQA` | `true` |
| `SUMMARIZER_SUM_MSG_HUMAN_IN_QA` | `SummHumanInQA` | `false` |
| `SUMMARIZER_LAST_SEC_BYTES` | `LastSecBytes` | `51200` (50 KiB) |
| `SUMMARIZER_MAX_BP_BYTES` | `MaxBPBytes` | `16384` (16 KiB) |
| `SUMMARIZER_MAX_QA_SECTIONS` | `MaxQASections` | `10` |
| `SUMMARIZER_MAX_QA_BYTES` | `MaxQABytes` | `65536` (64 KiB) |
| `SUMMARIZER_KEEP_QA_SECTIONS` | `KeepQASections` | `1` |

### Assistant summarizer (`ASSISTANT_SUMMARIZER_*`)

| Env | Default |
|---|---|
| `ASSISTANT_SUMMARIZER_PRESERVE_LAST` | `true` |
| `ASSISTANT_SUMMARIZER_LAST_SEC_BYTES` | `76800` |
| `ASSISTANT_SUMMARIZER_MAX_BP_BYTES` | `16384` |
| `ASSISTANT_SUMMARIZER_MAX_QA_SECTIONS` | `7` |
| `ASSISTANT_SUMMARIZER_MAX_QA_BYTES` | `76800` |
| `ASSISTANT_SUMMARIZER_KEEP_QA_SECTIONS` | `3` |

Built-in algorithm constants match the flow defaults when config values are non-positive (e.g. zero bytes fall back to 50 KiB last section / 16 KiB body pair / 10 QA sections / 64 KiB QA budget / keep 1 section).

## User knowledge documents API

REST and GraphQL share `database/knowledge.KnowledgeStore` over `langchain_pg_embedding` (collection `langchain`). List endpoints **exclude** `doc_type=memory` so agent auto-memory chunks do not appear as user knowledge docs. Manual documents set cmetadata `manual=true`.

### Permissions

| Permission | Effect |
|---|---|
| `knowledge.admin` | Read/write any document (no `user_id` filter) |
| `knowledge.view` | List/get own documents |
| `knowledge.create` | Create |
| `knowledge.edit` | Update own (admin: any) |
| `knowledge.delete` | Delete own (admin: any) |
| `knowledge.search` | Semantic search (own docs unless admin) |

### Document types

| `doc_type` | Optional subtype fields |
|---|---|
| `answer` | `answer_type`: `guide`, `vulnerability`, `code`, `tool`, `other` |
| `guide` | `guide_type`: `install`, `configure`, `use`, `pentest`, `development`, `other` |
| `code` | `code_lang` |

### REST (`/api/v1/knowledge`)

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/knowledge/` | Paginated list (`rdb.TableQuery` + `with_content`) |
| `GET` | `/knowledge/{id}` | Get by UUID |
| `POST` | `/knowledge/` | Create + embed |
| `POST` | `/knowledge/search` | Semantic search |
| `PUT` | `/knowledge/{id}` | Update + re-embed (`content` required) |
| `DELETE` | `/knowledge/{id}` | Delete |

**List filters:** `id`, `doc_type`, `question`, `description`, `guide_type`, `answer_type`, `code_lang`, `manual`, `user_id`, `flow_id`, `task_id`, `subtask_id`, `data` (concat of doc_type + question).

**Create body limits:** `content` 1–65536 chars; `question` 1–2048; `description` ≤1000; `code_lang` ≤100.

**Search body:** `query` required (1–2048); optional `limit` 1–100 (store default **10** when unset at store layer); optional filters for doc types, guide/answer types, code langs, `flow_id`, `manual`. Search threshold **0.2**.

:::endpoint POST /api/v1/knowledge/ Create knowledge document
Requires `knowledge.create`. Computes embedding; stores full content even when embedding input is truncated to `EMBEDDING_MAX_TEXT_BYTES`.

**Body:** `doc_type`, `content`, `question`, optional `description`, `guide_type`, `answer_type`, `code_lang`.

**Responses:** `201` document entry; `400` validation; `403` permission; `503` embedder unavailable.
:::

:::endpoint POST /api/v1/knowledge/search Semantic search
Requires `knowledge.search`. Admin searches all docs; users search own.

**Body:** `query`, optional `limit`, `doc_types`, `guide_types`, `answer_types`, `code_langs`, `flow_id`, `manual`.

**Responses:** `200` scored documents; `400` invalid; `403` permission; `503` store/embedder unavailable.
:::

### GraphQL

| Operation | Permission |
|---|---|
| `knowledgeDocuments(filter, withContent)` | `knowledge.view` |
| `knowledgeDocument(id)` | `knowledge.view` |
| `searchKnowledge(query, filter, limit)` | `knowledge.search` |
| `createKnowledgeDocument(input)` | `knowledge.create` |
| `updateKnowledgeDocument(id, input)` | `knowledge.edit` |
| `deleteKnowledgeDocument(id)` | `knowledge.delete` |
| Subscriptions `knowledgeDocumentCreated/Updated/Deleted` | Scoped by user; admin variants without filter |

`CreateKnowledgeDocumentInput`: `docType`, `content`, `question`, optional `description`, `guideType`, `answerType`, `codeLang`.

## Flow files and resources under `/work`

### Container mount

Docker sandbox containers set `WorkingDir` to `/work` and bind-mount host flow workdir:

- Host: `{DATA_DIR}/flow-{id}` (or a named volume when host workdir is empty)
- Container: `/work`

### Host-side flow file cache

API listing and transfer use a separate cache tree:

```text
{DATA_DIR}/
├── flow-{id}/                 # Docker bind for /work
├── flow-{id}-data/
│   ├── uploads/               # User uploads → /work/uploads
│   ├── container/             # Cached container pulls
│   └── resources/             # Materialized resource blobs → /work/resources
└── resources/
    └── {md5}.blob             # Global content-addressed store
```

| Constant | Value |
|---|---|
| Max single upload file | 300 MB |
| Max files per request | 1000 |
| Max total upload size | 2 GB |
| Max path length (resources) | 4096 |
| Max file name length | 255 |

Paths are sanitized: relative only, no `..`, no control chars or reserved filename characters. Resolve rules for flow file paths require a prefix of `uploads`, `container`, or `resources`.

### Prompt injection

`FileListingForPrompt` builds a compact XML block for agent system prompts when uploads or resources exist:

```xml
<task_files>
<uploads base="/work/uploads">
wordlist.txt
</uploads>
<resources base="/work/resources">
creds/passwords.txt
</resources>
</task_files>
```

Resource blobs are copied into `flow-{id}-data/resources/` then tar-copied into the container at `/work` so agents see them as normal files under `/work/resources/...`.

## Optional Graphiti graph search

Graphiti is **off by default**. When enabled, the client health-checks the Graphiti URL at construction; failures prevent enablement.

| Env | Default | Role |
|---|---|---|
| `GRAPHITI_ENABLED` | `false` | Master switch |
| `GRAPHITI_URL` | empty | Graphiti HTTP base (e.g. `http://graphiti:8000`) |
| `GRAPHITI_TIMEOUT` | `30` | Seconds |
| `GRAPHITI_MODEL_NAME` | (compose/stack) | Model used by Graphiti service side |

Compose overlay: `docker-compose-graphiti.yml` (see knowledge-graph page for Neo4j wiring).

During agent performance, successful agent responses and tool executions can be **written** to Graphiti (group id `flow-{id}`) via templates. Reads go through `graphiti_search`.

### `graphiti_search`

If Graphiti is disabled, the tool returns a soft message (not a hard error): *Graphiti knowledge graph is not enabled...*

| `search_type` | Default max results | Notable params |
|---|---|---|
| `temporal_window` | 15 | `time_start`, `time_end` (ISO 8601) |
| `entity_relationships` | 20 | `center_node_uuid`, `max_depth` (default 2, max 3) |
| `diverse_results` | 10 | `diversity_level`: `low`/`medium`/`high` |
| `episode_context` | 10 | Full agent reasoning / tool outputs |
| `successful_tools` | 15 | `min_mentions` (default 2) |
| `recent_context` | 10 | `recency_window`: `1h`/`6h`/`24h`/`7d` (default `24h`) |
| `entity_by_label` | 25 | `node_labels`, optional `edge_types` |

Always required: English `query`, `search_type`, engagement `message`.

## Operations checklist

<Steps>
  <Step title="Configure embeddings">
    Set `EMBEDDING_PROVIDER` and credentials (`EMBEDDING_KEY` / provider-specific fallbacks). Leave unset or use `none` only if you do not need vector memory or knowledge search.
  </Step>
  <Step title="Verify PostgreSQL + pgvector">
    Core compose must provide PostgreSQL with pgvector. The app uses collection `langchain` in `langchain_pg_embedding`.
  </Step>
  <Step title="Tune summarizer budgets">
    Adjust `SUMMARIZER_*` for autonomous flows and `ASSISTANT_SUMMARIZER_*` for assistant mode if models hit context limits or over-summarize.
  </Step>
  <Step title="Optional Graphiti">
    Deploy with the Graphiti compose overlay, set `GRAPHITI_ENABLED=true` and `GRAPHITI_URL`, then confirm health at startup and that `graphiti_search` no longer returns the disabled message.
  </Step>
  <Step title="Seed knowledge">
    Create documents via UI, REST, or GraphQL (`knowledge.create`). Agents also learn guides/answers/code via store tools during flows.
  </Step>
</Steps>

## Failure modes

| Symptom | Likely cause |
|---|---|
| REST/GraphQL create or search → 503 / “embedding provider is not …” | Embedder not configured, `none`, or init failure |
| `search_in_memory` unavailable / errors on store | pgvector store nil (embedder or DB connection) |
| Empty memory hits within a flow | Threshold 0.2, English query quality, or nothing stored yet for that `flow_id` |
| Knowledge list missing agent tool dumps | Expected: `doc_type=memory` excluded from knowledge list |
| `graphiti_search` “not enabled” string | `GRAPHITI_ENABLED=false`, URL unset, or health check failed at client create |
| Graphiti client construction error at boot | Backend unreachable when `GRAPHITI_ENABLED=true` |
| Resource path rejected | Absolute path, `..`, invalid characters, or length limits |
| Embed token errors on long docs | Truncation uses `EMBEDDING_MAX_TEXT_BYTES` for the embedding call only |

## Related pages

<CardGroup cols={2}>
  <Card title="Knowledge graph" href="/knowledge-graph">
    Graphiti + Neo4j compose overlay, GRAPHITI_* settings, and graph tool failure modes.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full Config struct: embeddings, summarizer, Graphiti, and database pool keys.
  </Card>
  <Card title="Tools reference" href="/tools-reference">
    Argument shapes for memory, guide/answer/code, and graphiti_search tools.
  </Card>
  <Card title="REST API" href="/rest-api">
    Gin routes under /api/v1 including knowledge, files, and resources.
  </Card>
  <Card title="GraphQL API" href="/graphql-api">
    knowledgeDocuments, searchKnowledge, mutations, and subscriptions.
  </Card>
  <Card title="Tools and sandbox execution" href="/tools-and-sandbox">
    Docker isolation, file tools, and how agents access /work.
  </Card>
  <Card title="Agents and supervision" href="/agents-and-supervision">
    Memorist specialist and tool-call limits around retrieval agents.
  </Card>
  <Card title="Development and testing" href="/development-and-testing">
    etester embeddings and local compose for contributors.
  </Card>
</CardGroup>

---

## 09. Configure LLM providers

> Wire OpenAI, Anthropic, Gemini, Bedrock, DeepSeek, GLM, Kimi, Qwen, and Ollama via env keys and server URLs; UI provider profiles for per-agent models; test providers before flows.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/09-configure-llm-providers.md
- Generated: 2026-07-10T07:07:17.414Z

### Source Files

- `backend/pkg/config/config.go`
- `backend/pkg/providers/providers.go`
- `backend/pkg/providers/provider/provider.go`
- `.env.example`
- `frontend/src/pages/settings/settings-providers.tsx`
- `backend/pkg/server/services/providers.go`
- `backend/docs/config.md`

---
title: "Configure LLM providers"
description: "Wire OpenAI, Anthropic, Gemini, Bedrock, DeepSeek, GLM, Kimi, Qwen, and Ollama via env keys and server URLs; UI provider profiles for per-agent models; test providers before flows."
---

PentAGI wires LLMs as a **BYOK (bring your own key)** stack: environment variables enable default providers of type `openai`, `anthropic`, `gemini`, `bedrock`, `deepseek`, `glm`, `kimi`, `qwen`, `ollama`, and `custom`; the web UI and GraphQL store optional user-defined provider profiles (per-agent models and runtime options) that reuse those same credentials. At least one provider type must become available before flows can call models.

## Two configuration layers

| Layer | What it configures | Where it lives | Editable at runtime |
| --- | --- | --- | --- |
| Connection credentials | API keys, base URLs, auth modes, optional config paths | `.env` / process env via `Config` | No — restart required after changes |
| Provider profiles | Per-agent `model`, temperature, reasoning, prices, etc. | Default embedded `config.yml` per type, or DB rows from Settings → Providers | Yes — UI / GraphQL CRUD |

<Info>
Web Settings manage **profiles** (agent model options). They do **not** replace `OPEN_AI_KEY`, `ANTHROPIC_API_KEY`, `BEDROCK_*`, `OLLAMA_SERVER_URL`, or other connection env vars. Without credentials for a type, that type stays disabled in `settingsProviders.enabled`.
</Info>

```mermaid
flowchart LR
  subgraph env [Environment]
    Keys["API keys / URLs<br/>Config struct"]
  end
  subgraph ctrl [ProviderController]
    Defaults["defaultConfigs + built-in Providers"]
    UserDB["user-defined providers table"]
  end
  subgraph surfaces [Surfaces]
    REST["GET /api/v1/providers/"]
    GQL["settingsProviders / test* / createProvider"]
    UI["Settings → Providers"]
    Flow["NewFlowProvider / NewAssistantProvider"]
  end
  Keys --> Defaults
  Defaults --> Flow
  UserDB --> Flow
  Defaults --> REST
  UserDB --> REST
  Defaults --> GQL
  UserDB --> GQL
  GQL --> UI
```

Lookup order for a named provider: **user-defined profile by name for the current user first**, then built-in defaults (`openai`, `anthropic`, …). User profiles of a type only work when that type is already enabled by env credentials.

## Prerequisites

- Stack deployed with a valid `DATABASE_URL` (see [Installation](/installation)).
- `.env` created from `.env.example` with **at least one** LLM credential set that satisfies an activation gate below.
- Process restart (or `docker compose up -d` recreate) after env changes so `NewProviderController` re-registers providers.
- Browser access to the UI (typically `https://localhost:8443`) or API auth with `providers.view` / `settings.providers.*` permissions.

## Enable default providers via environment

Copy `.env.example` keys into `.env`. Empty keys leave the provider unregistered. Defaults match `backend/pkg/config/config.go`.

### Activation gates

`NewProviderController` only registers a built-in provider when:

| Type | Register when |
| --- | --- |
| `openai` | `OPEN_AI_KEY` non-empty |
| `anthropic` | `ANTHROPIC_API_KEY` non-empty |
| `gemini` | `GEMINI_API_KEY` non-empty |
| `bedrock` | `BEDROCK_DEFAULT_AUTH=true`, **or** `BEDROCK_BEARER_TOKEN` set, **or** both `BEDROCK_ACCESS_KEY_ID` and `BEDROCK_SECRET_ACCESS_KEY` set |
| `ollama` | `OLLAMA_SERVER_URL` non-empty |
| `custom` | `LLM_SERVER_URL` non-empty **and** (`LLM_SERVER_MODEL` **or** `LLM_SERVER_CONFIG_PATH` set) |
| `deepseek` | `DEEPSEEK_API_KEY` non-empty |
| `glm` | `GLM_API_KEY` non-empty |
| `kimi` | `KIMI_API_KEY` non-empty |
| `qwen` | `QWEN_API_KEY` non-empty |

Failed construction of a registered provider returns an error from controller startup (hard fail for that process).

### Provider env reference

<Tabs>
  <Tab title="OpenAI">
    | Variable | Default | Role |
    | --- | --- | --- |
    | `OPEN_AI_KEY` | *(empty)* | API key; required to enable |
    | `OPEN_AI_SERVER_URL` | `https://api.openai.com/v1` | Chat Completions base URL |

    ```bash
    OPEN_AI_KEY=sk-...
    OPEN_AI_SERVER_URL=https://api.openai.com/v1
    ```
  </Tab>
  <Tab title="Anthropic">
    | Variable | Default | Role |
    | --- | --- | --- |
    | `ANTHROPIC_API_KEY` | *(empty)* | API key; required to enable |
    | `ANTHROPIC_SERVER_URL` | `https://api.anthropic.com/v1` | Anthropic API base URL |

    No dedicated Vertex AI env path exists for Claude. Use Bedrock or an OpenAI-compatible gateway as `custom` if you need non-Anthropic hosting.
  </Tab>
  <Tab title="Gemini">
    | Variable | Default | Role |
    | --- | --- | --- |
    | `GEMINI_API_KEY` | *(empty)* | Google AI Studio key; required to enable |
    | `GEMINI_SERVER_URL` | `https://generativelanguage.googleapis.com` | Gemini REST endpoint |
  </Tab>
  <Tab title="Bedrock">
    | Variable | Default | Role |
    | --- | --- | --- |
    | `BEDROCK_REGION` | `us-east-1` | AWS region |
    | `BEDROCK_DEFAULT_AUTH` | `false` | Use AWS SDK default credential chain |
    | `BEDROCK_BEARER_TOKEN` | *(empty)* | Bearer auth |
    | `BEDROCK_ACCESS_KEY_ID` | *(empty)* | Static access key |
    | `BEDROCK_SECRET_ACCESS_KEY` | *(empty)* | Static secret |
    | `BEDROCK_SESSION_TOKEN` | *(empty)* | Optional STS/session token |
    | `BEDROCK_SERVER_URL` | *(empty)* | Optional VPC/custom endpoint |

    Auth priority: `BEDROCK_DEFAULT_AUTH` → `BEDROCK_BEARER_TOKEN` → static key pair.
  </Tab>
  <Tab title="DeepSeek / GLM / Kimi / Qwen">
    | Provider | Key | Server URL default | Optional LiteLLM prefix |
    | --- | --- | --- | --- |
    | DeepSeek | `DEEPSEEK_API_KEY` | `https://api.deepseek.com` | `DEEPSEEK_PROVIDER` (e.g. `deepseek`) |
    | GLM | `GLM_API_KEY` | `https://api.z.ai/api/paas/v4` | `GLM_PROVIDER` (e.g. `zai`) |
    | Kimi | `KIMI_API_KEY` | `https://api.moonshot.ai/v1` | `KIMI_PROVIDER` (e.g. `moonshot`) |
    | Qwen | `QWEN_API_KEY` | `https://dashscope-us.aliyuncs.com/compatible-mode/v1` | `QWEN_PROVIDER` (e.g. `dashscope`) |

    Regional URL alternatives (China / Singapore / coding endpoints) are documented in the config guide; override `*_SERVER_URL` only.
  </Tab>
  <Tab title="Ollama / custom">
    **Ollama** (local or cloud): enable with `OLLAMA_SERVER_URL`. Cloud (`https://ollama.com`) needs `OLLAMA_SERVER_API_KEY`. Optional: `OLLAMA_SERVER_MODEL`, `OLLAMA_SERVER_CONFIG_PATH`, pull/load flags.

    **Custom** OpenAI-compatible endpoint: `LLM_SERVER_URL` plus `LLM_SERVER_MODEL` and/or `LLM_SERVER_CONFIG_PATH`. Optional: `LLM_SERVER_KEY`, `LLM_SERVER_PROVIDER`, reasoning flags.

    Full wiring, YAML config paths, aggregators (OpenRouter, DeepInfra), and vLLM are covered on [Local and custom providers](/local-and-custom-providers) and [Deploy with vLLM and Qwen](/vllm-qwen-deployment).
  </Tab>
</Tabs>

### Shared HTTP settings that affect all providers

| Variable | Default | Role |
| --- | --- | --- |
| `PROXY_URL` | *(empty)* | HTTP(S) proxy for outbound calls (including LLM clients) |
| `EXTERNAL_SSL_CA_PATH` | *(empty)* | Extra CA bundle for TLS to LLM backends |
| `EXTERNAL_SSL_INSECURE` | `false` | Skip TLS verify for external LLM HTTP (not recommended) |
| `HTTP_CLIENT_TIMEOUT` | `600` | Seconds for external API HTTP clients (0 falls back to default) |

## Default agent baselines

Each built-in type ships an embedded `config.yml` (for example `backend/pkg/providers/openai/config.yml`) with per-agent models. Agent option keys:

| Key | Role |
| --- | --- |
| `simple` | Lightweight completion (image chooser, titles, language) |
| `simple_json` | Structured JSON completion |
| `primary_agent` | Main flow agent loop |
| `assistant` | Assistant mode |
| `generator` / `refiner` | Subtask plan generation and refinement |
| `adviser` / `reflector` | Advice and reflection |
| `searcher` / `enricher` | Search and context enrichment |
| `coder` / `installer` / `pentester` | Specialist tool-using agents |

User profiles and UI edits override these per agent without changing env keys. Field-level schema (temperature, reasoning, `extra_body`, prices) is on [Provider configuration schema](/provider-config-schema).

## UI provider profiles

**Settings → Providers** (`settings-providers.tsx` / `settings-provider.tsx`) loads GraphQL `settingsProviders`:

- `enabled` — readiness booleans per type (credential gates above)
- `default` — server baselines for each type
- `userDefined` — your saved profiles
- `models` — catalogs from each type’s `models.yml` (where present)

Supported profile types in the UI: Anthropic, Bedrock, Custom, DeepSeek, Gemini, GLM, Kimi, Ollama, OpenAI, Qwen.

### Create or edit a profile

<Steps>
  <Step title="Confirm the type is enabled">
    Open Settings → Providers. If a type is missing from create options or readiness is false, set its env credentials and restart the backend.
  </Step>
  <Step title="Create or clone">
    Use **New** for a type, or clone an existing profile. Profiles are stored per user in the `providers` table (`name`, `type`, JSON `config`).
  </Step>
  <Step title="Set per-agent models">
    Assign models and options for each agent slot. Defaults are prefilled from the type’s embedded config when you start from a baseline.
  </Step>
  <Step title="Test before save (recommended)">
    Run **Test** (full provider) or per-agent **Test**. Mutations call live backends with your env credentials and the draft agent config.
  </Step>
  <Step title="Save">
    Persist with `createProvider` / `updateProvider`. Soft-delete with `deleteProvider`.
  </Step>
</Steps>

Permissions:

| Operation | Permission |
| --- | --- |
| List for flow picker / REST | `providers.view` |
| Settings list, test | `settings.providers.view` |
| Create / update / delete | `settings.providers.edit` |

### GraphQL surface (profiles and tests)

| Operation | Kind | Purpose |
| --- | --- | --- |
| `settingsProviders` | Query | Readiness, defaults, user profiles, model lists |
| `providers` | Query | Short list for flow/assistant selection |
| `testAgent` | Mutation | Run tests for one agent config slot |
| `testProvider` | Mutation | Run tests for all agent slots (parallel workers) |
| `createProvider` / `updateProvider` / `deleteProvider` | Mutation | CRUD user profiles |
| `providerCreated` / `providerUpdated` / `providerDeleted` | Subscription | Real-time UI updates |

### REST list for automation

:::endpoint GET /api/v1/providers/
Returns registered providers for the authenticated user (built-in + user-defined). Requires Bearer (or session) auth and `providers.view`.

Each item includes `name`, `type`, `default_model` (primary agent), and `models` with agent-type bindings and optional price info.
:::

## Test providers before flows

`TestProvider` / `TestAgent` build a temporary provider from the requested type and agent config, then run the internal tester suite (`tester.TestProvider`, default 16 parallel workers). Results expose per-agent suites: `simple`, `simpleJson`, `primaryAgent`, `assistant`, `generator`, `refiner`, `adviser`, `reflector`, `searcher`, `enricher`, `coder`, `installer`, `pentester`.

Each `TestResult` reports `name`, `type`, `result`, `reasoning`, `streaming`, optional `latency`, and `error`.

<Warning>
Tests consume real tokens against the live endpoint configured in env. A failed type construction (missing key for that type) returns an API error rather than a soft skip.
</Warning>

Recommended order: enable env → restart → UI readiness true → **Test** on defaults or a draft profile → create a flow selecting that provider name.

## End-to-end setup

<Steps>
  <Step title="Set credentials">
    Edit `.env` with at least one complete provider block (key and optional URL override).
  </Step>
  <Step title="Restart the stack">
    ```bash
    docker compose up -d
    # or restart the pentagi backend process after godotenv reload
    ```
  </Step>
  <Step title="Verify readiness">
    Open Settings → Providers, or query `settingsProviders { enabled { openai anthropic ... } }`. The types you enabled should be `true`.
  </Step>
  <Step title="Optional: tune profiles">
    Create a user profile if default models or temperatures need adjustment. Test the profile.
  </Step>
  <Step title="Create a flow">
    Choose the provider name (`openai`, a custom profile name, etc.) when starting a flow. The controller resolves user profile first, then built-in.
  </Step>
</Steps>

### Minimal `.env` examples

<CodeGroup>
```bash title="OpenAI only"
OPEN_AI_KEY=sk-...
OPEN_AI_SERVER_URL=https://api.openai.com/v1
```

```bash title="Anthropic only"
ANTHROPIC_API_KEY=sk-ant-...
ANTHROPIC_SERVER_URL=https://api.anthropic.com/v1
```

```bash title="Bedrock static credentials"
BEDROCK_REGION=us-east-1
BEDROCK_ACCESS_KEY_ID=AKIA...
BEDROCK_SECRET_ACCESS_KEY=...
# optional:
# BEDROCK_SESSION_TOKEN=...
# BEDROCK_SERVER_URL=https://bedrock-runtime.us-east-1.amazonaws.com
```

```bash title="DeepSeek"
DEEPSEEK_API_KEY=...
DEEPSEEK_SERVER_URL=https://api.deepseek.com
```
</CodeGroup>

## Failure modes and troubleshooting

| Symptom | Likely cause | What to check |
| --- | --- | --- |
| Type missing from UI / `enabled` false | Activation gate not met | Env var non-empty; restart process |
| Bedrock not registered | Incomplete auth | Default auth **or** bearer **or** both access+secret keys |
| Custom type missing | Incomplete custom gate | Need `LLM_SERVER_URL` **and** model or config path |
| User profile create fails / type unavailable | Type not in `ListTypes()` | Enable built-in credentials for that type first |
| REST `422` / invalid provider type | Unknown type string | Valid types only: `openai`, `anthropic`, `gemini`, `bedrock`, `ollama`, `custom`, `deepseek`, `glm`, `kimi`, `qwen` |
| Tests fail with TLS errors | Corporate proxy / private CA | `PROXY_URL`, `EXTERNAL_SSL_CA_PATH`, avoid `EXTERNAL_SSL_INSECURE` in production |
| Tests timeout | Slow or blocked egress | `HTTP_CLIENT_TIMEOUT`, network/firewall to provider URLs |
| Flow fails at first LLM call | Bad key, wrong base URL, or quota | Re-run provider test; confirm server URL path includes `/v1` where required |
| Profile name not used | Typo or wrong user | User profiles are scoped by `user_id`; name must match exactly |

## Embeddings note

Flow/tool embeddings use separate `EMBEDDING_*` env vars (`EMBEDDING_PROVIDER` defaults to `openai`). Configuring chat LLM providers does not automatically configure embeddings. See [Memory and knowledge](/memory-and-knowledge) and [Environment variables](/environment-variables).

## Scope boundaries

This page covers **credential env wiring**, **default registration**, **UI/GraphQL user profiles**, and **pre-flow testing** for the built-in provider types.

| Topic | Page |
| --- | --- |
| Custom `LLM_SERVER_*`, Ollama pull/load, OpenRouter/DeepInfra | [Local and custom providers](/local-and-custom-providers) |
| Per-agent YAML fields, `testAgent` field shapes | [Provider configuration schema](/provider-config-schema) |
| Copy-paste YAML examples | [Example provider configs](/example-provider-configs) |
| Full env inventory | [Environment variables](/environment-variables) |

## Related pages

<CardGroup>
  <Card title="Local and custom providers" href="/local-and-custom-providers">
    OpenAI-compatible endpoints, Ollama, reasoning flags, and aggregators.
  </Card>
  <Card title="Provider configuration schema" href="/provider-config-schema">
    AgentsConfig fields, built-in baselines, and testAgent/testProvider detail.
  </Card>
  <Card title="Example provider configs" href="/example-provider-configs">
    Ready YAML for vLLM, Ollama, OpenRouter, DeepInfra, and Azure-style endpoints.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Authoritative Config keys and defaults across the stack.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    First flow after setting one LLM key and bringing the stack up.
  </Card>
  <Card title="REST API" href="/rest-api">
    `GET /api/v1/providers/` and other automation endpoints.
  </Card>
</CardGroup>

---

## 10. Local and custom providers

> OpenAI-compatible custom endpoints (LLM_SERVER_*), Ollama local or cloud, config path YAML, legacy and preserve reasoning flags, and aggregator endpoints such as OpenRouter and DeepInfra.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/10-local-and-custom-providers.md
- Generated: 2026-07-10T07:06:56.344Z

### Source Files

- `backend/pkg/providers/custom/custom.go`
- `backend/pkg/providers/ollama/ollama.go`
- `backend/pkg/providers/ollama/config.yml`
- `examples/configs/custom-openai.provider.yml`
- `examples/configs/openrouter.provider.yml`
- `examples/configs/ollama-cloud.provider.yml`
- `backend/docs/ollama.md`
- `.env.example`

---
title: "Local and custom providers"
description: "OpenAI-compatible custom endpoints (LLM_SERVER_*), Ollama local or cloud, config path YAML, legacy and preserve reasoning flags, and aggregator endpoints such as OpenRouter and DeepInfra."
---

PentAGI registers two BYOK/BYOC-style providers for endpoints you control: **`custom`** (`ProviderCustom`) speaks the OpenAI Chat Completions protocol via `LLM_SERVER_*`, and **`ollama`** (`ProviderOllama`) talks to a local Ollama server or Ollama Cloud via `OLLAMA_SERVER_*`. Both load per-agent model and sampling settings from YAML at `LLM_SERVER_CONFIG_PATH` / `OLLAMA_SERVER_CONFIG_PATH` (or built-in defaults), then expose the same agent option keys as first-party providers.

## When each provider activates

At process start, `backend/pkg/providers/providers.go` builds default agent configs for every provider type, then instantiates only those that pass connection gates:

| Provider type | Gate | Default name |
| --- | --- | --- |
| `custom` | `LLM_SERVER_URL` is set **and** either `LLM_SERVER_MODEL` or `LLM_SERVER_CONFIG_PATH` is set | `custom` |
| `ollama` | `OLLAMA_SERVER_URL` is set | `ollama` |

<Warning>
Setting only `LLM_SERVER_URL` without a model or config path does **not** register the custom provider. Ollama needs only a non-empty server URL.
</Warning>

```mermaid
flowchart LR
  subgraph env [Environment]
    LLM["LLM_SERVER_*"]
    OLL["OLLAMA_SERVER_*"]
  end
  subgraph runtime [Provider factory]
    GateC{"URL and model or config?"}
    GateO{"URL set?"}
    Custom["custom.New<br/>openai.LLM client"]
    Ollama["ollama.New<br/>ollama.LLM + api.Client"]
  end
  subgraph yaml [Agent YAML]
    CfgC["LLM_SERVER_CONFIG_PATH<br/>or empty skeleton"]
    CfgO["OLLAMA_SERVER_CONFIG_PATH<br/>or embedded config.yml"]
  end
  LLM --> GateC
  GateC -->|yes| Custom
  CfgC --> Custom
  OLL --> GateO
  GateO -->|yes| Ollama
  CfgO --> Ollama
```

## Custom OpenAI-compatible provider

Implementation: `backend/pkg/providers/custom`. The client is `langchaingo/llms/openai` pointed at `LLM_SERVER_URL` with bearer token `LLM_SERVER_KEY` and base model `LLM_SERVER_MODEL`. HTTP traffic uses the shared proxy-aware client from `system.GetHTTPClient`.

### Environment variables

| Variable | Config field | Default | Role |
| --- | --- | --- | --- |
| `LLM_SERVER_URL` | `LLMServerURL` | *(empty)* | Base URL of the OpenAI-compatible API (must include path prefix such as `/v1` when the upstream expects it) |
| `LLM_SERVER_KEY` | `LLMServerKey` | *(empty)* | API key / bearer token |
| `LLM_SERVER_MODEL` | `LLMServerModel` | *(empty)* | Default model for all agents when YAML does not override |
| `LLM_SERVER_CONFIG_PATH` | `LLMServerConfig` | *(empty)* | Path to per-agent YAML; empty uses a skeleton of empty agent blocks |
| `LLM_SERVER_PROVIDER` | `LLMServerProvider` | *(empty)* | Optional model-name prefix for LiteLLM-style proxies (`prefix/model`) |
| `LLM_SERVER_LEGACY_REASONING` | `LLMServerLegacyReasoning` | `false` | Reasoning parameter wire format |
| `LLM_SERVER_PRESERVE_REASONING` | `LLMServerPreserveReasoning` | `false` | Keep reasoning content across multi-turn tool calls |

Docker Compose also supports a **host path** mount variable (maps into the container config tree):

| Host / compose variable | Container path (default target) |
| --- | --- |
| `PENTAGI_LLM_SERVER_CONFIG_PATH` | `/opt/pentagi/conf/custom.provider.yml` (default host file: `./example.custom.provider.yml`) |

Set the in-container path on `LLM_SERVER_CONFIG_PATH` to match the mount, for example:

```bash
PENTAGI_LLM_SERVER_CONFIG_PATH=./examples/configs/openrouter.provider.yml
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/custom.provider.yml
```

### Default call options

When building the provider config, custom defaults are:

- `temperature`: `1.0`
- `top_p`: `1.0`
- `n`: `1`
- `max_tokens`: `16384`
- `model`: `LLM_SERVER_MODEL` when that env is non-empty

YAML agent blocks override these per agent type.

### Reasoning flags

When `LLM_SERVER_LEGACY_REASONING` is **`false`** (default), the OpenAI client enables modern reasoning options:

- `WithUsingReasoningMaxTokens()`
- `WithModernReasoningFormat()`

When **`true`**, those options are omitted so the client uses the legacy string-style `reasoning_effort` path. Use `true` for official OpenAI reasoning-compatible endpoints when modern object-shaped reasoning fails.

When `LLM_SERVER_PRESERVE_REASONING` is **`true`**, the client sets `WithPreserveReasoningContent()`. Enable this for providers (for example Moonshot-class APIs) that reject multi-turn tool-call histories if assistant reasoning content is stripped.

| Setting | `false` (default) | `true` |
| --- | --- | --- |
| `LLM_SERVER_LEGACY_REASONING` | Modern structured reasoning (`max_tokens` object style) | Legacy `reasoning_effort` string style |
| `LLM_SERVER_PRESERVE_REASONING` | Do not keep reasoning in conversation history | Preserve reasoning content for subsequent turns |

### Model discovery and LiteLLM prefix

On init, custom calls `provider.LoadModelsFromHTTP(baseURL, apiKey, httpClient, prefix)`:

- `GET {baseURL}/models` with optional `Authorization: Bearer …`
- 3-second timeout
- On failure, the models list falls back to empty (provider still starts)

When `LLM_SERVER_PROVIDER` is set (for example `moonshot`):

- Discovery **filters** model IDs that start with `prefix/`
- Stored names strip the prefix
- `ModelWithPrefix` re-applies `prefix/model` at call time via `ApplyModelPrefix`

That lets one YAML file work for both direct vendor URLs and a LiteLLM (or similar) proxy that namespaces models.

### Config path behavior

```text
LLM_SERVER_CONFIG_PATH empty?
  yes → pconfig.EmptyProviderConfigRaw (all agent keys present, empty objects)
  no  → os.ReadFile(path) → pconfig.LoadConfigData
```

Agent keys in YAML (same surface as other providers):

`simple`, `simple_json`, `primary_agent`, `assistant`, `generator`, `refiner`, `adviser`, `reflector`, `searcher`, `enricher`, `coder`, `installer`, `pentester`

Common fields: `model`, `temperature`, `top_p`, `n`, `max_tokens`, `json`, `reasoning` (`effort` and/or `max_tokens`), `price` (`input` / `output`).

## Ollama provider (local and cloud)

Implementation: `backend/pkg/providers/ollama`. Uses `langchaingo/llms/ollama` for inference and the official `ollama/api` client for list/show/pull.

### Environment variables

| Variable | Config field | Default | Role |
| --- | --- | --- | --- |
| `OLLAMA_SERVER_URL` | `OllamaServerURL` | *(empty)* | Local base URL or cloud `https://ollama.com` |
| `OLLAMA_SERVER_API_KEY` | `OllamaServerAPIKey` | *(empty)* | Required for Ollama Cloud; leave empty for local |
| `OLLAMA_SERVER_MODEL` | `OllamaServerModel` | *(empty)* | Default model; also injected into default call options |
| `OLLAMA_SERVER_CONFIG_PATH` | `OllamaServerConfig` | *(empty)* | Custom agent YAML; empty uses embedded `config.yml` |
| `OLLAMA_SERVER_PULL_MODELS_TIMEOUT` | `OllamaServerPullModelsTimeout` | `600` | Pull timeout in seconds |
| `OLLAMA_SERVER_PULL_MODELS_ENABLED` | `OllamaServerPullModelsEnabled` | `false` | Auto-pull models from YAML + base model on startup |
| `OLLAMA_SERVER_LOAD_MODELS_ENABLED` | `OllamaServerLoadModelsEnabled` | `false` | Query server model list for UI/catalog |

Docker host mount:

| Host / compose variable | Container path (default target) |
| --- | --- |
| `PENTAGI_OLLAMA_SERVER_CONFIG_PATH` | `/opt/pentagi/conf/ollama.provider.yml` (default host file: `./example.ollama.provider.yml`) |

### Local vs cloud

<Tabs>
  <Tab title="Local server">
```bash
OLLAMA_SERVER_URL=http://localhost:11434
# or from another Compose service: http://ollama-server:11434
OLLAMA_SERVER_API_KEY=
OLLAMA_SERVER_MODEL=llama3.1:8b-instruct-q8_0
OLLAMA_SERVER_PULL_MODELS_ENABLED=false
OLLAMA_SERVER_LOAD_MODELS_ENABLED=false
```

Local inference needs no API key. Pricing fields on models are left unset (`Price: nil`) when listing from the server.
  </Tab>
  <Tab title="Ollama Cloud">
```bash
OLLAMA_SERVER_URL=https://ollama.com
OLLAMA_SERVER_API_KEY=your_ollama_cloud_api_key
OLLAMA_SERVER_MODEL=gpt-oss:120b
# Multi-agent assignments (paid / multi-model):
OLLAMA_SERVER_CONFIG_PATH=/opt/pentagi/conf/ollama-cloud.provider.yml
```

Non-empty `OLLAMA_SERVER_API_KEY` is passed as `ollama.WithAPIKey`. Free-tier usage typically pins a single concurrent model via `OLLAMA_SERVER_MODEL`; multi-agent YAML (for example `examples/configs/ollama-cloud.provider.yml`) assigns cloud-tagged models per agent.
  </Tab>
</Tabs>

### Default agent config and pull behavior

If `OLLAMA_SERVER_CONFIG_PATH` is empty, the provider embeds `backend/pkg/providers/ollama/config.yml` (sampling defaults only—no per-agent `model` keys; agents inherit `OLLAMA_SERVER_MODEL`).

Build defaults: `n: 1`, `max_tokens: 32768`, model from `OLLAMA_SERVER_MODEL`.

When `OLLAMA_SERVER_PULL_MODELS_ENABLED=true`:

1. Collect unique models from the base model plus every model named in the agent config.
2. For each model, `Show` with a 10s timeout; if missing, `Pull` within `OLLAMA_SERVER_PULL_MODELS_TIMEOUT` (or 10 minutes if timeout ≤ 0).
3. Any pull error fails provider construction.

When `OLLAMA_SERVER_LOAD_MODELS_ENABLED=true`, the provider lists models via the Ollama API (10s timeout). When false, the available list is only the base model.

`ModelWithPrefix` is a passthrough (no LiteLLM-style prefix for Ollama).

## Aggregator and example endpoints

Point the custom provider at any OpenAI-compatible gateway. Repository examples under `examples/configs/`:

| Endpoint pattern | Example base URL | Example config file |
| --- | --- | --- |
| OpenRouter | `https://openrouter.ai/api/v1` | `openrouter.provider.yml` |
| DeepInfra | `https://api.deepinfra.com/v1/openai` | `deepinfra.provider.yml` |
| OpenAI-compatible direct | `https://api.openai.com/v1` | `custom-openai.provider.yml` |
| Novita | `https://api.novita.ai/openai` | `novita.provider.yml` |
| Azure OpenAI-compatible | vendor-specific | `azure-openai.provider.yml` |
| vLLM (local) | your `vllm serve` base URL | `vllm-qwen*.provider.yml` |
| Ollama Cloud multi-model | `https://ollama.com` | `ollama-cloud.provider.yml` |

### Wire OpenRouter or DeepInfra

<Steps>
  <Step title="Choose aggregator credentials">
    Obtain an API key from OpenRouter, DeepInfra, or another OpenAI-compatible aggregator. No first-party vendor key is required for the custom path beyond that gateway key.
  </Step>
  <Step title="Mount agent YAML">
    Copy or reference `examples/configs/openrouter.provider.yml` or `deepinfra.provider.yml`. Under Docker, set `PENTAGI_LLM_SERVER_CONFIG_PATH` to the host path so it mounts at `/opt/pentagi/conf/custom.provider.yml`.
  </Step>
  <Step title="Set custom LLM env">
```bash
LLM_SERVER_URL=https://openrouter.ai/api/v1
# or: https://api.deepinfra.com/v1/openai
LLM_SERVER_KEY=your_api_key
LLM_SERVER_MODEL=
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/custom.provider.yml
LLM_SERVER_PROVIDER=
LLM_SERVER_LEGACY_REASONING=false
LLM_SERVER_PRESERVE_REASONING=false
```
  Leave `LLM_SERVER_MODEL` empty when every agent specifies `model` in YAML. Set `LLM_SERVER_PROVIDER` only when a proxy namespaces models (for example LiteLLM).
  </Step>
  <Step title="Restart and verify">
    Restart the PentAGI container or process. Confirm the `custom` provider appears in Settings and passes provider/agent tests before creating a flow.
  </Step>
</Steps>

### OpenRouter-style agent snippet

From `examples/configs/openrouter.provider.yml`—models use `vendor/model` IDs and optional `reasoning` / `price` blocks:

```yaml
primary_agent:
  model: "openai/gpt-5"
  n: 1
  max_tokens: 6000
  reasoning:
    effort: medium
  price:
    input: 1.25
    output: 10.0

generator:
  model: "anthropic/claude-sonnet-4.5"
  n: 1
  max_tokens: 12000
  reasoning:
    max_tokens: 4000
  price:
    input: 3.0
    output: 15.0
```

### DeepInfra-style agent snippet

From `examples/configs/deepinfra.provider.yml`—IDs follow DeepInfra’s `org/model` naming:

```yaml
simple:
  model: "Qwen/Qwen3-Next-80B-A3B-Instruct"
  temperature: 0.7
  top_p: 0.95
  n: 1
  max_tokens: 4000
  price:
    input: 0.14
    output: 1.4

pentester:
  model: "moonshotai/Kimi-K2-Instruct-0905"
  temperature: 1.0
  n: 1
  max_tokens: 6000
  price:
    input: 0.4
    output: 2.0
```

### Official OpenAI via custom path

When using the custom provider against `https://api.openai.com/v1` with `custom-openai.provider.yml`:

```bash
LLM_SERVER_URL=https://api.openai.com/v1
LLM_SERVER_KEY=your_openai_api_key
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/custom.provider.yml
LLM_SERVER_LEGACY_REASONING=true
```

Prefer the dedicated OpenAI provider (`OPEN_AI_KEY`) when you do not need the custom path; use custom when you share one OpenAI-compatible stack for aggregators and self-hosted gateways.

### LiteLLM proxy with the same YAML

```bash
# Direct vendor
LLM_SERVER_URL=https://api.moonshot.ai/v1
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/moonshot.provider.yml
LLM_SERVER_PROVIDER=

# Same YAML through LiteLLM
LLM_SERVER_URL=http://litellm-proxy:4000
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/moonshot.provider.yml
LLM_SERVER_PROVIDER=moonshot
```

With `LLM_SERVER_PROVIDER=moonshot`, a YAML model `kimi-2.5` is sent as `moonshot/kimi-2.5`.

## Quick reference: local Ollama

```bash
# Install and pull models on the host (or Ollama container)
ollama pull llama3.1:8b-instruct-q8_0

# Fast startup (static config)
OLLAMA_SERVER_URL=http://localhost:11434
OLLAMA_SERVER_MODEL=llama3.1:8b-instruct-q8_0
OLLAMA_SERVER_PULL_MODELS_ENABLED=false
OLLAMA_SERVER_LOAD_MODELS_ENABLED=false

# Optional: auto-pull + discover
OLLAMA_SERVER_PULL_MODELS_ENABLED=true
OLLAMA_SERVER_PULL_MODELS_TIMEOUT=900
OLLAMA_SERVER_LOAD_MODELS_ENABLED=true
```

Example local multi-agent files: `ollama-llama318b.provider.yml`, `ollama-qwen332b-fp16-tc.provider.yml`, `ollama-qwq32b-fp16-tc.provider.yml`.

## Operational notes

<AccordionGroup>
  <Accordion title="Custom provider experimental surface">
    `LLM_SERVER_*` is documented in the repo as an experimental wiring surface that may evolve. Prefer stable first-party providers when a dedicated adapter exists; use custom for aggregators, proxies, and self-hosted OpenAI-compatible servers.
  </Accordion>
  <Accordion title="Proxy">
    Both providers use `system.GetHTTPClient(cfg)`, so a global `PROXY_URL` applies to model HTTP traffic when configured.
  </Accordion>
  <Accordion title="Vertex AI workaround">
    There is no first-class Vertex AI adapter. Supported path: expose Vertex through an OpenAI-compatible gateway that preserves chat and tool-call behavior, then set `LLM_SERVER_URL`, `LLM_SERVER_KEY`, and `LLM_SERVER_MODEL` (or a config path).
  </Accordion>
  <Accordion title="vLLM and smaller local models">
    Production-style local stacks often use vLLM with Qwen-class models via `LLM_SERVER_*` and the `vllm-qwen*.provider.yml` templates (thinking vs non-thinking). See the dedicated deployment page for hardware and supervision flags.
  </Accordion>
</AccordionGroup>

## Failure modes

| Symptom | Likely cause | Mitigation |
| --- | --- | --- |
| Custom provider missing from UI | Gate failed: no URL, or URL without model **and** config path | Set `LLM_SERVER_URL` plus `LLM_SERVER_MODEL` and/or `LLM_SERVER_CONFIG_PATH` |
| Startup fails reading config | Bad path or unreadable file | Fix path; under Docker use `PENTAGI_*_CONFIG_PATH` host mount + in-container `*_CONFIG_PATH` |
| Reasoning / tool-call errors | Wrong wire format or stripped reasoning | Toggle `LLM_SERVER_LEGACY_REASONING`; enable `LLM_SERVER_PRESERVE_REASONING` for multi-turn reasoning APIs |
| Models empty in custom UI | `/models` fetch failed or timed out | Provider still runs; list models manually in YAML; check URL auth and reachability |
| Ollama connection errors | Server down or wrong URL | Ensure Ollama is listening; use Docker network hostname when applicable |
| Ollama model not found | Not pulled | `ollama pull <model>` or enable `OLLAMA_SERVER_PULL_MODELS_ENABLED` |
| Slow Ollama startup | Pull and/or load enabled | Disable load/pull after first successful warm-up for static deployments |
| LiteLLM 404 on model | Missing or wrong prefix | Align `LLM_SERVER_PROVIDER` with proxy namespace |

## Related pages

<CardGroup>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    First-party env keys, UI profiles, and testing providers before flows.
  </Card>
  <Card title="Provider configuration schema" href="/provider-config-schema">
    Per-agent YAML fields: model, sampling, reasoning, price, extra_body.
  </Card>
  <Card title="Example provider configs" href="/example-provider-configs">
    Copy-paste YAML for vLLM, Ollama, OpenRouter, DeepInfra, Azure, and more.
  </Card>
  <Card title="Deploy with vLLM and Qwen" href="/vllm-qwen-deployment">
    Local/air-gapped inference: serve flags, LLM_SERVER wiring, thinking configs.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full Config struct and `.env.example` reference.
  </Card>
</CardGroup>

---

## 11. Search engines

> Enable and configure DuckDuckGo, Google CSE, Tavily, Traversaal, Perplexity, Sploitus, and Searxng; scraper URLs; proxy and timeout constraints that gate network search tools.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/11-search-engines.md
- Generated: 2026-07-10T07:07:42.360Z

### Source Files

- `backend/pkg/tools/duckduckgo.go`
- `backend/pkg/tools/google.go`
- `backend/pkg/tools/tavily.go`
- `backend/pkg/tools/perplexity.go`
- `backend/pkg/tools/searxng.go`
- `backend/pkg/tools/sploitus.go`
- `.env.example`
- `backend/docs/config.md`

---
title: "Search engines"
description: "Enable and configure DuckDuckGo, Google CSE, Tavily, Traversaal, Perplexity, Sploitus, and Searxng; scraper URLs; proxy and timeout constraints that gate network search tools."
---

PentAGI exposes network search as agent tools under type `SearchNetworkToolType` in `backend/pkg/tools/registry.go`. Each engine is registered only when its `IsAvailable()` gate passes; agents never see a tool that is disabled or missing credentials. HTTP traffic for all engines goes through `system.GetHTTPClient`, which applies `PROXY_URL`, `HTTP_CLIENT_TIMEOUT`, and optional external TLS settings from `backend/pkg/config`.

## How search tools enter a flow

Primary agents usually call the high-level `search` agent tool (`SearchToolName`), which delegates to the **searcher** specialist. The searcher executor (`GetSearcherExecutor` in `backend/pkg/tools/tools.go`) attaches every available network engine plus optional memory/store tools.

When assistant mode runs with `UseAgents=false`, the same engines are registered directly on the assistant executor instead of behind `search`.

```text
Agent / Assistant
        │
        ├─ search (agent tool) ──► Searcher executor
        │                              ├─ google, duckduckgo, tavily, ...
        │                              ├─ browser (if scraper URL set)
        │                              └─ search_answer / store_answer (vector DB)
        │
        └─ (UseAgents=false) ──► engines registered on the executor itself
```

Unconfigured engines are omitted from the LLM tool list. Calling a registered engine that later fails typically returns a string error result (swallowed failure) so the agent can continue; unavailable tools return a hard error from `Handle`.

## Engine matrix

| Tool name | Registry name | Availability gate | Default on? | Auth / endpoint |
|---|---|---|---|---|
| DuckDuckGo | `duckduckgo` | `DUCKDUCKGO_ENABLED` | **Yes** (`true`) | HTML endpoint `https://html.duckduckgo.com/html/` (no API key) |
| Google CSE | `google` | `GOOGLE_API_KEY` **and** `GOOGLE_CX_KEY` non-empty | Off until keys set | Google Custom Search JSON API |
| Tavily | `tavily` | `TAVILY_API_KEY` non-empty | Off until key set | `https://api.tavily.com/search` |
| Traversaal | `traversaal` | `TRAVERSAAL_API_KEY` non-empty | Off until key set | `https://api-ares.traversaal.ai/live/predict` |
| Perplexity | `perplexity` | `PERPLEXITY_API_KEY` non-empty | Off until key set | `https://api.perplexity.ai/chat/completions` |
| Searxng | `searxng` | `SEARXNG_URL` non-empty | Off until URL set | Self-hosted base URL + `/search?format=json` |
| Sploitus | `sploitus` | `SPLOITUS_ENABLED` | **No** (`false`) | `https://sploitus.com/search` (no API key; Cloudflare-sensitive) |
| Browser | `browser` | `SCRAPER_PRIVATE_URL` or `SCRAPER_PUBLIC_URL` non-empty | Depends on compose/scraper | Internal scraper service |

<Note>
`backend/docs/config.md` documents `SPLOITUS_ENABLED` default as `true` in one table, but `config.go` sets `envDefault:"false"`. Runtime follows `config.go`.
</Note>

## Common tool arguments

Most engines use `SearchAction` (`backend/pkg/tools/args.go`):

| Field | Type | Required | Notes |
|---|---|---|---|
| `query` | string | yes | Always English for index quality; short, exact queries work best |
| `max_results` | integer | yes | Schema: min 1, max 10, default guidance 5; engines clamp independently |
| `message` | string | yes | Engagement-log commentary (may use engagement language) |

Sploitus uses `SploitusAction` instead:

| Field | Type | Required | Notes |
|---|---|---|---|
| `query` | string | yes | English-only index (CVE, product, class) |
| `exploit_type` | string | no | `exploits` (default) or `tools` |
| `sort` | string | no | `default`, `date`, or `score` |
| `max_results` | integer | yes | Clamped to 1–25; default 10 if out of range |
| `message` | string | yes | Engagement-log commentary |

Browser uses `Browser` with `url`, `action` (`markdown` \| `html` \| `links`), and `message`.

## Configure engines

Copy keys into `.env` from `.env.example`, then restart the stack so `pentagi` reloads env.

### DuckDuckGo

Free HTML search. Enabled by default.

<ParamField body="DUCKDUCKGO_ENABLED" type="bool" default="true">
When false, `duckduckgo` is not registered.
</ParamField>
<ParamField body="DUCKDUCKGO_REGION" type="string">
Region code such as `us-en` (code default when empty), `uk-en`, `de-de`, `fr-fr`, `jp-jp`, `cn-zh`, `ru-ru`.
</ParamField>
<ParamField body="DUCKDUCKGO_SAFESEARCH" type="string">
`strict` → `kp=1`, `moderate` → `kp=0`, `off` → `kp=-1`; empty leaves `kp` unset.
</ParamField>
<ParamField body="DUCKDUCKGO_TIME_RANGE" type="string">
`d` (day), `w` (week), `m` (month), `y` (year).
</ParamField>

Implementation notes:

- POST form to DuckDuckGo HTML search; parses result list.
- Max results capped at 10; invalid `max_results` resets to 10.
- Client timeout **30s**; up to **3** retries with 1s backoff.
- Empty result set returns the string `No results found`.

### Google Custom Search

Requires a [Programmable Search Engine](https://programmablesearchengine.google.com/) ID plus API key.

<ParamField body="GOOGLE_API_KEY" type="string" required>
API key for Custom Search.
</ParamField>
<ParamField body="GOOGLE_CX_KEY" type="string" required>
Custom Search Engine ID (`cx`). Both key and `cx` must be set for `IsAvailable()`.
</ParamField>
<ParamField body="GOOGLE_LR_KEY" type="string" default="lang_en">
Language restriction passed as `lr` (for example `lang_en`).
</ParamField>

Results format as markdown (`# title`, `## URL`, `## Snippet`). Max results clamped to 1–10.

### Tavily

AI search with answer + ranked links + optional raw page content.

<ParamField body="TAVILY_API_KEY" type="string" required>
Bearer key for Tavily API.
</ParamField>

Fixed request shape in code: `topic=general`, `search_depth=advanced`, `include_answer=true`, `include_raw_content=true`. Raw content over ~3000 chars can be summarized via the flow summarizer when attached.

HTTP status mapping returns agent-readable strings (wrong API key → `API key is wrong`, rate limits, maintenance, etc.).

### Traversaal

Live predict API returning a single answer plus URL list.

<ParamField body="TRAVERSAAL_API_KEY" type="string" required>
Sent as header `x-api-key`.
</ParamField>

Output: `# Answer` block and numbered `# Links`. Non-200 responses become `unexpected status code: N`.

### Perplexity

Chat-completions style web research (`sonar` by default).

<ParamField body="PERPLEXITY_API_KEY" type="string" required>
Bearer token.
</ParamField>
<ParamField body="PERPLEXITY_MODEL" type="string" default="sonar">
Model name sent in the completion request.
</ParamField>
<ParamField body="PERPLEXITY_CONTEXT_SIZE" type="string" default="low">
`search_context_size`: `low`, `medium`, or `high`.
</ParamField>

Hardcoded request knobs: temperature `0.5`, top_p `0.9`, max_tokens `4000`, timeout **60s**, non-streaming. Large answers may be summarized or truncated at ~3000 chars when no summarizer succeeds.

### Searxng

Self-hosted meta-search. No cloud key; availability is entirely URL-based.

<ParamField body="SEARXNG_URL" type="string" required>
Base URL of the instance. Code appends `/search` if the path does not already end with `/search`.
</ParamField>
<ParamField body="SEARXNG_CATEGORIES" type="string" default="general">
Passed as `categories` query param.
</ParamField>
<ParamField body="SEARXNG_LANGUAGE" type="string">
Language filter (for example `en`).
</ParamField>
<ParamField body="SEARXNG_SAFESEARCH" type="string" default="0">
`0` none, `1` moderate, `2` strict.
</ParamField>
<ParamField body="SEARXNG_TIME_RANGE" type="string">
Optional `time_range` (for example `day`, `month`, `year`).
</ParamField>
<ParamField body="SEARXNG_TIMEOUT" type="int">
Seconds; if unset or ≤0, default **30s**.
</ParamField>

Query always requests `format=json`. Empty hit lists return a markdown “No Results Found” block.

### Sploitus

Exploit/tool aggregator (ExploitDB, Packet Storm, GitHub advisories, and others). Disabled by default because the upstream sits behind Cloudflare; the config comment notes egress IP reputation matters.

<ParamField body="SPLOITUS_ENABLED" type="bool" default="false">
Set `true` to register `sploitus`.
</ParamField>

Behavior:

- POST JSON to `https://sploitus.com/search` with browser-like headers.
- Request timeout **30s**.
- HTTP **499** or **422** treated as rate limit.
- Output size hard limits: ~50 KB per source field, ~80 KB total markdown (truncation message when exceeded).

### Example `.env` fragments

```bash
# Zero-config path: DuckDuckGo only (enabled by default)
DUCKDUCKGO_ENABLED=true
DUCKDUCKGO_REGION=us-en

# Google CSE
GOOGLE_API_KEY=AIza...
GOOGLE_CX_KEY=your_cse_id
GOOGLE_LR_KEY=lang_en

# Cloud AI search APIs
TAVILY_API_KEY=tvly-...
TRAVERSAAL_API_KEY=...
PERPLEXITY_API_KEY=pplx-...
PERPLEXITY_MODEL=sonar
PERPLEXITY_CONTEXT_SIZE=low

# Self-hosted meta-search
SEARXNG_URL=http://searxng:8080
SEARXNG_CATEGORIES=general
SEARXNG_SAFESEARCH=0
SEARXNG_TIMEOUT=30

# Exploit search (opt-in)
SPLOITUS_ENABLED=true
```

## Scraper URLs and the browser tool

The `browser` tool is not a search engine API; it is the network tool for opening URLs and returning markdown, HTML, or link lists via the `vxcontrol/scraper` service in `docker-compose.yml`.

<ParamField body="SCRAPER_PRIVATE_URL" type="string">
Internal backend → scraper URL (compose example: `https://someuser:somepass@scraper/`).
</ParamField>
<ParamField body="SCRAPER_PUBLIC_URL" type="string">
URL usable for client-facing screenshot/content access when needed.
</ParamField>
<ParamField body="LOCAL_SCRAPER_USERNAME" type="string" default="someuser">
Scraper basic-auth user for the compose service.
</ParamField>
<ParamField body="LOCAL_SCRAPER_PASSWORD" type="string" default="somepass">
Scraper basic-auth password.
</ParamField>
<ParamField body="LOCAL_SCRAPER_MAX_CONCURRENT_SESSIONS" type="int" default="10">
Mapped into the scraper container as `MAX_CONCURRENT_SESSIONS`.
</ParamField>

`browser.IsAvailable()` is true if **either** private or public scraper URL is set. Empty both → tool never registered.

Compose service (defaults):

- Image: `vxcontrol/scraper:latest`
- Host port: `SCRAPER_LISTEN_IP` / `SCRAPER_LISTEN_PORT` (default `127.0.0.1:9443` → container 443)
- Podman rootless note in `.env.example`: use `http://user:pass@scraper:3000/` for private URL

Binary/non-HTML URLs (`.pdf`, archives, media, etc.) short-circuit with a descriptive hint rather than a generic “content too small” failure.

## Proxy, TLS, and timeouts

All engines build clients with `system.GetHTTPClient(cfg)`:

| Setting | Env | Default | Effect |
|---|---|---|---|
| Proxy | `PROXY_URL` | empty | When set, all search HTTP calls use this HTTP(S) proxy |
| Global timeout | `HTTP_CLIENT_TIMEOUT` | `600` seconds | Client timeout in seconds; `0` means no timeout (not recommended) |
| External CA | `EXTERNAL_SSL_CA_PATH` | empty | PEM appended to system trust pool |
| Skip verify | `EXTERNAL_SSL_INSECURE` | `false` | `InsecureSkipVerify` on the TLS config |

Per-tool overrides after client creation:

| Tool | Effective timeout |
|---|---|
| DuckDuckGo | 30s |
| Perplexity | 60s |
| Sploitus | 30s |
| Searxng | `SEARXNG_TIMEOUT` seconds, else 30s |
| Google / Tavily / Traversaal | inherit `HTTP_CLIENT_TIMEOUT` |

Proxy and TLS apply to search APIs and other external HTTP from the backend; Docker sandbox terminals use separate networking (see Docker sandbox docs).

## Logging and observability

Successful and failed searches (when agent context is present) call `SearchLogProvider.PutLog` with:

- Parent/current agent types
- `database.SearchengineType*` enum (`google`, `duckduckgo`, `tavily`, `traversaal`, `perplexity`, `searxng`, `sploitus`, …)
- Query and result text
- Optional task/subtask IDs

Search failures that are swallowed also emit Langfuse warning events named `search engine error swallowed` (or `sploitus search error swallowed`) with engine metadata.

## Failure modes

| Symptom | Likely cause | Fix |
|---|---|---|
| Agent never calls `google` / `tavily` / … | Tool not in definitions | Set required env keys / enable flags and restart |
| Only DuckDuckGo works | Expected with defaults | Other engines need keys or `SEARXNG_URL` / `SPLOITUS_ENABLED` |
| `browser` missing | No scraper URL | Set `SCRAPER_PRIVATE_URL` (and credentials matching the scraper service) |
| Timeouts to cloud APIs | Proxy/firewall or low timeout | Set `PROXY_URL` if required; raise `HTTP_CLIENT_TIMEOUT` carefully |
| Sploitus HTTP 499/422 | Rate limit / Cloudflare | Wait/retry; improve egress IP reputation; keep disabled if unreliable |
| Google 4xx | Bad key or CSE ID | Verify `GOOGLE_API_KEY` + `GOOGLE_CX_KEY` and CSE permissions |
| Tavily/Perplexity “API key is wrong” | Invalid key | Rotate key in `.env` |
| Searxng unexpected status / decode errors | Instance not JSON-enabled or wrong URL | Ensure instance allows `format=json` and path resolves to `/search` |
| Empty DuckDuckGo results | Upstream HTML change or blocking | Retries already applied; try another engine or proxy |

## Verify configuration

<Steps>
  <Step title="Set env and restart">
    Edit `.env`, then `docker compose up -d` (or restart the `pentagi` service) so config reloads.
  </Step>
  <Step title="Confirm minimum path">
    Leave `DUCKDUCKGO_ENABLED` unset or `true`. Create a flow that needs web research; tool logs should show `duckduckgo` when the searcher runs.
  </Step>
  <Step title="Add at least one richer engine">
    Set `TAVILY_API_KEY` or `PERPLEXITY_API_KEY` for answer-style results, or `SEARXNG_URL` for self-hosted aggregation.
  </Step>
  <Step title="Optional exploit research">
    Set `SPLOITUS_ENABLED=true` only if egress can reach Sploitus reliably.
  </Step>
  <Step title="Optional page fetch">
    Ensure compose `scraper` is up and `SCRAPER_PRIVATE_URL` matches service auth; agents should then receive `browser`.
  </Step>
</Steps>

Search engines are BYOK/BYOC: you supply API keys or self-host Searxng; PentAGI does not require a specific commercial search vendor to run (DuckDuckGo ships enabled by default).

## Related pages

<CardGroup>
  <Card title="Environment variables" href="/environment-variables">
    Full Config struct and `.env.example` reference, including search and proxy keys.
  </Card>
  <Card title="Tools and sandbox execution" href="/tools-and-sandbox">
    Tool categories, timeouts, and how network tools relate to Docker-isolated execution.
  </Card>
  <Card title="Tools reference" href="/tools-reference">
    Named registry entries, argument shapes, and agent/barrier tools.
  </Card>
  <Card title="Agents and supervision" href="/agents-and-supervision">
    Searcher specialist, limited-agent tool-call budgets, and supervision limits.
  </Card>
  <Card title="Installation" href="/installation">
    Compose stack including the scraper service and `.env` bootstrap.
  </Card>
  <Card title="Memory and knowledge" href="/memory-and-knowledge">
    Vector memory and guide/answer stores used alongside network search.
  </Card>
</CardGroup>

---

## 12. Authentication and API tokens

> Local session login, OAuth Google and GitHub, default admin account, password change, Bearer API tokens for REST and GraphQL, and permission-scoped token lifecycle.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/12-authentication-and-api-tokens.md
- Generated: 2026-07-10T07:08:23.798Z

### Source Files

- `backend/pkg/server/auth/auth_middleware.go`
- `backend/pkg/server/auth/api_token_jwt.go`
- `backend/pkg/server/services/auth.go`
- `backend/pkg/server/services/api_tokens.go`
- `frontend/src/pages/settings/settings-api-tokens.tsx`
- `backend/pkg/graph/schema.graphqls`
- `README.md`

---
title: "Authentication and API tokens"
description: "Local session login, OAuth Google and GitHub, default admin account, password change, Bearer API tokens for REST and GraphQL, and permission-scoped token lifecycle."
---

PentAGI authenticates browser clients with an encrypted `auth` session cookie and automation clients with HS256 JWT Bearer API tokens. Both paths run under `/api/v1`, share `COOKIE_SIGNING_SALT` for key derivation, and attach role privileges (`prm`) used by REST handlers and GraphQL permission checks.

## Auth model

| Mode | Client surface | Credential | Middleware | Session type (`tid`) |
|---|---|---|---|---|
| Local login | Web UI | Email + password | Cookie session after `POST /auth/login` | `local` |
| OAuth | Web UI (Google / GitHub) | OAuth code exchange | Cookie session after callback | `oauth` |
| API token | REST / GraphQL / scripts | `Authorization: Bearer <jwt>` | `AuthTokenRequired` | `api` |

```mermaid
flowchart TB
  subgraph clients [Clients]
    UI[Web UI]
    CLI[Scripts / automation]
  end

  subgraph api ["/api/v1"]
    Pub["TryAuth: /info, /auth/*"]
    Pwd["AuthUserRequired + local: PUT /user/password"]
    Priv["AuthTokenRequired: GraphQL, flows, tokens REST, …"]
  end

  subgraph store [State]
    Cookie["Cookie store name=auth"]
    JWT["JWT API tokens in api_tokens"]
    Users[(users + privileges)]
  end

  UI --> Pub
  UI --> Pwd
  UI --> Priv
  CLI --> Priv
  Pub --> Cookie
  Pwd --> Cookie
  Priv --> Cookie
  Priv --> JWT
  Cookie --> Users
  JWT --> Users
```

### Middleware selection

| Handler | Behavior |
|---|---|
| `TryAuth` | Optional: Bearer token, then cookie; continues as guest if neither succeeds |
| `AuthUserRequired` | Cookie session only (password change) |
| `AuthTokenRequired` | Bearer API token first, then cookie; 401 if neither succeeds |

Request context keys after successful auth: `uid`, `uhash`, `rid`, `tid`, `prm`, `gtm`, `exp` (and for API tokens: `cpt=automation`, `uuid`).

## Default admin account

A fresh database seed creates one local administrator:

| Field | Value |
|---|---|
| Email | `admin@pentagi.com` |
| Password | `admin` |
| Role | Admin (`role_id = 1`) |
| Status | `active` |
| `password_change_required` | `true` |

There is no public self-service sign-up on the login page. Additional local users are created by an administrator via the Users REST API (`/api/v1/users/`). If the admin password is lost, use the installer maintenance path to reset `admin@pentagi.com`.

<Warning>
Change the default password before production use. Leave `COOKIE_SIGNING_SALT` as the example value `salt` only for throwaway local demos — API token create/validate is disabled while that default remains.
</Warning>

## Local session login

:::endpoint POST /api/v1/auth/login Local email/password login
**Body**

| Field | Type | Rules |
|---|---|---|
| `mail` | string | Required; user email |
| `password` | string | Required |

**Success:** `200` with empty data object; sets the `auth` session cookie.

**Failure cases**

| HTTP | Condition |
|---|---|
| 400 | Invalid body |
| 401 | Unknown user or wrong password |
| 403 | Inactive user, or external-style role (`role_id = 100`) |
| 500 | Privilege load / session save failure |

On success the server loads privileges for the user’s role, stores them in the session, and returns:

- Cookie name: `auth`
- `HttpOnly: true`
- `Secure: true` when the request is TLS
- `SameSite: Lax`
- `Path: /api/v1`
- `MaxAge`: session timeout (4 hours)
:::

### Session claims

| Key | Meaning |
|---|---|
| `uid` | User ID |
| `uhash` | Per-user hash (invalidates sessions if the user row hash changes) |
| `rid` | Role ID |
| `tid` | User type: `local` / `oauth` / (API path sets `api`) |
| `prm` | Privilege name list |
| `gtm` | Issued-at (unix) |
| `exp` | Expires-at (unix) |
| `uuid` / `uname` | Display identity |

Cookie keys are derived from `COOKIE_SIGNING_SALT` with PBKDF2-HMAC-SHA512 (210000 iterations).

### Session lifetime and refresh

- **Timeout:** 4 hours (`SessionTimeout` in the auth service config).
- **Validation:** middleware rejects expired sessions, blocked/created users, deleted users, and `uhash` mismatches.
- **Sliding refresh:** `GET /api/v1/info` refreshes the cookie when the session is at least 5 minutes old (unless `refresh_cookie=false`). Near final expiry, a failed refresh returns auth required.

:::endpoint GET /api/v1/info Current auth and system info
Optional auth. Returns guest info when no valid session/token is present.

| Response field | Notes |
|---|---|
| `type` | `guest`, `user`, or `api` |
| `user` / `role` | Present when authenticated |
| `privileges` | Effective privilege list |
| `providers` | Configured OAuth provider names |
| `oauth` | `true` when `tid` is `oauth` |
| `issued_at` / `expires_at` | Session window |
| `develop` | Develop-mode flag |

For API-token auth (`cpt=automation`), privilege names starting with `users.`, `roles.`, `settings.user.`, or `settings.tokens.` are stripped from the `/info` response.
:::

:::endpoint GET /api/v1/auth/logout End session
Clears the cookie (`MaxAge: -1`) and redirects to `return_uri` (default `/`).
:::

## OAuth (Google and GitHub)

OAuth clients are registered only when both of these are true:

1. `PUBLIC_URL` parses successfully (redirect base).
2. Provider client ID and secret are non-empty.

| Env var | Provider |
|---|---|
| `OAUTH_GOOGLE_CLIENT_ID` / `OAUTH_GOOGLE_CLIENT_SECRET` | Google |
| `OAUTH_GITHUB_CLIENT_ID` / `OAUTH_GITHUB_CLIENT_SECRET` | GitHub |
| `PUBLIC_URL` | Redirect base (example: `https://localhost:8443`) |

Callback URL registered with the IdP:

```text
{PUBLIC_URL}/api/v1/auth/login-callback
```

Google POST callbacks also require CORS allow for `accounts.google.com` when origins are not `*`.

### Flow

<Steps>
  <Step title="Start authorize">
    Browser opens `GET /api/v1/auth/authorize?provider=google|github&return_uri=…`.

    Server sets short-lived `state` and `nonce` cookies (5 minutes). State is HMAC-signed JSON including `provider`, optional `return_uri`, and expiry. Google uses `SameSite=None` (POST form callback); GitHub uses `SameSite=Lax` (GET callback).
  </Step>
  <Step title="IdP callback">
    - Google: `POST /api/v1/auth/login-callback` (code + state in form body)
    - GitHub: `GET /api/v1/auth/login-callback?code=…&state=…`

    State cookie must match; signature and expiry are checked.
  </Step>
  <Step title="User provision">
    Email is resolved from the OAuth token. If no `type=oauth` user exists for that email, one is created with:

    - `role_id` = User (`2`)
    - `status` = `active`
    - `type` = `oauth`
    - default user preferences row
  </Step>
  <Step title="Session">
    Same cookie session shape as local login. Temporary `state`/`nonce` cookies are cleared. Redirect adds `status=success` when `return_uri` was provided.
  </Step>
</Steps>

The UI launches OAuth in a popup to `/api/v1/auth/authorize?provider=…&return_uri=/oauth/result`. Available providers are listed in `/info` → `providers`.

<Note>
OAuth users cannot call `PUT /user/password` (local type required). Password change UI is limited to `type === 'local'`.
</Note>

## Password change

:::endpoint PUT /api/v1/user/password Change current local user password
**Guards:** `AuthUserRequired` + local user type (`tid=local`).

**Body**

| Field | Type | Rules |
|---|---|---|
| `current_password` | string | Required; min 5, max 100; must differ from new password |
| `password` | string | Required; max 100; strong-password (`stpass`) |
| `confirm_password` | string | Must equal `password` |

**Strong password (`stpass`)** — either:

- length **> 15**, or
- length **≥ 8** and contains all of: digit, lowercase, uppercase, special from `!@#$&*`

Frontend mirrors the same rules in the password-change form.

**Effects**

- bcrypt-hashes the new password
- sets `password_change_required = false`
- returns `200` empty success body

**Errors**

| HTTP | Case |
|---|---|
| 400 | Validation failure / invalid new password |
| 403 | Wrong current password, or not a local user |
| 404 | User not found |
:::

Default admin seeds with `password_change_required=true`. The login page can show an “Update Password” step after first local login (skippable in the UI, but production instances should complete the change).

## Bearer API tokens

API tokens are long-lived JWTs for programmatic access to the private REST surface and GraphQL. Metadata lives in `api_tokens`; the secret JWT string is returned **only at creation**.

### Prerequisites

| Requirement | Detail |
|---|---|
| `COOKIE_SIGNING_SALT` | Must be set and **not** equal to `salt` or empty |
| Auth for create | Cookie session (`local` or `oauth`) via UI / GraphQL; REST create requires authenticated principal |
| Privileges (GraphQL) | `settings.tokens.create` / `view` / `edit` / `delete` / `subscribe` |
| Admin privilege | `settings.tokens.admin` — Admin role only; list/manage across users |

Role privilege seeds:

| Privilege | Admin | User |
|---|---|---|
| `settings.tokens.create` | ✓ | ✓ |
| `settings.tokens.view` | ✓ | ✓ |
| `settings.tokens.edit` | ✓ | ✓ |
| `settings.tokens.delete` | ✓ | ✓ |
| `settings.tokens.subscribe` | ✓ | ✓ |
| `settings.tokens.admin` | ✓ | — |

### Token lifecycle

```mermaid
stateDiagram-v2
  [*] --> active: create
  active --> revoked: update status=revoked
  active --> expired: created_at + ttl elapsed
  revoked --> active: update status=active
  active --> [*]: soft delete
  revoked --> [*]: soft delete
  expired --> [*]: soft delete
```

| Status | Stored in DB? | Meaning |
|---|---|---|
| `active` | yes | Usable if JWT not expired and TTL window not elapsed |
| `revoked` | yes | Rejected by middleware |
| `expired` | derived in API responses | `created_at + ttl` in the past while status was still `active` |

TTL constraints: **60 … 94_608_000** seconds (1 minute … 3 years). Optional `name` is unique per user among non-deleted tokens (max 100 REST / 255 UI). Soft delete sets `deleted_at`.

### JWT shape

Signed with HS256; signing key = PBKDF2 from `COOKIE_SIGNING_SALT`.

| Claim | Description |
|---|---|
| `tid` | Public token ID (10-char id, not the JWT) |
| `uid` | Owner user ID |
| `rid` | Role ID snapshot |
| `uhash` | User hash at issue time |
| `sub` | `api_token` |
| `iat` / `exp` | Issued / expires (from TTL) |

Validation path:

1. `Authorization: Bearer <token>` present
2. Salt is not default
3. JWT signature + expiry valid
4. Row exists, not deleted, `status=active`
5. User not blocked/deleted; `uhash` still matches
6. Privileges = role privileges **plus** `pentagi.automation`

Token status lookups are cached for 5 minutes; create/update/delete invalidates cache.

### REST token endpoints

All under `/api/v1/tokens` with `AuthTokenRequired` (cookie or Bearer).

| Method | Path | Behavior |
|---|---|---|
| `POST` | `/tokens/` | Create; body `{ "name"?: string, "ttl": number }`; returns `APITokenWithSecret` including `token` |
| `GET` | `/tokens/` | List; non-admins see own tokens only |
| `GET` | `/tokens/:tokenID` | Get one |
| `PUT` | `/tokens/:tokenID` | Update `name` and/or `status` (`active` / `revoked`; `expired` input is stored as revoked) |
| `DELETE` | `/tokens/:tokenID` | Soft delete |

Create is rejected with “token creation is disabled with default salt” when salt is unset or `salt`.

### GraphQL token surface

Token management mutations/queries require a **user session** (`local` or `oauth`). API tokens (`tid=api`) cannot create, update, delete, list, or subscribe to token events.

| Operation | Permission |
|---|---|
| `createAPIToken(input: { name, ttl })` | `settings.tokens.create` |
| `apiTokens` / `apiToken(tokenId)` | `settings.tokens.view` |
| `updateAPIToken(tokenId, input)` | `settings.tokens.edit` |
| `deleteAPIToken(tokenId)` | `settings.tokens.delete` |
| `apiTokenCreated` / `Updated` / `Deleted` | `settings.tokens.subscribe` |

Types: `APIToken`, `APITokenWithSecret` (includes `token` once), `TokenStatus`, `CreateAPITokenInput`, `UpdateAPITokenInput`.

### UI

Settings → **API tokens** (`/settings/api-tokens`):

- Create with name + expiration date (UI converts to TTL, minimum 60s)
- List with active / revoked / expired display
- Edit name/status, revoke, delete
- Copy secret once after create
- Links to GraphQL Playground and Swagger under `/api/v1`

### Using a token

```bash
# REST
curl -sk \
  -H "Authorization: Bearer $PENTAGI_TOKEN" \
  https://localhost:8443/api/v1/info

# GraphQL
curl -sk \
  -H "Authorization: Bearer $PENTAGI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ providers { name type } }"}' \
  https://localhost:8443/api/v1/graphql
```

API-token requests receive the owner’s role privileges plus `pentagi.automation`. GraphQL permission checks use that privilege list the same way as cookie sessions, except token-management operations explicitly reject non-user sessions.

## Configuration

| Variable | Role | Notes |
|---|---|---|
| `COOKIE_SIGNING_SALT` | Cookie + JWT key material | Change from `salt`; required for API tokens |
| `PUBLIC_URL` | OAuth redirect base | Required for OAuth client registration |
| `OAUTH_GOOGLE_CLIENT_ID` / `SECRET` | Google SSO | Optional |
| `OAUTH_GITHUB_CLIENT_ID` / `SECRET` | GitHub SSO | Optional |

Session duration is fixed at **4 hours** in server wiring (not an env key today).

## Failure modes and troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| Login 401 | Wrong credentials or no local password | Use `admin@pentagi.com` / `admin` on fresh install; reset via installer if needed |
| Login 403 inactive | User status not `active` | Unblock/activate user |
| OAuth “provider not initialized” | Missing client ID/secret or invalid `PUBLIC_URL` | Set env vars and restart |
| OAuth state errors | Expired/mismatched state cookie (>5 min) or cookie SameSite issues | Retry quickly; ensure HTTPS for Google (`SameSite=None` + Secure) |
| Password change 403 | OAuth user or wrong current password | Only `local` users; re-enter current password |
| Token create fails (default salt) | `COOKIE_SIGNING_SALT` empty or `salt` | Set a strong unique salt and redeploy |
| Bearer 401 “token validation disabled” | Same default salt | Same as above |
| Bearer 401 revoked / not found | Soft-deleted or revoked token | Create a new token |
| Bearer 401 hash mismatch | User hash changed or token from another install | Re-issue tokens after salt/user changes |
| GraphQL token mutations unauthorized | Called with API token instead of browser session | Manage tokens from UI or cookie session |
| Session dropped after salt change | Cookie keys re-derived | Users must log in again; re-issue API tokens |

## Operational checklist

<Steps>
  <Step title="Harden signing salt">
    Set `COOKIE_SIGNING_SALT` to a unique value before enabling automation tokens.
  </Step>
  <Step title="First admin login">
    Open `https://localhost:8443`, sign in as `admin@pentagi.com` / `admin`, change password under the forced or settings flow.
  </Step>
  <Step title="Optional OAuth">
    Configure `PUBLIC_URL` and Google/GitHub client credentials; confirm providers appear on the login page and in `/info`.
  </Step>
  <Step title="Issue API tokens">
    Settings → API tokens (or GraphQL `createAPIToken`). Copy the secret once. Verify with `GET /api/v1/info` using `Authorization: Bearer …`.
  </Step>
  <Step title="Revoke when done">
    Set status to `revoked` or delete the token; cache invalidation is immediate for that `token_id`.
  </Step>
</Steps>

## Related pages

<CardGroup cols={2}>
  <Card title="Quickstart" href="/quickstart">
    First login, change default admin password, and run a flow.
  </Card>
  <Card title="Interactive installer" href="/installer">
    Guided deploy and admin password reset maintenance paths.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    `COOKIE_SIGNING_SALT`, `PUBLIC_URL`, and OAuth keys.
  </Card>
  <Card title="REST API" href="/rest-api">
    Full Gin route map including auth, users, and tokens.
  </Card>
  <Card title="GraphQL API" href="/graphql-api">
    Queries, mutations, and subscriptions for API tokens and live streams.
  </Card>
  <Card title="Overview" href="/overview">
    Product surface: UI, REST/GraphQL under `/api/v1`, and first-flow path.
  </Card>
</CardGroup>

---

## 13. Observability and Langfuse

> Optional OpenTelemetry path to Grafana, VictoriaMetrics, Jaeger, and Loki; Langfuse LLM analytics compose stack; OTEL_HOST and LANGFUSE_* keys; what each stack measures.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/13-observability-and-langfuse.md
- Generated: 2026-07-10T07:07:26.894Z

### Source Files

- `docker-compose-observability.yml`
- `docker-compose-langfuse.yml`
- `backend/docs/observability.md`
- `backend/docs/langfuse.md`
- `observability/otel/config.yml`
- `backend/pkg/config/config.go`
- `.env.example`

---
title: "Observability and Langfuse"
description: "Optional OpenTelemetry path to Grafana, VictoriaMetrics, Jaeger, and Loki; Langfuse LLM analytics compose stack; OTEL_HOST and LANGFUSE_* keys; what each stack measures."
---

PentAGI exposes two independent, optional observability paths: **OpenTelemetry** (`OTEL_HOST`) for platform traces, logs, and metrics into Grafana/VictoriaMetrics/Jaeger/Loki, and **Langfuse** (`LANGFUSE_*`) for LLM and agent analytics. Both are disabled when their env keys are empty; `cmd/pentagi` treats `ErrNotConfigured` as non-fatal and continues with no-op clients.

## What each stack measures

| Stack | Signal focus | Primary UIs | Compose file |
| --- | --- | --- | --- |
| OpenTelemetry platform stack | App spans, structured logs, process/Go runtime metrics, host/container scrapes | Grafana `:3000`, Jaeger query, Loki Explore | `docker-compose-observability.yml` |
| Langfuse | LLM generations, agent/tool/chain observations, token usage, scores, flow sessions | Langfuse Web `:4000` | `docker-compose-langfuse.yml` |

They are complementary: OTEL answers “is the service healthy and where is latency?”, Langfuse answers “what did the models and agents do, with which tokens and outcomes?”.

## Architecture

```mermaid
flowchart LR
  subgraph app [PentAGI process]
    Main["cmd/pentagi"]
    Obs["pkg/observability Observer"]
    Logrus["logrus hook"]
    Main --> Obs
    Logrus --> Obs
  end

  subgraph otelPath [OpenTelemetry path]
    OTEL["otelcol hostname<br/>gRPC :8148 / HTTP :4318"]
    VM["VictoriaMetrics"]
    Jaeger["Jaeger + ClickHouse clickstore"]
    Loki["Loki"]
    Grafana["Grafana"]
    NodeExp["node-exporter"]
    cAdvisor["cadvisor"]
    OTEL --> VM
    OTEL --> Jaeger
    OTEL --> Loki
    NodeExp --> OTEL
    cAdvisor --> OTEL
    VM --> Grafana
    Jaeger --> Grafana
    Loki --> Grafana
  end

  subgraph lfPath [Langfuse path]
    LFWeb["langfuse-web :4000"]
    LFWorker["langfuse-worker"]
    LFPG["langfuse-postgres"]
    LFCH["langfuse-clickhouse"]
    LFRedis["langfuse-redis"]
    LFMinio["langfuse-minio"]
    LFWeb --> LFPG
    LFWeb --> LFCH
    LFWeb --> LFRedis
    LFWeb --> LFMinio
    LFWorker --> LFPG
    LFWorker --> LFCH
    LFWorker --> LFRedis
    LFWorker --> LFMinio
  end

  Obs -->|"OTEL_HOST gRPC OTLP"| OTEL
  Obs -->|"LANGFUSE_BASE_URL HTTP"| LFWeb
  LFWeb -.->|"optional LANGFUSE_OTEL_EXPORTER_OTLP_ENDPOINT"| OTEL
```

Collector pipelines (`observability/otel/config.yml`):

| Pipeline | Receivers | Exporters |
| --- | --- | --- |
| `traces` | OTLP | OTLP → `jaeger:4317` |
| `logs` | OTLP | OTLP/HTTP → `http://loki:3100/otlp` |
| `metrics` | OTLP + Prometheus scrapes (node-exporter, cAdvisor, clickstore, jaeger, loki, pgexporter, host Docker engine) | Prometheus remote write → `http://victoriametrics:8428/api/v1/write` |

Grafana is pre-provisioned with datasources **VictoriaMetrics**, **Jaeger** (trace→log to Loki), and **Loki** (log→trace to Jaeger).

## Prerequisites

- Core stack networks exist: run `docker-compose.yml` first so external networks `pentagi-network`, `observability-network`, and `langfuse-network` are created.
- `.env` copied from `.env.example` and loaded by Compose.
- Overlay files present next to the core compose file (or fetched from the repo).

<Warning>
If Compose fails with missing `pentagi-network`, `observability-network`, or `langfuse-network`, start the core stack first, then add overlays.
</Warning>

## Enable the OpenTelemetry stack

<Steps>
  <Step title="Set the collector endpoint for PentAGI">
    In `.env`, point the app at the collector **hostname** and **gRPC** port (the telemetry client dials gRPC, not HTTP):

```bash
OTEL_HOST=otelcol:8148
```

    The collector service is named `otel` in Compose; DNS hostname is `otelcol`. Host-published ports default to `127.0.0.1:8148` (gRPC) and `127.0.0.1:4318` (HTTP).
  </Step>
  <Step title="Start core + observability overlay">
```bash
docker compose -f docker-compose.yml -f docker-compose-observability.yml up -d
```
  </Step>
  <Step title="Verify">
    - Open Grafana at `http://localhost:3000` (bind via `GRAFANA_LISTEN_IP` / `GRAFANA_LISTEN_PORT`, default host `127.0.0.1:3000`).
    - Confirm datasources VictoriaMetrics, Jaeger, and Loki resolve.
    - Restart PentAGI after setting `OTEL_HOST` so `NewTelemetryClient` connects; empty `OTEL_HOST` yields `ErrNotConfigured` and skips exporters.
  </Step>
</Steps>

### Observability services

| Service | Role | Notes |
| --- | --- | --- |
| `otel` | OTLP collector (contrib `0.116.1`) | Pipelines for traces, logs, metrics; joins `observability-network`, `langfuse-network`, `pentagi-network` |
| `grafana` | Dashboards / Explore | Config and dashboards under `observability/grafana/` |
| `victoriametrics` | Metrics TSDB | Remote-write target for collector metrics |
| `jaeger` | Trace UI / query | ClickHouse plugin storage via `clickstore` |
| `clickstore` | Trace storage (ClickHouse) | DB `jaeger`, user/password `clickhouse` in compose defaults |
| `loki` | Log store | Config `observability/loki/config.yml` |
| `node-exporter` | Host metrics scrape | Scraped by collector Prometheus receiver |
| `cadvisor` | Container metrics scrape | Docker-only mode |

### App-side OTEL behavior

On startup (`backend/cmd/pentagi/main.go`):

1. `NewTelemetryClient(ctx, cfg)` — requires `OTEL_HOST`; exports logs, traces, and metrics over gRPC OTLP with ~30s batch intervals.
2. `InitObserver` attaches a logrus hook for Debug/Info/Warn/Error so log lines become span events and OTEL log records when a span is active.
3. `StartProcessMetricCollect` and `StartGoRuntimeMetricCollect` publish process and Go runtime metrics when telemetry is live.

HTTP (Gin) and GraphQL middleware emit structured request logs with context for correlation. Profiling is separate: `pkg/observability/profiling` serves pprof on `:7777` under `/profiler/*` (heap, CPU, goroutine, mutex, trace).

## Enable the Langfuse stack

<Steps>
  <Step title="Harden Langfuse infrastructure secrets">
    Set non-default credentials in `.env` for Postgres, ClickHouse, Redis, MinIO/S3, salt, encryption key, and NextAuth before exposing beyond localhost. Generate a 32-byte hex encryption key with `openssl rand -hex 32`.
  </Step>
  <Step title="Wire PentAGI → Langfuse API">
```bash
LANGFUSE_BASE_URL=http://langfuse-web:3000
LANGFUSE_PROJECT_ID=          # default: LANGFUSE_INIT_PROJECT_ID
LANGFUSE_PUBLIC_KEY=          # default: LANGFUSE_INIT_PROJECT_PUBLIC_KEY
LANGFUSE_SECRET_KEY=          # default: LANGFUSE_INIT_PROJECT_SECRET_KEY
```

    Align project ID and keys with the Langfuse init project (`LANGFUSE_INIT_*`).
  </Step>
  <Step title="Start core + Langfuse overlay">
```bash
docker compose -f docker-compose.yml -f docker-compose-langfuse.yml up -d
```
  </Step>
  <Step title="Open Langfuse UI">
    Browse `http://localhost:4000` (default bind `LANGFUSE_LISTEN_IP`/`LANGFUSE_LISTEN_PORT` → `127.0.0.1:4000`).

    Sign in with `LANGFUSE_INIT_USER_EMAIL` / `LANGFUSE_INIT_USER_PASSWORD` (compose demo defaults include `admin@pentagi.com`; change them).
  </Step>
</Steps>

### Langfuse services

| Service | Role |
| --- | --- |
| `langfuse-web` | UI + API (image `langfuse/langfuse:3`), on `langfuse-network` and `pentagi-network` |
| `langfuse-worker` | Background ingestion/processing |
| `postgres` (`langfuse-postgres`) | Primary relational store |
| `clickhouse` (`langfuse-clickhouse`) | Analytics queries (reads prefer ClickHouse in default env) |
| `redis` (`langfuse-redis`) | Queue/cache |
| `minio` (`langfuse-minio`) | S3-compatible media/event storage |

### App-side Langfuse behavior

`NewLangfuseClient` requires `LANGFUSE_BASE_URL`. Client options use public/secret keys and project ID; the observer batches with defaults: send interval **10s**, timeout **10s**, queue size **10**, max attempts **3**, and attaches the binary release version.

When base URL is empty, the process keeps a no-op path. Observations are non-blocking and fail soft so LLM analytics outages do not stop flows.

Observation types supported by `pkg/observability/langfuse`: span, event, generation, score, agent, tool, chain, retriever, evaluator, embedding, guardrail. Inputs/outputs convert LangChainGo message shapes to OpenAI-compatible JSON for Langfuse UI (tool calls, thinking, multimodal content, table-friendly tool results).

Typical flow worker metadata: trace name like `{id} flow worker`, user id, session `flow-{id}`, tags such as `controller`, and flow/user metadata.

## Run both stacks (and Graphiti)

```bash
docker compose \
  -f docker-compose.yml \
  -f docker-compose-langfuse.yml \
  -f docker-compose-observability.yml \
  up -d
```

Optional third overlay: `docker-compose-graphiti.yml` (knowledge graph; separate from observability).

To export Langfuse’s own OTEL into the platform collector:

```bash
LANGFUSE_OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4318
```

Shell aliases from the project README:

```bash
alias pentagi="docker compose -f docker-compose.yml -f docker-compose-langfuse.yml -f docker-compose-graphiti.yml -f docker-compose-observability.yml"
alias pentagi-up="... up -d"
alias pentagi-down="... down"
```

## Configuration reference

### PentAGI process (required to enable each path)

<ParamField body="OTEL_HOST" type="string">
OpenTelemetry collector endpoint for gRPC OTLP. Example: `otelcol:8148`. Empty disables telemetry client (`TelemetryEndpoint` in `Config`).
</ParamField>

<ParamField body="LANGFUSE_BASE_URL" type="string">
Langfuse API base URL. Example: `http://langfuse-web:3000`. Empty disables Langfuse client.
</ParamField>

<ParamField body="LANGFUSE_PROJECT_ID" type="string">
Langfuse project id; should match `LANGFUSE_INIT_PROJECT_ID` for the demo project.
</ParamField>

<ParamField body="LANGFUSE_PUBLIC_KEY" type="string">
Project public key (`pk-lf-...`). Treated as a secret redaction candidate in config sanitization.
</ParamField>

<ParamField body="LANGFUSE_SECRET_KEY" type="string">
Project secret key (`sk-lf-...`). Treated as a secret redaction candidate.
</ParamField>

### Host bind ports (optional)

| Variable | Purpose | Compose default bind |
| --- | --- | --- |
| `GRAFANA_LISTEN_IP` / `GRAFANA_LISTEN_PORT` | Grafana host bind | `127.0.0.1:3000` |
| `OTEL_GRPC_LISTEN_IP` / `OTEL_GRPC_LISTEN_PORT` | Collector gRPC host bind | `127.0.0.1:8148` |
| `OTEL_HTTP_LISTEN_IP` / `OTEL_HTTP_LISTEN_PORT` | Collector HTTP host bind | `127.0.0.1:4318` |
| `LANGFUSE_LISTEN_IP` / `LANGFUSE_LISTEN_PORT` | Langfuse Web host bind | `127.0.0.1:4000` |
| `LANGFUSE_NEXTAUTH_URL` | Public URL for Langfuse auth | `http://localhost:4000` |

### Langfuse infrastructure (high-value secrets)

Change before any non-local exposure:

| Area | Variables |
| --- | --- |
| DB | `LANGFUSE_POSTGRES_USER`, `LANGFUSE_POSTGRES_PASSWORD`, `LANGFUSE_POSTGRES_DB` |
| ClickHouse | `LANGFUSE_CLICKHOUSE_USER`, `LANGFUSE_CLICKHOUSE_PASSWORD`, `LANGFUSE_CLICKHOUSE_URL`, `LANGFUSE_CLICKHOUSE_MIGRATION_URL` |
| Redis | `LANGFUSE_REDIS_HOST`, `LANGFUSE_REDIS_PORT`, `LANGFUSE_REDIS_AUTH` |
| S3/MinIO | `LANGFUSE_S3_BUCKET`, `LANGFUSE_S3_ACCESS_KEY_ID`, `LANGFUSE_S3_SECRET_ACCESS_KEY`, `LANGFUSE_S3_ENDPOINT` |
| Crypto / auth | `LANGFUSE_SALT`, `LANGFUSE_ENCRYPTION_KEY`, `LANGFUSE_NEXTAUTH_SECRET` |
| Bootstrap project | `LANGFUSE_INIT_ORG_*`, `LANGFUSE_INIT_PROJECT_*`, `LANGFUSE_INIT_USER_*` |
| Optional OTEL from Langfuse | `LANGFUSE_OTEL_EXPORTER_OTLP_ENDPOINT`, `LANGFUSE_OTEL_SERVICE_NAME` |

Full key inventory lives under [Environment variables](/environment-variables).

## Repository layout

:::files
observability/
  otel/config.yml              # collector pipelines
  grafana/config/              # grafana.ini + provisioning
  grafana/dashboards/          # home, node, docker, pentagi service
  loki/config.yml
  jaeger/                      # clickhouse plugin binaries + config
  clickhouse/prometheus.xml
docker-compose-observability.yml
docker-compose-langfuse.yml
backend/pkg/observability/     # Observer, OTEL client, Langfuse SDK
backend/docs/observability.md
backend/docs/langfuse.md
:::

## Failure modes and operations

| Symptom | Likely cause | Mitigation |
| --- | --- | --- |
| Overlay compose: network not found | Core stack not started | `docker compose -f docker-compose.yml up -d` first |
| No traces/metrics in Grafana | `OTEL_HOST` empty or wrong host/port | Set `otelcol:8148`; restart PentAGI; confirm collector container `otel` healthy |
| Fatal on startup mentioning telemetry | Collector unreachable with `OTEL_HOST` set | Only empty endpoint is soft-skipped; a set but dead endpoint fails `NewTelemetryClient` hard |
| No Langfuse traces | Missing `LANGFUSE_BASE_URL` or key/project mismatch | Match init project keys; open Langfuse UI and confirm project |
| Langfuse UI login fails | Init user/password not rotated or NextAuth URL wrong | Align `LANGFUSE_INIT_USER_*` and `LANGFUSE_NEXTAUTH_URL` with access URL |
| Logs without parent traces | `logrus` without request context | Always `logrus.WithContext(ctx)` so the hook attaches to the active span |
| Container registry pull fails for cAdvisor | `gcr.io/cadvisor/cadvisor` not reachable | Mirror/proxy that registry or skip observability overlay in restricted networks |

<Tip>
Use Grafana Explore (Loki ↔ Jaeger links) for platform debugging, and Langfuse sessions/traces for multi-agent LLM workflows and token usage. Keep both optional in air-gapped or BYOK-only installs by leaving `OTEL_HOST` and `LANGFUSE_BASE_URL` empty.
</Tip>

## Next

<CardGroup>
  <Card title="Installation" href="/installation">
    Core Compose, `.env`, SSL/volumes, and how observability, Langfuse, and Graphiti overlays attach.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Authoritative `Config` and `.env.example` keys including OTEL, Langfuse, and related defaults.
  </Card>
  <Card title="Knowledge graph" href="/knowledge-graph">
    Optional Graphiti/Neo4j overlay (`docker-compose-graphiti.yml`) orthogonal to OTEL/Langfuse.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    BYOK providers whose generations appear in Langfuse when analytics is enabled.
  </Card>
  <Card title="Development and testing" href="/development-and-testing">
    Local backend build/test paths and contributor compose usage.
  </Card>
</CardGroup>

---

## 14. Knowledge graph

> Enable Graphiti with Neo4j via docker-compose-graphiti.yml, GRAPHITI_* settings, graphiti_search tool behavior, and failure modes when the graph backend is down.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/14-knowledge-graph.md
- Generated: 2026-07-10T07:08:58.826Z

### Source Files

- `docker-compose-graphiti.yml`
- `backend/pkg/graphiti/client.go`
- `backend/pkg/tools/graphiti_search.go`
- `backend/pkg/config/config.go`
- `.env.example`
- `README.md`

---
title: "Knowledge graph"
description: "Enable Graphiti with Neo4j via docker-compose-graphiti.yml, GRAPHITI_* settings, graphiti_search tool behavior, and failure modes when the graph backend is down."
---

PentAGI’s optional knowledge graph is a **beta** Graphiti + Neo4j stack. Agents ingest episodes into a temporal graph and query them with the `graphiti_search` tool (`backend/pkg/tools/graphiti_search.go`), backed by `backend/pkg/graphiti.Client` and the `vxcontrol/graphiti-go-client` library. The feature is off unless `GRAPHITI_ENABLED=true`, `GRAPHITI_URL` is set, and the Graphiti HTTP service is reachable at startup health check.

<Warning>
Graphiti entity extraction uses a single OpenAI-compatible endpoint (`OPEN_AI_KEY` / `OPEN_AI_SERVER_URL`), not Anthropic, Gemini, Bedrock, DeepSeek, GLM, Kimi, or Qwen credentials configured for flows. Leave Graphiti disabled if that endpoint is unavailable.
</Warning>

## Architecture

```mermaid
flowchart LR
  subgraph pentagi [PentAGI runtime]
    FP[flowProvider<br/>storeToGraphiti]
    Tools[graphiti_search tool]
    Client[pkg/graphiti.Client]
  end
  subgraph graphiti_stack [docker-compose-graphiti.yml]
    API[graphiti :8000]
    Neo[neo4j :7687 / :7474]
  end
  FP -->|AddMessages group_id flow-N| Client
  Tools -->|search GroupID flow-N| Client
  Client -->|HTTP| API
  API --> Neo
```

| Layer | Role |
| --- | --- |
| `flowProvider` | Writes agent responses and tool executions as Graphiti messages under `group_id=flow-{id}` |
| `graphiti_search` | Read path for seven search types, same group scoping |
| `graphiti` container | `vxcontrol/graphiti:latest` API on port `8000` |
| `neo4j` container | Graph store (`neo4j:5.26.2`), Bolt `7687`, Browser `7474` |

When disabled or failed at init, pgvector memory and the rest of the stack continue; only graph features are skipped.

## Enable the stack

### Prerequisites

- Core compose already running so external network `pentagi-network` exists
- OpenAI-compatible API key for Graphiti extraction (`OPEN_AI_KEY`)
- Optional: change default Neo4j password (`NEO4J_PASSWORD`, default `devpassword`)

### Environment

Set in `.env` (see `.env.example`):

```bash
GRAPHITI_ENABLED=true
GRAPHITI_TIMEOUT=30
GRAPHITI_URL=http://graphiti:8000
GRAPHITI_MODEL_NAME=gpt-5-mini

NEO4J_USER=neo4j
NEO4J_DATABASE=neo4j
NEO4J_PASSWORD=devpassword
NEO4J_URI=bolt://neo4j:7687

OPEN_AI_KEY=your_openai_api_key
# OPEN_AI_SERVER_URL defaults to https://api.openai.com/v1
```

| Variable | Consumed by | Default | Purpose |
| --- | --- | --- | --- |
| `GRAPHITI_ENABLED` | PentAGI `Config` | `false` | Master switch |
| `GRAPHITI_URL` | PentAGI client | empty | Graphiti base URL; required with enabled |
| `GRAPHITI_TIMEOUT` | PentAGI client | `30` (seconds) | HTTP / store timeout |
| `GRAPHITI_MODEL_NAME` | Graphiti container only | `gpt-5-mini` | Extraction model (`MODEL_NAME` in compose) |
| `NEO4J_*` | Graphiti/Neo4j containers | see compose | Graph DB auth and Bolt URI |
| `OPEN_AI_KEY` / `OPEN_AI_SERVER_URL` | Mapped to container `OPENAI_API_KEY` / `OPENAI_BASE_URL` | API OpenAI default | Entity extraction LLM |

PentAGI reads only `GRAPHITI_ENABLED`, `GRAPHITI_TIMEOUT`, and `GRAPHITI_URL` in `backend/pkg/config/config.go`. Model name and Neo4j credentials are compose-side for the Graphiti service.

### Compose

```bash
# Core first if pentagi-network is missing
docker compose -f docker-compose.yml up -d

docker compose -f docker-compose.yml -f docker-compose-graphiti.yml up -d
```

`docker-compose-graphiti.yml` services:

| Service | Image | Host ports | Health |
| --- | --- | --- | --- |
| `neo4j` | `neo4j:5.26.2` | `127.0.0.1:7474`, `127.0.0.1:7687` | HTTP `7474` |
| `graphiti` | `vxcontrol/graphiti:latest` | `127.0.0.1:8000` | `GET /healthcheck` on `:8000` |

`graphiti` depends on healthy `neo4j`, joins `pentagi-network`, and uses volume `neo4j_data`.

Installer support: TUI Graphiti form can set embedded (`http://graphiti:8000`), external URL, or disabled modes (`GRAPHITI_ENABLED` + `GRAPHITI_URL`).

### Verify

```bash
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml ps graphiti neo4j
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml logs -f graphiti

# Optional UIs
# Neo4j Browser: http://localhost:7474
# Graphiti Swagger: http://localhost:8000/docs
```

Client init calls Graphiti `HealthCheck()` when enabled with a non-empty URL. On failure, providers log a warning and continue with a disabled client:

```text
failed to initialize graphiti client, continuing without it
```

## Runtime wiring

1. **Provider controller** constructs `graphiti.NewClient(url, timeout, enabled && url != "")`.
2. Client is shared into flow tools and `flowProvider`.
3. **Ingest** (when enabled): after agent turns, `storeAgentResponseToGraphiti` and `storeToolExecutionToGraphiti` render templates under `backend/pkg/templates/graphiti/` and call `AddMessages` with `GroupID: flow-{id}`.
4. **Search**: `graphiti_search` is registered only if `IsAvailable()` (client non-nil and `IsEnabled()`).

Agents that receive the tool when available: **coder**, **pentester**, **memorist**, **enricher**. Prompts inject `GraphitiEnabled` and `GraphitiSearchToolName` (`graphiti_search`) so protocol blocks appear only when the client is live.

### What is stored

| Episode kind | Source | Content |
| --- | --- | --- |
| Agent response | Agent completion text | Rendered via `agent_response.tmpl` with agent type, task/subtask |
| Tool execution | Tool call + output (or error) | Rendered via `tool_execution.tmpl`; status success/failure |

Store failures are logged as warnings and do not fail the agent turn. `AddMessages` is a no-op when the client is disabled.

### Scoping

Write and search use `group_id = flow-{id}`. Graph search context is per active flow, not a global cross-flow library (unlike the shared English-indexed pgvector store). Soft-deleted flows remain separate from optional graph reuse workflows.

## `graphiti_search` tool

| Property | Value |
| --- | --- |
| Tool name | `graphiti_search` |
| Registry type | `SearchVectorDbToolType` |
| Args type | `GraphitiSearchAction` |
| Availability | Client enabled after successful init |

### Arguments

| Field | Required | Notes |
| --- | --- | --- |
| `search_type` | yes | One of seven types below |
| `query` | yes | English technical-channel NL query (graph indexed in English) |
| `message` | yes | Engagement-language 1–2 sentence log line |
| `max_results` | no | Per-type defaults if ≤ 0 |
| `time_start` / `time_end` | for `temporal_window` | RFC3339 / ISO 8601; `time_end` after `time_start` |
| `center_node_uuid` | for `entity_relationships` | Center entity UUID |
| `max_depth` | no | Default `2`, capped at `3` |
| `node_labels` | for `entity_by_label` (required); optional filters elsewhere | e.g. `IP_ADDRESS`, `SERVICE`, `VULNERABILITY` |
| `edge_types` | no | e.g. `HAS_PORT`, `EXPLOITS` |
| `diversity_level` | no | `low` \| `medium` \| `high` (default `medium`) |
| `min_mentions` | no | Default `2` for `successful_tools` |
| `recency_window` | no | `1h` \| `6h` \| `24h` \| `7d` (default `24h`) |

### Search types and defaults

| `search_type` | Purpose | Default `max_results` | Extra params |
| --- | --- | --- | --- |
| `temporal_window` | Time-bounded facts, entities, episodes | 15 | `time_start`, `time_end` |
| `entity_relationships` | Graph traversal from a node | 20 | `center_node_uuid`, `max_depth`, labels/edges |
| `diverse_results` | MMR-style diverse communities/facts | 10 | `diversity_level` |
| `episode_context` | Agent responses and tool records | 10 | — |
| `successful_tools` | Proven tool/technique patterns | 15 | `min_mentions` |
| `recent_context` | Latest findings in a window | 10 | `recency_window` |
| `entity_by_label` | Inventory by node labels | 25 | `node_labels` (required) |

Results are formatted as Markdown (facts/edges, entities/nodes with UUIDs, episodes with content). Empty sets produce explicit “no results” strings.

### Example calls

```json
{
  "search_type": "recent_context",
  "query": "recent pentester findings about web application open ports",
  "recency_window": "6h",
  "message": "Checking Graphiti for recent recon on the target app."
}
```

```json
{
  "search_type": "temporal_window",
  "query": "all reconnaissance activities",
  "time_start": "2024-01-01T00:00:00Z",
  "time_end": "2024-01-01T23:59:59Z",
  "message": "Pulling recon episodes from the first day of the engagement."
}
```

```json
{
  "search_type": "entity_relationships",
  "query": "related vulnerabilities and services",
  "center_node_uuid": "<uuid-from-prior-search>",
  "max_depth": 2,
  "message": "Expanding relationships from the discovered host entity."
}
```

## Relationship to pgvector memory

| | Graphiti | pgvector memory tools |
| --- | --- | --- |
| Role | Episodic: what happened in this flow | Reusable knowledge: guides, code, Q&A |
| Index language | English queries | English queries |
| Scope | `flow-{id}` group | Optional task/subtask filters; shared store |
| Primary tool | `graphiti_search` | `search_in_memory`, store/search guide/code |

Memorist prompts (when Graphiti is on) instruct: search Graphiti first for execution history, then vector DB for reusable knowledge.

## Failure modes

| Condition | Runtime behavior |
| --- | --- |
| `GRAPHITI_ENABLED=false` or empty `GRAPHITI_URL` | Client created disabled; no health check; `AddMessages` no-op; tool not registered |
| Health check fails at provider init | Warning log; empty disabled client; same as off |
| Client disabled, search API called | Methods return `graphiti is not enabled` |
| Tool invoked while unavailable | Soft message: `Graphiti knowledge graph is not enabled. No historical context or memory data is available for this search.` (no hard error) |
| Store path error while enabled | Warning + Langfuse evaluator error; agent continues |
| Search transport/API error while enabled | Hard tool error: e.g. `temporal window search failed: ...` |
| Invalid args | Hard errors: missing `query`/`search_type`, bad times, unknown `search_type`, invalid `diversity_level`/`recency_window`, missing `center_node_uuid`/`node_labels` where required |
| Graphiti/Neo4j down after successful init | Ingest warns; searches fail for that call; flows otherwise proceed |
| No OpenAI-compatible key for Graphiti | Extraction/store quality fails in Graphiti service logs; configure `OPEN_AI_KEY` or disable |
| Missing `pentagi-network` | Compose overlay fails; start core `docker-compose.yml` first |

### Operational recovery

```bash
# Confirm health
curl -sS http://127.0.0.1:8000/healthcheck

# Restart graph stack
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml up -d neo4j graphiti

# Temporarily disable without removing volumes
# GRAPHITI_ENABLED=false  (or empty GRAPHITI_URL)
```

Restart PentAGI after Graphiti becomes healthy if the first init failed the health check; the client does not auto-reenable mid-process after a failed `NewClient`.

## Limitations (beta)

- Single OpenAI-compatible extraction endpoint per deployment
- One model (`GRAPHITI_MODEL_NAME`) for all extractions; not per agent/flow
- Separate billing from the flow’s LLM provider
- No in-app graph explorer: use Neo4j Browser and Graphiti `/docs`
- Custom pentest entity/edge types come from the `vxcontrol/graphiti` image/fork

## Related pages

<CardGroup cols={2}>
  <Card title="Memory and knowledge" href="/memory-and-knowledge">
    pgvector tools, embeddings, summarizer budgets, and where Graphiti fits beside long-term memory.
  </Card>
  <Card title="Installation" href="/installation">
    Core compose, `.env`, volumes, and optional Graphiti/Langfuse/observability overlays.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full Config and `.env.example` reference including GRAPHITI_* and related keys.
  </Card>
  <Card title="Tools reference" href="/tools-reference">
    Named registry entries including graphiti_search argument shapes.
  </Card>
  <Card title="Interactive installer" href="/installer">
    TUI paths for embedded, external, or disabled Graphiti deployment.
  </Card>
  <Card title="Tools and sandbox" href="/tools-and-sandbox">
    Tool categories and how specialist executors attach optional tools.
  </Card>
</CardGroup>

---

## 15. Docker sandbox and worker nodes

> Docker socket and network options, DOCKER_INSIDE and NET_ADMIN, default and pentest images, custom OpenVAS image, and multi-host worker_node deployment constraints.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/15-docker-sandbox-and-worker-nodes.md
- Generated: 2026-07-10T07:07:54.425Z

### Source Files

- `backend/pkg/docker/client.go`
- `backend/docs/docker.md`
- `backend/pkg/config/config.go`
- `examples/guides/worker_node.md`
- `examples/guides/openvas-custom-image.md`
- `docker-compose.yml`
- `.env.example`

---
title: "Docker sandbox and worker nodes"
description: "Docker socket and network options, DOCKER_INSIDE and NET_ADMIN, default and pentest images, custom OpenVAS image, and multi-host worker_node deployment constraints."
---

PentAGI runs agent terminal and file tools inside Docker containers managed by `backend/pkg/docker`. The backend connects with the official Docker SDK (`client.FromEnv`), creates one **primary** container per flow (`pentagi-terminal-{flowID}`), mounts workspace data at `/work`, and optionally mounts a Docker socket and grants `NET_ADMIN` for nested Docker and network tooling. Single-node Compose mounts the host socket into the PentAGI service; multi-host setups point `DOCKER_HOST` at a remote TLS Docker API on a worker node.

## Runtime architecture

```mermaid
flowchart TB
  subgraph main ["Main node"]
    UI["Web UI / API"]
    PA["pentagi service"]
    CFG["Config DOCKER_*"]
  end

  subgraph dockerAPI ["Docker API"]
    SOCK["unix socket or tcp TLS"]
  end

  subgraph workers ["Primary containers"]
    T1["pentagi-terminal-N"]
    WORK["/work volume or bind"]
    SOCKM["optional docker.sock"]
  end

  subgraph optional ["Optional worker node"]
    HD["Host Docker :2376 TLS"]
    DIND["dind :3376 TLS"]
  end

  UI --> PA
  CFG --> PA
  PA -->|"RunContainer / Exec / Copy"| SOCK
  SOCK --> T1
  T1 --> WORK
  T1 -.-> SOCKM
  SOCK -.-> HD
  HD --> T1
  T1 -.->|"DOCKER_INSIDE socket path"| DIND
  PA -.->|"Direct mode"| DIND
```

| Layer | Responsibility |
|---|---|
| `backend/pkg/docker.DockerClient` | Create/stop/remove containers, exec, copy, path list/stat, startup `Cleanup` |
| `backend/pkg/tools` (`Prepare`) | Launch primary container with `NET_RAW` / optional `NET_ADMIN`, sync uploads/resources into `/work` |
| `backend/pkg/providers` | LLM image selection via `PromptTypeImageChooser` using `DOCKER_DEFAULT_IMAGE` and `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` |
| Compose / installer | Mount host socket or TLS certs; pass `DOCKER_*` into the PentAGI container |

## Configuration reference

Environment variables map to `config.Config` and are passed through `docker-compose.yml` into the `pentagi` service. The Docker SDK also reads standard client env vars (`DOCKER_HOST`, `DOCKER_TLS_VERIFY`, `DOCKER_CERT_PATH`).

<ParamField body="DOCKER_HOST" type="string">
Docker daemon endpoint. Compose default: `unix:///var/run/docker.sock`. Worker nodes use `tcp://{PRIVATE_IP}:2376` (host Docker) or `tcp://{PRIVATE_IP}:3376` (dind).
</ParamField>

<ParamField body="DOCKER_TLS_VERIFY" type="string">
Set to `1` when the remote daemon requires mutual TLS.
</ParamField>

<ParamField body="DOCKER_CERT_PATH" type="string">
Directory containing client `ca.pem`, `cert.pem`, and `key.pem` for TLS Docker API access.
</ParamField>

<ParamField body="DOCKER_INSIDE" type="bool">
Default `false` in code/Compose; `.env.example` often sets `true`. When true, primary containers bind-mount `DOCKER_SOCKET` → `/var/run/docker.sock` so agents can run Docker against the mapped daemon.
</ParamField>

<ParamField body="DOCKER_NET_ADMIN" type="bool">
Default `false`. When true, primary containers get `CapAdd: NET_RAW, NET_ADMIN`. When false, only `NET_RAW`.
</ParamField>

<ParamField body="DOCKER_SOCKET" type="string">
Host-side socket path mounted into primary containers when `DOCKER_INSIDE=true`. Default resolution uses `/var/run/docker.sock` or inspects the running PentAGI container mount. Worker standard mode commonly uses `/var/run/docker-dind/docker.sock`.
</ParamField>

<ParamField body="DOCKER_NETWORK" type="string">
Empty: default bridge networking with port bindings. Named network (e.g. `pentagi-network`): ensure/create bridge network and attach containers. Special value `host`: container uses host network stack; no port bindings.
</ParamField>

<ParamField body="DOCKER_PUBLIC_IP" type="string">
Default `0.0.0.0`. Host IP used in port bindings for the flow port pair (bridge mode only). On worker nodes, set to the worker private IP facing targets for OOB callbacks.
</ParamField>

<ParamField body="DOCKER_WORK_DIR" type="string">
Optional host path root for per-flow work dirs (`{DOCKER_WORK_DIR}/flow-{id}`). Empty: bind-mount resolved host data path if PentAGI runs with a bind-mounted data dir; otherwise create a dedicated Docker volume per container.
</ParamField>

<ParamField body="DOCKER_DEFAULT_IMAGE" type="string">
Fallback image when pull/create fails. Code default: `debian:latest`.
</ParamField>

<ParamField body="DOCKER_DEFAULT_IMAGE_FOR_PENTEST" type="string">
Default pentest image for LLM image selection. Code default: `vxcontrol/kali-linux`.
</ParamField>

Compose also mounts:

| Host path (env override) | Container path | Purpose |
|---|---|---|
| `${PENTAGI_DOCKER_SOCKET:-/var/run/docker.sock}` | `/var/run/docker.sock` | Local daemon access for the PentAGI process |
| `${PENTAGI_DOCKER_CERT_PATH:-./docker-ssl}` | `/opt/pentagi/docker/ssl` | TLS client certs for remote Docker |
| `${PENTAGI_DATA_DIR:-pentagi-data}` | `/opt/pentagi/data` | Local flow file cache (`DATA_DIR`) |

The PentAGI service runs as `user: root:root` while using the Docker socket.

## Socket modes: `DOCKER_INSIDE`

| Mode | Behavior |
|---|---|
| `DOCKER_INSIDE=false` | Primary containers do **not** receive a Docker socket. Agents cannot run Docker CLI/API inside the sandbox against a sibling daemon. |
| `DOCKER_INSIDE=true` | `RunContainer` appends bind `{DOCKER_SOCKET}:/var/run/docker.sock`. Agents can create sibling/nested containers against that daemon. |

Socket path resolution order:

1. Explicit `DOCKER_SOCKET` if set.
2. Otherwise `getHostDockerSocket`: use the SDK daemon unix path, or map back through the current container’s mounts when PentAGI itself runs in Docker.

<Warning>
Mounting a Docker socket grants near-root control of that Docker engine. Prefer a dedicated worker node and TLS; avoid exposing host Docker on public interfaces.
</Warning>

## Capabilities: `DOCKER_NET_ADMIN`

Primary container capabilities are set in `flowToolsExecutor.Prepare`:

```go
capAdd := []string{"NET_RAW"}
if fte.cfg.DockerNetAdmin {
    capAdd = append(capAdd, "NET_ADMIN")
}
```

| Setting | Caps | Typical use |
|---|---|---|
| `DOCKER_NET_ADMIN=false` | `NET_RAW` | Safer default; basic raw sockets |
| `DOCKER_NET_ADMIN=true` | `NET_RAW`, `NET_ADMIN` | Routing, iptables, advanced nmap/network tooling |

`NET_ADMIN` can alter network configuration visible to the engine’s network namespace. Disable it when flows do not need low-level networking.

## Network modes and ports

### Bridge (default)

When `DOCKER_NETWORK` is empty or a custom bridge name:

- Each flow gets **two** TCP ports from the deterministic range **28000–29999**:
  - `port = 28000 + (flowID * 2 + i) % 2000` for `i ∈ {0,1}`
- Bindings use `DOCKER_PUBLIC_IP` as host IP.
- If `DOCKER_NETWORK` is a non-empty name other than `host`, the client ensures a bridge network with that name exists and attaches the container.

These ports support out-of-band (OOB) techniques during pentests. Firewall targets and operator networks must allow inbound access to the published pair on the worker/public IP.

### Host network

When `DOCKER_NETWORK=host`:

- `NetworkMode=host`; no port bindings.
- Container shares host interfaces (lower isolation, higher fidelity for some network tests).
- Agent prompts warn not to bind arbitrary ports that conflict with host services.

## Container lifecycle

1. **Image selection** — On new flow creation, providers render `PromptTypeImageChooser` with `DefaultImage`, `DefaultImageForPentest`, and flow `Input`, then call the LLM to choose an image string.
2. **Prepare** — Tools executor reuses a running primary container or removes a non-running one and creates a new primary with entrypoint `tail -f /dev/null`.
3. **Pull / fallback** — `pullImage` on the requested image; on failure, switch to `DOCKER_DEFAULT_IMAGE` / `debian:latest` and retry create.
4. **Workspace** — Mount host path or volume at `/work`. Sync local `uploads/` and resources into the container (cache under `DATA_DIR` remains source of truth for uploads).
5. **DB status** — Tracked as starting → running / failed / stopped / deleted.
6. **Cleanup** — On startup, `Cleanup` stops/removes containers for terminated flows; restart policy is `on-failure` with max 5 retries (avoids auto-start side effects around DinD socket dirs).

Primary name: `pentagi-terminal-{flowID}`. Working directory inside containers: `/work`.

## Default and pentest images

| Variable / constant | Default | Role |
|---|---|---|
| `DOCKER_DEFAULT_IMAGE` | `debian:latest` | SDK fallback when pull/create fails |
| `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` | `vxcontrol/kali-linux` | Hint for automatic pentest image choice |
| Hardcoded `pentestDockerImage` in providers | `vxcontrol/kali-linux` | Prefix check for “is default pentest image” in agent context |

Explicit user/task image requests can override automatic selection. Changing the pentest default requires restart and a **new** flow/task; existing containers keep their original image.

Pull both images on worker nodes before first use so cold starts do not fail offline.

## Custom OpenVAS image

PentAGI does **not** install, feed-sync, or orchestrate OpenVAS/GVM. The supported approach is a self-built image:

<Steps>
  <Step title="Base on systemd Kali">
    Start from `vxcontrol/kali-linux:systemd` (OpenVAS/GVM expects systemd). Upstream entrypoint lives in the [vxcontrol/kali-linux-image](https://github.com/vxcontrol/kali-linux-image) repo as `container-entrypoint.sh`.
  </Step>
  <Step title="Install OpenVAS/GVM">
    Either install packages in a Dockerfile or follow Greenbone’s source-build examples, adapted into your image. Maintain a custom `/usr/local/bin/container-entrypoint` that starts systemd **and** GVM/OpenVAS services.
  </Step>
  <Step title="Point PentAGI at the image">
    ```bash
    docker build -t myorg/kali-linux:openvas .
    # .env
    DOCKER_DEFAULT_IMAGE_FOR_PENTEST=myorg/kali-linux:openvas
    ```
    Restart PentAGI. On worker-node deployments, ensure the same image is available on the worker Docker engine.
  </Step>
  <Step title="Prompt agents">
    Document availability under Settings → Prompts (and flow-specific prompts). Agents will not discover OpenVAS without explicit guidance.
  </Step>
</Steps>

```dockerfile
FROM vxcontrol/kali-linux:systemd

RUN apt update && apt install -y openvas gvm \
    && rm -rf /var/lib/apt/lists/*

COPY container-entrypoint.sh /usr/local/bin/container-entrypoint
RUN chmod +x /usr/local/bin/container-entrypoint
```

Treat package names as illustrative; fall back to source builds if distro packages are missing.

## Multi-host worker node deployment

Use a separate worker when you want sandbox compute and Docker engines off the main API/UI host. Reference procedure: `examples/guides/worker_node.md`.

### Topology

| Mode | Main node connects to | Worker containers get socket? | Notes |
|---|---|---|---|
| **Standard** | Host Docker TLS `:2376` | Yes → dind socket (e.g. `/var/run/docker-dind/docker.sock`) | Host Docker creates `pentagi-terminal-*`; nested Docker uses dind |
| **Direct** | dind TLS `:3376` | No (`DOCKER_SOCKET` empty, `DOCKER_INSIDE` off for socket map) | Workers created directly inside dind |

### Ports on the worker private IP

| Port | Service |
|---|---|
| 2376 | Host Docker API (TLS) |
| 3376 | dind API (TLS) |
| 9323 / 9324 | Docker metrics (optional observability scrape) |
| 28000–30000 | Per-flow OOB port pairs on all interfaces |

Firewall: allow 2376/3376 (and metrics if used) from the main node; allow 28000–30000 from engagement targets for OOB.

### Setup outline

<Steps>
  <Step title="Install Docker on main and worker">
    Docker CE + compose plugin on both nodes.
  </Step>
  <Step title="TLS for host Docker on worker">
    easy-rsa CA, server cert with SAN for worker `PRIVATE_IP`, `daemon.json` with `tls`/`tlsverify`, listen on `tcp://{PRIVATE_IP}:2376`.
  </Step>
  <Step title="Run privileged dind">
    Separate cert tree; publish `{PRIVATE_IP}:3376→2376`; persist `/var/lib/docker-dind` and socket dir `/var/run/docker-dind`.
  </Step>
  <Step title="Copy client certs to main">
    Place host client certs at e.g. `/opt/pentagi/docker-host-ssl` and dind client certs at `/opt/pentagi/docker-dind-ssl` (`ca.pem`, `cert.pem`, `key.pem` each).
  </Step>
  <Step title="Configure PentAGI Docker environment">
    Via installer **Tools → Docker Environment** or `.env`:
  </Step>
</Steps>

**Standard mode example values:**

```bash
DOCKER_INSIDE=true
DOCKER_NET_ADMIN=true
DOCKER_SOCKET=/var/run/docker-dind/docker.sock
DOCKER_NETWORK=pentagi-network
DOCKER_PUBLIC_IP=${PRIVATE_IP}
DOCKER_WORK_DIR=          # empty → Docker volumes on worker
DOCKER_DEFAULT_IMAGE=debian:latest
DOCKER_DEFAULT_IMAGE_FOR_PENTEST=vxcontrol/kali-linux
DOCKER_HOST=tcp://${PRIVATE_IP}:2376
DOCKER_TLS_VERIFY=1
DOCKER_CERT_PATH=/opt/pentagi/docker-host-ssl
```

**Direct mode:** same as above but `DOCKER_HOST=tcp://${PRIVATE_IP}:3376`, `DOCKER_CERT_PATH=/opt/pentagi/docker-dind-ssl`, clear `DOCKER_SOCKET` (no socket mapping).

### Deployment constraints

- **Filesystem split**: Main node `DATA_DIR` (uploads cache under `flow-{id}-data/`) is not the worker container filesystem. Uploads push best-effort into `/work/uploads` when the primary is running; on start, cache is source of truth. Container-pulled files land under local `container/` paths.
- **Empty `DOCKER_WORK_DIR` on remote Docker**: When the daemon is remote or data is not bind-mounted on that host, `getHostDataDir` returns empty and each primary gets a dedicated named volume (`{containerName}-data`).
- **Image availability**: Worker daemon must pull/have default, pentest, and any custom images.
- **TLS cert SANs**: Certificates must include the worker IP used in `DOCKER_HOST`.
- **Privileged dind**: Required for nested Docker; isolate worker network accordingly.
- **Same custom images**: OpenVAS or other custom pentest images must exist on the worker engine, not only on the main node.

## Single-node Compose defaults

Minimal local stack (socket on same host):

```bash
# .env — common local pattern
DOCKER_HOST=unix:///var/run/docker.sock
DOCKER_INSIDE=true
DOCKER_NET_ADMIN=true
DOCKER_SOCKET=/var/run/docker.sock
DOCKER_PUBLIC_IP=0.0.0.0
# leave DOCKER_DEFAULT_* empty to use code defaults
```

```bash
docker compose up -d
# verify daemon from host
docker ps
# primary containers appear as pentagi-terminal-* when flows run
```

## Failure modes and troubleshooting

| Symptom | Likely cause | Action |
|---|---|---|
| `failed to initialize docker client` / info error | Bad `DOCKER_HOST` or missing socket | Check socket mount, TLS env, cert paths |
| Image pull fails, falls back to debian | Registry/network or missing private image | Pre-pull on worker; set reachable defaults |
| Container create fails even on default image | Engine resource/permission/network issue | Inspect daemon logs; verify network name creation |
| Agents cannot run `docker` inside sandbox | `DOCKER_INSIDE=false` or wrong `DOCKER_SOCKET` | Enable inside mode; point socket at dind or host as intended |
| Advanced nmap/network tools fail | `DOCKER_NET_ADMIN=false` | Enable only if isolation policy allows |
| OOB callbacks never arrive | Firewall or wrong `DOCKER_PUBLIC_IP` | Open 28000–30000; bind public IP to worker-facing address |
| Still using old pentest image | Env not applied or old flow | Restart PentAGI; start a **new** flow |
| OpenVAS not used | Custom image/services/prompts incomplete | Fix entrypoint readiness; update prompts |
| Remote TLS handshake errors | Wrong certs/SAN/IP | Regenerate SANs for `PRIVATE_IP`; remount cert dir |

## Operations notes

- Prefer dedicated bridge networks over host mode unless host networking is required.
- Keep `DOCKER_INSIDE` and `DOCKER_NET_ADMIN` off in locked-down environments that only need app-layer testing.
- Monitor worker disk for named volumes and dind storage (`/var/lib/docker-dind`).
- Optional metrics on 9323/9324 can feed observability collectors when that stack is enabled.
- Container tests for contributors use `cmd/ctester` against the same Docker client surface.

## Related pages

<CardGroup cols={2}>
  <Card title="Tools and sandbox execution" href="/tools-and-sandbox">
    Tool categories, terminal/file isolation, timeouts, and how agents use the primary container.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full Config and `.env.example` catalog including Docker and pool settings.
  </Card>
  <Card title="Installation" href="/installation">
    Compose stack, SSL volumes, and overlay services that sit beside sandbox workers.
  </Card>
  <Card title="Interactive installer" href="/installer">
    TUI Docker Environment form and guided deploy including TLS cert paths.
  </Card>
  <Card title="Memory and knowledge" href="/memory-and-knowledge">
    Flow files under `/work`, upload cache, and resource sync behavior across hosts.
  </Card>
  <Card title="Development and testing" href="/development-and-testing">
    `ctester` and local compose workflows for container execution.
  </Card>
</CardGroup>

---

## 16. Environment variables

> Authoritative Config struct and .env.example: core, Docker, server, providers, embeddings, summarizer, search, OAuth, proxy, supervision, Graphiti, Langfuse, and DB pool keys with defaults.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/16-environment-variables.md
- Generated: 2026-07-10T07:08:09.131Z

### Source Files

- `backend/pkg/config/config.go`
- `.env.example`
- `backend/docs/config.md`
- `docker-compose.yml`
- `backend/pkg/config/config_test.go`

---
title: "Environment variables"
description: "Authoritative Config struct and .env.example: core, Docker, server, providers, embeddings, summarizer, search, OAuth, proxy, supervision, Graphiti, Langfuse, and DB pool keys with defaults."
---

PentAGI loads runtime configuration from process environment variables (optional `.env` via `godotenv`) into `config.Config` in `backend/pkg/config/config.go`. `NewConfig()` parses fields with `github.com/caarlos0/env/v10`, applies `envDefault` values when unset, then ensures `INSTALLATION_ID` and validates `LICENSE_KEY`. Compose deployments also use host-side keys in `.env.example` (for example `PENTAGI_*`, Postgres credentials, Langfuse stack) that docker-compose interpolates into container env and volumes—those keys are not all fields on `Config`.

<Info>
Defaults below come from `env` struct tags on `Config`. Empty means no default (must be set for the feature). Docker Compose may override some app defaults (for example `SERVER_PORT=8443`, `SERVER_USE_SSL=true`, `DOCKER_INSIDE=true` in `.env.example`).
</Info>

## Load path and ownership

```text
.env / process env
        │
        ▼
 NewConfig()  ── godotenv.Load (ignore missing)
        │
        ▼
 env.Parse → Config
        │
        ├── ensureInstallationID  (UUID → $DATA_DIR/installation_id)
        └── ensureLicenseKey      (SDK introspect; invalid → clear)
        │
        ▼
 main / providers / tools / docker / server
```

| Surface | Role |
| --- | --- |
| `config.Config` | Authoritative application settings (`env:"..."` tags) |
| `.env.example` | Template for compose + app env |
| `docker-compose.yml` | Maps host `.env` into `pentagi` service env/volumes |
| UI Settings | Provider profiles, prompts, API tokens—not LLM credentials or integration secrets |

`PgxPool` on `Config` is populated at runtime after pool creation; it is tagged `env:"-"` and is not an environment variable.

## Minimal deploy set

<Steps>
  <Step title="Copy template">
    `cp .env.example .env` from the repository root.
  </Step>
  <Step title="Set at least one LLM credential">
    Example: `OPEN_AI_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, Bedrock auth, or `LLM_SERVER_*` / `OLLAMA_SERVER_*`. Without a reachable provider, flows cannot run.
  </Step>
  <Step title="Harden secrets">
    Change `COOKIE_SIGNING_SALT`, `PENTAGI_POSTGRES_PASSWORD`, and any stack passwords before production.
  </Step>
  <Step title="Align public URL and CORS">
    For compose: `PUBLIC_URL=https://localhost:8443`, `CORS_ORIGINS=https://localhost:8443` (or your real origin).
  </Step>
  <Step title="Start stack">
    `docker compose up -d` then open `https://localhost:8443`. Compose builds `DATABASE_URL` from `PENTAGI_POSTGRES_*`.
  </Step>
</Steps>

## Core and cloud

| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `DATABASE_URL` | string | `postgres://pentagiuser:pentagipass@pgvector:5432/pentagidb?sslmode=disable` | Postgres + pgvector DSN. Compose overrides with `PENTAGI_POSTGRES_*` |
| `DEBUG` | bool | `false` | Extra logging / debug paths |
| `DATA_DIR` | string | `./data` | Persistent data (installation id, volumes, screenshots) |
| `ASK_USER` | bool | `false` | Require user confirmation for sensitive tool operations |
| `INSTALLATION_ID` | string | *(auto UUID)* | Cloud install id; if missing/invalid, written under `$DATA_DIR/installation_id` |
| `LICENSE_KEY` | string | *(none)* | Cloud license; invalid keys are cleared after introspect |

## Database connection pools

| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `DATABASE_MAX_OPEN_CONNS` | int | `25` | Max open `sql.DB` connections (sqlc + GORM share one pool) |
| `DATABASE_MAX_IDLE_CONNS` | int | `5` | Max idle `sql.DB` connections |
| `DATABASE_VECTOR_MAX_CONNS` | int | `10` | Max connections in shared `pgxpool` for pgvector stores |

Compose host credentials (not on `Config`): `PENTAGI_POSTGRES_USER` (default `postgres`), `PENTAGI_POSTGRES_PASSWORD` (default `postgres`), `PENTAGI_POSTGRES_DB` (default `pentagidb`).

## Docker and terminal sandbox

| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `DOCKER_INSIDE` | bool | `false` | PentAGI runs in Docker and must use host Docker via socket |
| `DOCKER_NET_ADMIN` | bool | `false` | Grant `NET_ADMIN` to primary terminal container |
| `DOCKER_SOCKET` | string | *(none)* | Docker socket path on host |
| `DOCKER_NETWORK` | string | *(none)* | Bridge network name, or `host` for host networking |
| `DOCKER_PUBLIC_IP` | string | `0.0.0.0` | Host IP for port bindings (bridge mode) |
| `DOCKER_WORK_DIR` | string | *(none)* | Custom work dir mapping for containers |
| `DOCKER_DEFAULT_IMAGE` | string | `debian:latest` | Fallback general image |
| `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` | string | `vxcontrol/kali-linux` | Default pentest image |
| `TERMINAL_TOOL_TIMEOUT` | int | `1200` | Default terminal timeout (seconds) when agent passes `timeout<=0`. Clamped to `1`–`10800` (over-range → 10800) |

Docker SDK client env (compose / daemon, not `Config` fields): `DOCKER_HOST`, `DOCKER_TLS_VERIFY`, `DOCKER_CERT_PATH`.

Compose volume helpers: `PENTAGI_DATA_DIR`, `PENTAGI_SSL_DIR`, `PENTAGI_OLLAMA_DIR`, `PENTAGI_DOCKER_SOCKET`, `PENTAGI_DOCKER_CERT_PATH`, `PENTAGI_LLM_SERVER_CONFIG_PATH`, `PENTAGI_OLLAMA_SERVER_CONFIG_PATH`, `PENTAGI_LISTEN_IP`, `PENTAGI_LISTEN_PORT`, `PENTAGI_IMAGE`.

## HTTP server and frontend

| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `SERVER_PORT` | int | `8080` | Listen port (compose example often `8443`) |
| `SERVER_HOST` | string | `0.0.0.0` | Bind address |
| `SERVER_USE_SSL` | bool | `false` | TLS listen when cert/key set (compose often `true`) |
| `SERVER_SSL_KEY` | string | *(none)* | TLS key path |
| `SERVER_SSL_CRT` | string | *(none)* | TLS certificate path |
| `STATIC_DIR` | string | `./fe` | Local SPA assets when not reverse-proxying |
| `STATIC_URL` | URL | *(none)* | External static origin; enables reverse-proxy static mode |
| `CORS_ORIGINS` | string list | `*` | Allowed origins (comma-separated). Non-`*` enables credentials |

## Authentication and OAuth

| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `COOKIE_SIGNING_SALT` | string | *(none)* | Session cookie store key material |
| `PUBLIC_URL` | string | `""` | Public origin for OAuth callbacks |
| `OAUTH_GOOGLE_CLIENT_ID` | string | *(none)* | Google OAuth client id |
| `OAUTH_GOOGLE_CLIENT_SECRET` | string | *(none)* | Google OAuth client secret |
| `OAUTH_GITHUB_CLIENT_ID` | string | *(none)* | GitHub OAuth client id |
| `OAUTH_GITHUB_CLIENT_SECRET` | string | *(none)* | GitHub OAuth client secret |

OAuth registers only when `PUBLIC_URL` parses and the provider pair is non-empty. Provider console redirect URI must be:

```text
${PUBLIC_URL}/api/v1/auth/login-callback
```

## Scraper

| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `SCRAPER_PUBLIC_URL` | string | *(none)* | Client-facing scraper base URL |
| `SCRAPER_PRIVATE_URL` | string | *(none)* | Backend→scraper URL (compose often basic-auth to `scraper`) |

Compose-only scraper service keys: `LOCAL_SCRAPER_USERNAME`, `LOCAL_SCRAPER_PASSWORD`, `LOCAL_SCRAPER_MAX_CONCURRENT_SESSIONS`, `SCRAPER_LISTEN_IP`, `SCRAPER_LISTEN_PORT`.

## LLM providers (BYOK)

At least one configured provider is required for agent work. Keys stay server-managed; the UI manages per-agent model profiles on top of these endpoints.

### OpenAI and Anthropic

| Variable | Default |
| --- | --- |
| `OPEN_AI_KEY` | *(none)* |
| `OPEN_AI_SERVER_URL` | `https://api.openai.com/v1` |
| `ANTHROPIC_API_KEY` | *(none)* |
| `ANTHROPIC_SERVER_URL` | `https://api.anthropic.com/v1` |

### Gemini

| Variable | Default |
| --- | --- |
| `GEMINI_API_KEY` | *(none)* |
| `GEMINI_SERVER_URL` | `https://generativelanguage.googleapis.com` |

### AWS Bedrock

| Variable | Default | Notes |
| --- | --- | --- |
| `BEDROCK_REGION` | `us-east-1` | AWS region |
| `BEDROCK_DEFAULT_AUTH` | `false` | SDK default credential chain (highest priority when true) |
| `BEDROCK_BEARER_TOKEN` | *(none)* | Bearer auth over static keys |
| `BEDROCK_ACCESS_KEY_ID` | *(none)* | Static access key |
| `BEDROCK_SECRET_ACCESS_KEY` | *(none)* | Static secret |
| `BEDROCK_SESSION_TOKEN` | *(none)* | Optional STS session token |
| `BEDROCK_SERVER_URL` | *(none)* | Custom / VPC endpoint |

Auth priority: `BEDROCK_DEFAULT_AUTH` → bearer token → access key + secret.

### DeepSeek, GLM, Kimi, Qwen

| Provider | Key | Server URL default | Optional LiteLLM prefix |
| --- | --- | --- | --- |
| DeepSeek | `DEEPSEEK_API_KEY` | `https://api.deepseek.com` | `DEEPSEEK_PROVIDER` |
| GLM | `GLM_API_KEY` | `https://api.z.ai/api/paas/v4` | `GLM_PROVIDER` |
| Kimi | `KIMI_API_KEY` | `https://api.moonshot.ai/v1` | `KIMI_PROVIDER` |
| Qwen | `QWEN_API_KEY` | `https://dashscope-us.aliyuncs.com/compatible-mode/v1` | `QWEN_PROVIDER` |

Regional alternate endpoints are documented in configure-llm-providers; only the defaults above live in `Config`.

### Custom OpenAI-compatible (`LLM_SERVER_*`)

| Variable | Default | Description |
| --- | --- | --- |
| `LLM_SERVER_URL` | *(none)* | Base URL |
| `LLM_SERVER_KEY` | *(none)* | API key |
| `LLM_SERVER_MODEL` | *(none)* | Default model id |
| `LLM_SERVER_PROVIDER` | *(none)* | Model name prefix (e.g. LiteLLM) |
| `LLM_SERVER_CONFIG_PATH` | *(none)* | Per-agent YAML config path |
| `LLM_SERVER_LEGACY_REASONING` | `false` | Legacy reasoning payload format |
| `LLM_SERVER_PRESERVE_REASONING` | `false` | Keep reasoning content across turns |

### Ollama

| Variable | Default | Description |
| --- | --- | --- |
| `OLLAMA_SERVER_URL` | *(none)* | Local or `https://ollama.com` |
| `OLLAMA_SERVER_API_KEY` | *(none)* | Required for Ollama Cloud |
| `OLLAMA_SERVER_MODEL` | *(none)* | Default model |
| `OLLAMA_SERVER_CONFIG_PATH` | *(none)* | Provider YAML path |
| `OLLAMA_SERVER_PULL_MODELS_TIMEOUT` | `600` | Pull timeout (seconds) |
| `OLLAMA_SERVER_PULL_MODELS_ENABLED` | `false` | Auto-pull models |
| `OLLAMA_SERVER_LOAD_MODELS_ENABLED` | `false` | List models from server API |

## Embeddings

| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `EMBEDDING_URL` | string | *(none)* | Override embedder base URL |
| `EMBEDDING_KEY` | string | *(none)* | Override embedder API key |
| `EMBEDDING_MODEL` | string | *(none)* | Embedding model id |
| `EMBEDDING_PROVIDER` | string | `openai` | `openai`, `ollama`, `mistral`, `jina`, `huggingface` |
| `EMBEDDING_STRIP_NEW_LINES` | bool | `true` | Strip newlines before embed |
| `EMBEDDING_BATCH_SIZE` | int | `512` | Batch size |
| `EMBEDDING_MAX_TEXT_BYTES` | int | `8192` | Max bytes per document sent to embedder (full text kept in DB) |

When `EMBEDDING_KEY` / `EMBEDDING_URL` are empty, OpenAI embedder falls back to `OPEN_AI_KEY` / `OPEN_AI_SERVER_URL`.

## Chain summarizer

### Flow summarizer

| Variable | Default |
| --- | --- |
| `SUMMARIZER_PRESERVE_LAST` | `true` |
| `SUMMARIZER_USE_QA` | `true` |
| `SUMMARIZER_SUM_MSG_HUMAN_IN_QA` | `false` |
| `SUMMARIZER_LAST_SEC_BYTES` | `51200` |
| `SUMMARIZER_MAX_BP_BYTES` | `16384` |
| `SUMMARIZER_MAX_QA_SECTIONS` | `10` |
| `SUMMARIZER_MAX_QA_BYTES` | `65536` |
| `SUMMARIZER_KEEP_QA_SECTIONS` | `1` |

### Assistant mode

| Variable | Default |
| --- | --- |
| `ASSISTANT_USE_AGENTS` | `false` |
| `ASSISTANT_SUMMARIZER_PRESERVE_LAST` | `true` |
| `ASSISTANT_SUMMARIZER_LAST_SEC_BYTES` | `76800` |
| `ASSISTANT_SUMMARIZER_MAX_BP_BYTES` | `16384` |
| `ASSISTANT_SUMMARIZER_MAX_QA_SECTIONS` | `7` |
| `ASSISTANT_SUMMARIZER_MAX_QA_BYTES` | `76800` |
| `ASSISTANT_SUMMARIZER_KEEP_QA_SECTIONS` | `3` |

## Search engines

| Variable | Default | Description |
| --- | --- | --- |
| `DUCKDUCKGO_ENABLED` | `true` | Enable DuckDuckGo tool |
| `DUCKDUCKGO_REGION` | *(none)* | e.g. `us-en` |
| `DUCKDUCKGO_SAFESEARCH` | *(none)* | `off` / `moderate` / `strict` |
| `DUCKDUCKGO_TIME_RANGE` | *(none)* | `d` / `w` / `m` / `y` |
| `SPLOITUS_ENABLED` | `false` | Sploitus exploit search (IP reputation matters) |
| `GOOGLE_API_KEY` | *(none)* | Google CSE API key |
| `GOOGLE_CX_KEY` | *(none)* | Custom Search Engine id |
| `GOOGLE_LR_KEY` | `lang_en` | Language restriction |
| `TRAVERSAAL_API_KEY` | *(none)* | Traversaal |
| `TAVILY_API_KEY` | *(none)* | Tavily |
| `PERPLEXITY_API_KEY` | *(none)* | Perplexity |
| `PERPLEXITY_MODEL` | `sonar` | Perplexity model |
| `PERPLEXITY_CONTEXT_SIZE` | `low` | `low` / `medium` / `high` |
| `SEARXNG_URL` | *(none)* | Searxng base URL |
| `SEARXNG_CATEGORIES` | `general` | Categories |
| `SEARXNG_LANGUAGE` | *(none)* | Language filter |
| `SEARXNG_SAFESEARCH` | `0` | `0` / `1` / `2` |
| `SEARXNG_TIME_RANGE` | *(none)* | Time filter |
| `SEARXNG_TIMEOUT` | *(none)* | Request timeout seconds |

Search tools also use `PROXY_URL` when set.

## Proxy, TLS outbound, and HTTP timeouts

| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `PROXY_URL` | string | *(none)* | HTTP proxy for providers and network tools |
| `EXTERNAL_SSL_CA_PATH` | string | `""` | Extra CA for outbound LLM TLS |
| `EXTERNAL_SSL_INSECURE` | bool | `false` | Skip TLS verify (dev only) |
| `HTTP_CLIENT_TIMEOUT` | int | `600` | Outbound HTTP timeout seconds; `0` = no timeout (not recommended) |

## Agent supervision and limits

| Variable | Default | Description |
| --- | --- | --- |
| `EXECUTION_MONITOR_ENABLED` | `false` | Mentor review on tool-loop patterns |
| `EXECUTION_MONITOR_SAME_TOOL_LIMIT` | `5` | Consecutive identical tool calls before mentor |
| `EXECUTION_MONITOR_TOTAL_TOOL_LIMIT` | `10` | Total tool calls before mentor |
| `MAX_GENERAL_AGENT_TOOL_CALLS` | `100` | Cap for Assistant, Primary, Pentester, Coder, Installer |
| `MAX_LIMITED_AGENT_TOOL_CALLS` | `20` | Cap for Searcher, Memorist, Reporter, Adviser, Reflector, etc. |
| `AGENT_PLANNING_STEP_ENABLED` | `false` | Planner step before specialist agents |

## Graphiti knowledge graph

On `Config`:

| Variable | Default | Description |
| --- | --- | --- |
| `GRAPHITI_ENABLED` | `false` | Enable graph integration |
| `GRAPHITI_TIMEOUT` | `30` | Client timeout seconds |
| `GRAPHITI_URL` | *(none)* | Graphiti API base (compose: `http://graphiti:8000`) |

Compose/stack only (Graphiti service, not `Config`): `GRAPHITI_MODEL_NAME` (compose default `gpt-5-mini`), `NEO4J_USER`, `NEO4J_PASSWORD`, `NEO4J_DATABASE`, `NEO4J_URI`. Use `docker-compose-graphiti.yml` overlay.

## Observability and Langfuse

On `Config`:

| Variable | Default | Description |
| --- | --- | --- |
| `OTEL_HOST` | *(none)* | OpenTelemetry collector host/endpoint |
| `LANGFUSE_BASE_URL` | *(none)* | Langfuse API base |
| `LANGFUSE_PROJECT_ID` | *(none)* | Project id |
| `LANGFUSE_PUBLIC_KEY` | *(none)* | Public key |
| `LANGFUSE_SECRET_KEY` | *(none)* | Secret key |

`.env.example` also defines a large Langfuse/observability compose surface (`LANGFUSE_*` Postgres/ClickHouse/S3/Redis/init, Grafana/OTEL listen ports). Those configure optional stacks, not fields on `Config`. Wire `LANGFUSE_*` / `OTEL_HOST` on the `pentagi` service when overlays are enabled.

## Compose-only keys (quick map)

| Prefix / key | Consumed by | Purpose |
| --- | --- | --- |
| `PENTAGI_LISTEN_*`, `PENTAGI_IMAGE`, volume dirs | `docker-compose.yml` | Host publish ports, image, mounts |
| `PENTAGI_POSTGRES_*` | compose → `DATABASE_URL` | DB credentials for pgvector service |
| `PENTAGI_LLM_SERVER_CONFIG_PATH`, `PENTAGI_OLLAMA_SERVER_CONFIG_PATH` | volume mounts | Host YAML → in-container config paths |
| `LOCAL_SCRAPER_*`, `SCRAPER_LISTEN_*` | scraper service | Scraper auth and ports |
| `PGVECTOR_LISTEN_*`, `POSTGRES_EXPORTER_LISTEN_*` | compose | Optional host port publish |
| `LANGFUSE_*` (stack), `NEO4J_*`, Grafana/OTEL listen | optional compose files | Side stacks |

## Secret handling

`Config.GetSecretPatterns()` builds redaction regexes for non-empty secret fields (API keys, OAuth secrets, DB URL, proxy URL with credentials, Langfuse keys, cookie salt, license key, and related). Values are trimmed; empty/whitespace entries are skipped. Use this when logging tool output or chain content that may echo credentials.

## Verification and common failures

| Symptom | Check |
| --- | --- |
| No providers in UI / flow fails at LLM | At least one key + reachable server URL; for custom/Ollama, config path mounts |
| DB connection refused | `DATABASE_URL` host (`pgvector` in compose vs localhost for bare metal); pool limits |
| OAuth redirect mismatch | Exact `PUBLIC_URL` + `/api/v1/auth/login-callback` |
| CORS browser errors | `CORS_ORIGINS` matches browser origin (not only `*`) |
| Terminal / docker errors | `DOCKER_SOCKET` / `DOCKER_HOST`, `DOCKER_INSIDE`, socket mount |
| Search tools missing | Keys/enable flags; `PROXY_URL` if network requires proxy |
| Graphiti tool no-ops | `GRAPHITI_ENABLED=true`, `GRAPHITI_URL`, Neo4j stack healthy |
| TLS to private LLM fails | `EXTERNAL_SSL_CA_PATH` or controlled `EXTERNAL_SSL_INSECURE` |
| Long LLM calls abort | Raise `HTTP_CLIENT_TIMEOUT` (default 600s) |

```bash
# From repo root after editing .env
docker compose config >/dev/null   # validate interpolation
docker compose up -d
# Confirm pentagi sees expected env (example)
docker compose exec pentagi env | rg 'OPEN_AI_|DATABASE_|SERVER_|GRAPHITI_|LANGFUSE_|OTEL_'
```

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    Compose core stack, `.env` from `.env.example`, SSL volumes, optional overlays.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    Wire cloud and regional providers; test before first flow.
  </Card>
  <Card title="Local and custom providers" href="/local-and-custom-providers">
    `LLM_SERVER_*`, Ollama, YAML config paths, aggregators.
  </Card>
  <Card title="Search engines" href="/search-engines">
    Enable DuckDuckGo, Google, Tavily, Perplexity, Searxng, Sploitus.
  </Card>
  <Card title="Observability and Langfuse" href="/observability-and-langfuse">
    OTEL and Langfuse stacks and what each measures.
  </Card>
  <Card title="Knowledge graph" href="/knowledge-graph">
    Graphiti + Neo4j enablement and failure modes.
  </Card>
  <Card title="Docker sandbox and workers" href="/docker-sandbox-workers">
    Socket, `DOCKER_INSIDE`, images, multi-host constraints.
  </Card>
  <Card title="Agents and supervision" href="/agents-and-supervision">
    Monitor thresholds, tool-call hard limits, planning step.
  </Card>
</CardGroup>

---

## 17. REST API

> Gin routes under /api/v1: auth, flows, tasks, subtasks, files, resources, containers, toolcalls, assistants, logs, knowledge, providers, settings, users, tokens; Swagger at /api/v1/swagger.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/17-rest-api.md
- Generated: 2026-07-10T07:10:18.111Z

### Source Files

- `backend/pkg/server/router.go`
- `backend/pkg/server/docs/swagger.yaml`
- `backend/pkg/server/services/flows.go`
- `backend/pkg/server/services/api_tokens.go`
- `backend/pkg/server/services/knowledge.go`
- `backend/pkg/server/auth/auth_middleware.go`
- `README.md`

---
title: "REST API"
description: "Gin routes under /api/v1: auth, flows, tasks, subtasks, files, resources, containers, toolcalls, assistants, logs, knowledge, providers, settings, users, tokens; Swagger at /api/v1/swagger."
---

PentAGI mounts its HTTP surface on Gin at **`/api/v1`** (`baseURL` in `backend/pkg/server/router.go`). The same engine serves REST handlers, GraphQL at `ANY /api/v1/graphql`, Swagger UI at `GET /api/v1/swagger/*any`, CORS, cookie sessions named `auth`, and optional static SPA or reverse-proxy frontend. OpenAPI metadata is generated into `backend/pkg/server/docs/swagger.yaml` (swag tags on service methods; `@BasePath /api/v1`, `@securityDefinitions.apikey BearerAuth`).

## Auth and access tiers

Middleware lives in `backend/pkg/server/auth/auth_middleware.go`. Three patterns apply on `/api/v1`:

| Group | Middleware | Routes |
| --- | --- | --- |
| Public | `TryAuth` (optional cookie or Bearer) | `GET /info`, `GET /swagger/*any`, `GET /graphql/playground`, `/auth/*` |
| Private | `AuthTokenRequired` (Bearer **or** session cookie) | Flows, tasks, knowledge, providers, settings, logs, files, resources, assistants, prompts, usage, GraphQL, roles, users, tokens |
| Password | `AuthUserRequired` + `localUserRequired` | `PUT /user/password` |

**Session login.** `POST /auth/login` accepts `models.Login` (`mail`, `password`), verifies bcrypt for local users (not OAuth-only / role_id `100`), loads privileges from `privileges` by `role_id`, and sets a signed cookie session (`HttpOnly`, `SameSite=Lax`, path `/api/v1`, max age **4 hours**). OAuth: `GET /auth/authorize?provider=google|github&return_uri=...`, then GET/POST `/auth/login-callback`. Logout: `GET /auth/logout`, `POST /auth/logout-callback`.

**Bearer API tokens.** Header: `Authorization: Bearer <jwt>`. Tokens are signed with `CookieSigningSalt`. If salt is empty or the literal `"salt"`, Bearer validation is skipped and token **creation** returns `Token.CreationDisabled`. Active tokens set privileges from the token cache; status must be `active`; user must not be blocked; user hash must match the installation.

**Privilege checks.** Handlers inspect `c.GetStringSlice("prm")` (for example `flows.view`, `flows.create`, `flows.edit`, `flows.delete`, `flows.admin`, `knowledge.*`, `providers.view`, `settings.view`, `settings.tokens.admin`). Missing privilege → `403` with code `NotPermitted`. Non-admin scopes typically filter by `user_id`.

**Guest info.** `GET /info` with `TryAuth` returns guest vs authenticated shape, privileges, OAuth provider names configured on the server, and develop-mode flags.

## Response envelope

All REST handlers use `backend/pkg/server/response`:

**Success**

```json
{ "status": "success", "data": { } }
```

**Error**

```json
{
  "status": "error",
  "code": "AuthRequired",
  "msg": "auth required",
  "error": "optional detail when develop mode is on"
}
```

Common codes: `AuthRequired` (403), `NotPermitted` (403), `PrivilegesRequired` (403), `Token.CreationDisabled` (400), `Token.NotFound` (404), resource-specific `*.NotFound` / `*.InvalidRequest`.

## Swagger

| Item | Value |
| --- | --- |
| UI | `GET /api/v1/swagger/index.html` (gin-swagger under `/swagger/*any`) |
| Spec | Generated `swagger.yaml` / `swagger.json` in `backend/pkg/server/docs/` |
| Security | `BearerAuth` header scheme on protected operations |
| Title | PentAGI Swagger API v1.0 |

Swagger and GraphQL playground sit on the public group (no hard auth required to open the UI; API calls still need credentials).

## List query protocol (`rdb.TableQuery`)

Paginated list endpoints bind query params from `backend/pkg/server/rdb/table.go`:

| Field | Query key | Rules |
| --- | --- | --- |
| Page | `page` | Required, min 1, default 1 |
| Size | `pageSize` | −1…1000 (−1 = unlimited), default 5 |
| Type | `type` | `init` \| `sort` \| `filter` \| `page` \| `size` |
| Sort | `sort[]` | `{prop, order}` with `ascending` / `descending` |
| Filters | `filters[]` | `{field, value, operator}`; operators `< <= >= > = != like not like in` |
| Group | `group` | Optional distinct-value grouping |

Typical list success body: `{ "items-or-entity-key": [...], "total": N }`. Flows group response: `{ "grouped": [...], "total": N }`.

## Auth routes

| Method | Path | Body / notes |
| --- | --- | --- |
| `POST` | `/auth/login` | `{ "mail", "password" }` |
| `GET` | `/auth/logout` | Clears session |
| `GET` | `/auth/authorize` | Query: `provider`, optional `return_uri` |
| `GET` | `/auth/login-callback` | Query: `code`, `state` (cookie state must match) |
| `POST` | `/auth/login-callback` | `models.AuthCallback` |
| `POST` | `/auth/logout-callback` | OAuth logout callback |
| `GET` | `/info` | Session/token introspection |
| `PUT` | `/user/password` | Local user password change; session required |

## Flows, tasks, subtasks

Flow lifecycle statuses: `created`, `running`, `waiting`, `finished`, `failed`.

| Method | Path | Privilege pattern | Notes |
| --- | --- | --- | --- |
| `GET` | `/flows/` | `flows.view` or `flows.admin` | TableQuery; admin sees all |
| `GET` | `/flows/:flowID` | view/admin | Single flow |
| `GET` | `/flows/:flowID/graph` | view/admin | Flow + tasks + subtasks |
| `POST` | `/flows/` | `flows.create` | Creates flow via `FlowController` |
| `PUT` | `/flows/:flowID` | `flows.edit` or admin | Actions: `stop`, `finish`, `input`, `rename` |
| `DELETE` | `/flows/:flowID` | `flows.delete` or admin | Finishes then deletes |
| `GET` | `/flows/:flowID/tasks/` | view scope | Task list |
| `GET` | `/flows/:flowID/tasks/:taskID` | | Task detail |
| `GET` | `/flows/:flowID/tasks/:taskID/graph` | | Task graph |
| `GET` | `/flows/:flowID/subtasks/` | | All subtasks for flow |
| `GET` | `/flows/:flowID/tasks/:taskID/subtasks/` | | Subtasks for task |
| `GET` | `/flows/:flowID/tasks/:taskID/subtasks/:subtaskID` | | One subtask |

:::endpoint POST /api/v1/flows/ Create flow
Create a pentest (or general) flow and start agent execution.

**Auth:** Bearer or session · **Privilege:** `flows.create`

**Body (`models.CreateFlow`)**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `input` | string | yes | First-task user objective |
| `provider` | string | yes | Provider name (BYOK/env or UI profile) |
| `functions` | object | no | Optional tool function allowlist |
| `resource_ids` | uint64[] | no | User resources to attach |

**Responses:** `201` → `models.Flow` · `403` not permitted · `500` provider/controller failure
:::

:::endpoint PUT /api/v1/flows/{flowID} Patch flow
Drive lifecycle actions on an existing flow.

**Body (`models.PatchFlow`)**

| Field | Type | Required when | Values / notes |
| --- | --- | --- | --- |
| `action` | string | always | `stop` \| `finish` \| `input` \| `rename` |
| `input` | string | `action=input` | Non-empty user message (`PutInput`) |
| `provider` | string | optional on input | Override provider for this input |
| `name` | string | `action=rename` | New title |
| `resource_ids` | uint64[] | optional on input | Attach resources |

Maps to controller: `Stop`, `Finish`, `PutInput`, `Rename`.
:::

## Assistants

Nested under a flow:

| Method | Path | Body |
| --- | --- | --- |
| `POST` | `/flows/:flowID/assistants/` | `CreateAssistant`: `input`, `provider`, `use_agents`, optional `functions`, `resource_ids` |
| `GET` | `/flows/:flowID/assistants/` | List |
| `GET` | `/flows/:flowID/assistants/:assistantID` | Detail |
| `PUT` | `/flows/:flowID/assistants/:assistantID` | `PatchAssistant`: `action` = `stop` \| `input`; optional `use_agents`, `resource_ids` |
| `DELETE` | `/flows/:flowID/assistants/:assistantID` | Delete |

## Files, resources, containers, toolcalls

**Flow files** (`/flows/:flowID/files/`) — host/container workspace under data dir + Docker:

| Method | Path | Role |
| --- | --- | --- |
| `GET` | `/` | List flow files |
| `GET` | `/container` | List container-side files |
| `POST` | `/` | Upload |
| `DELETE` | `/` | Delete |
| `GET` | `/download` | Download |
| `POST` | `/pull` | Pull from container |
| `POST` | `/resources` | Add existing resources into flow |
| `POST` | `/to-resources` | Promote flow file to user resource |

**User resources** (`/resources/`):

| Method | Path | Role |
| --- | --- | --- |
| `GET` | `/` | List |
| `POST` | `/` | Upload |
| `POST` | `/mkdir` | Create directory |
| `PUT` | `/move` | Move/rename |
| `POST` | `/copy` | Copy |
| `DELETE` | `/` | Delete |
| `GET` | `/download` | Download |

**Containers / toolcalls** — read-only inventory:

- `GET /containers/`, `GET /flows/:flowID/containers/`, `GET /flows/:flowID/containers/:containerID`
- `GET /toolcalls/`, `GET /flows/:flowID/toolcalls/`, `GET /flows/:flowID/toolcalls/:toolcallID`

## Logs and screenshots

Global list + per-flow list (TableQuery where applicable):

| Domain | Global | Per flow |
| --- | --- | --- |
| Agent logs | `GET /agentlogs/` | `GET /flows/:flowID/agentlogs/` |
| Assistant logs | `GET /assistantlogs/` | `GET /flows/:flowID/assistantlogs/` |
| Message logs | `GET /msglogs/` | `GET /flows/:flowID/msglogs/` |
| Terminal logs | `GET /termlogs/` | `GET /flows/:flowID/termlogs/` |
| Search logs | `GET /searchlogs/` | `GET /flows/:flowID/searchlogs/` |
| Vector-store logs | `GET /vecstorelogs/` | `GET /flows/:flowID/vecstorelogs/` |
| Screenshots | `GET /screenshots/` | `GET /flows/:flowID/screenshots/`, `.../:screenshotID`, `.../:screenshotID/file` |

## Knowledge (pgvector)

REST mirrors GraphQL knowledge ops via `KnowledgeService` + shared `knowledge.KnowledgeStore`. Collection name `langchain`; documents with `doc_type=memory` are excluded from list. Embedder unavailable → create/search return **503** (`ErrKnowledgeStoreUnavail`).

| Method | Path | Privilege intent |
| --- | --- | --- |
| `GET` | `/knowledge/` | view/admin; TableQuery + `with_content=true\|1` |
| `GET` | `/knowledge/:id` | UUID |
| `POST` | `/knowledge/` | create + embed |
| `POST` | `/knowledge/search` | semantic search |
| `PUT` | `/knowledge/:id` | update + re-embed |
| `DELETE` | `/knowledge/:id` | delete |

Filter fields on list: `id`, `doc_type`, `question`, `description`, `guide_type`, `answer_type`, `code_lang`, `manual`, `user_id`, `flow_id`, `task_id`, `subtask_id`, `data`.

**Create body (`CreateKnowledgeDocRequest`)**

| Field | Constraints |
| --- | --- |
| `doc_type` | `answer` \| `guide` \| `code` |
| `content` | 1…65536 chars |
| `question` | 1…2048 chars |
| `description` | optional, max 1000 |
| `guide_type` / `answer_type` / `code_lang` | optional typed metadata |

**Search body (`KnowledgeSearchRequest`):** `query` (required), `limit` 1…100, optional `doc_types`, `guide_types`, `answer_types`, `code_langs`, `flow_id`, `manual`.

## Providers, settings, prompts, usage

| Method | Path | Privilege | Returns |
| --- | --- | --- | --- |
| `GET` | `/providers/` | `providers.view` | Provider names, types, default model, model price/info for current user |
| `GET` | `/settings/` | `settings.view` | `debug`, `ask_user`, binary `version`, `docker_inside`, develop mode, `assistant_use_agents` |
| `GET` | `/prompts/` | | All prompt templates |
| `GET` | `/prompts/:promptType` | | One template |
| `PUT` | `/prompts/:promptType` | | Patch custom template |
| `POST` | `/prompts/:promptType/default` | | Reset to default |
| `DELETE` | `/prompts/:promptType` | | Delete custom override |
| `GET` | `/usage/` | | System usage |
| `GET` | `/usage/:period` | | Period usage |
| `GET` | `/flows/:flowID/usage` | | Flow token/usage stats |
| `POST` | `/anonymize/text` | | PII-style text anonymization (shared pattern replacer) |

Provider list is BYOK-oriented: only configured/available providers for the authenticated user appear; no hardcoded cloud vendor is required.

## Users, roles, tokens

| Method | Path | Notes |
| --- | --- | --- |
| `GET` | `/user/` | Current user |
| `GET` | `/users/` | List (admin-scoped where applicable) |
| `GET` | `/users/:hash` | By user hash |
| `POST` | `/users/` | Create |
| `PUT` | `/users/:hash` | Patch |
| `DELETE` | `/users/:hash` | Delete |
| `GET` | `/roles/` | Roles |
| `GET` | `/roles/:roleID` | Role detail |
| `POST` | `/tokens/` | Create API token (JWT secret returned once) |
| `GET` | `/tokens/` | List (own, or all with `settings.tokens.admin`) |
| `GET` | `/tokens/:tokenID` | Metadata |
| `PUT` | `/tokens/:tokenID` | Update `name` / `status` |
| `DELETE` | `/tokens/:tokenID` | Soft-delete / revoke |

:::endpoint POST /api/v1/tokens/ Create API token
Issues a JWT usable as `Authorization: Bearer` on REST and GraphQL.

**Body (`CreateAPITokenRequest`)**

| Field | Type | Constraints |
| --- | --- | --- |
| `name` | string | optional, max 100, unique per user |
| `ttl` | uint64 | required, **60…94608000** seconds (1 min…3 years) |

**Responses:** `201` → `APITokenWithSecret` (`token` JWT + metadata) · `400` default salt / validation · `403` unauthorized

Statuses: `active`, `revoked`, `expired` (expired may be computed when `created_at + ttl` is past).
:::

## GraphQL co-location

| Method | Path | Auth |
| --- | --- | --- |
| `ANY` | `/graphql` | `AuthTokenRequired` |
| `GET` | `/graphql/playground` | Public (`TryAuth`) |

Realtime subscriptions and some mutations exist only on GraphQL; REST covers CRUD-style and list/report surfaces above.

## Request examples

<RequestExample>
```bash title="Login (session cookie)"
curl -sk -c cookies.txt -X POST 'https://localhost:8443/api/v1/auth/login' \
  -H 'Content-Type: application/json' \
  -d '{"mail":"admin@pentagi.com","password":"YOUR_PASSWORD"}'
```
```bash title="Create flow with Bearer token"
curl -sk -X POST 'https://localhost:8443/api/v1/flows/' \
  -H 'Authorization: Bearer YOUR_JWT' \
  -H 'Content-Type: application/json' \
  -d '{"input":"Scan https://target.example for OWASP top 10","provider":"openai"}'
```
```bash title="List flows (TableQuery)"
curl -sk 'https://localhost:8443/api/v1/flows/?page=1&pageSize=20&type=init' \
  -H 'Authorization: Bearer YOUR_JWT'
```
```bash title="Stop a running flow"
curl -sk -X PUT 'https://localhost:8443/api/v1/flows/42' \
  -H 'Authorization: Bearer YOUR_JWT' \
  -H 'Content-Type: application/json' \
  -d '{"action":"stop"}'
```
</RequestExample>

## Failure modes

| Symptom | Likely cause |
| --- | --- |
| `403` `AuthRequired` | Missing/expired session or invalid Bearer |
| `403` `NotPermitted` | Role lacks resource privilege |
| `400` `Token.CreationDisabled` | `CookieSigningSalt` still default `"salt"` or empty |
| Bearer ignored / skip | Same default salt disables token validation path |
| Knowledge `503` | Embedder not configured or pgvector store init failed at boot |
| Flow create `500` | Unknown `provider` name for user, or `FlowController` error |
| OAuth authorize `403` | Provider not configured (`OAuthGoogle*` / `OAuthGithub*` env) |
| Password change `403` | Non-local user or missing session-only middleware |

## Architecture (request path)

```text
Client (UI / curl / automation)
        │
        ▼
  Gin /api/v1  ── CORS · sessions · no-cache · recovery · logger
        │
   ┌────┴─────────────────────┐
   │ TryAuth (public)         │ AuthTokenRequired (private)
   │  auth, info, swagger     │  REST resources + GraphQL
   └────────────┬─────────────┘
                ▼
     services/*  →  GORM / FlowController / ProviderController
                │         KnowledgeStore (pgvector)
                ▼
           response.Success | response.Error
```

## Next

<CardGroup>
  <Card title="Authentication and API tokens" href="/auth-and-api-tokens">
    Session login, OAuth, default admin, Bearer lifecycle and privilege scopes.
  </Card>
  <Card title="GraphQL API" href="/graphql-api">
    Query, Mutation, and Subscription surface including real-time log streams.
  </Card>
  <Card title="Flows, tasks, and subtasks" href="/flows-tasks-subtasks">
    StatusType lifecycle, putUserInput / stop / finish boundaries for REST actions.
  </Card>
  <Card title="Memory and knowledge" href="/memory-and-knowledge">
    pgvector documents, embeddings, flow files and resources under /work.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    Wire BYOK providers used by POST /flows and GET /providers.
  </Card>
</CardGroup>

---

## 18. GraphQL API

> schema.graphqls surface: Query, Mutation, and Subscription operations for flows, assistants, providers, prompts, API tokens, knowledge, usage stats, and real-time log streams over WebSocket.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/18-graphql-api.md
- Generated: 2026-07-10T07:09:01.093Z

### Source Files

- `backend/pkg/graph/schema.graphqls`
- `backend/pkg/graph/schema.resolvers.go`
- `backend/pkg/graph/subscriptions/controller.go`
- `backend/pkg/server/services/graphql.go`
- `frontend/graphql-schema.graphql`
- `frontend/src/lib`
- `README.md`

---
title: "GraphQL API"
description: "schema.graphqls surface: Query, Mutation, and Subscription operations for flows, assistants, providers, prompts, API tokens, knowledge, usage stats, and real-time log streams over WebSocket."
---

PentAGI exposes a single gqlgen GraphQL endpoint at **`/api/v1/graphql`**. The schema lives in `backend/pkg/graph/schema.graphqls` and covers flow execution, assistants, providers, prompts, API tokens, knowledge documents, usage analytics, and real-time subscriptions over WebSocket. Queries and mutations share HTTP transports; subscriptions upgrade to WebSocket on the same path.

## Endpoint and transports

| Surface | Path | Auth |
|---|---|---|
| GraphQL HTTP + WebSocket | `POST/GET/ANY /api/v1/graphql` | Required (`AuthTokenRequired`) |
| GraphQL Playground | `GET /api/v1/graphql/playground` | Optional try-auth (public group) |
| REST (parallel surface) | `/api/v1/*` | Same token/session stack |

Registered transports:

| Transport | Notes |
|---|---|
| `Options`, `GET`, `POST` | Standard query/mutation |
| `MultipartForm` | Max memory **32 MiB** |
| `Websocket` | Keepalive ping every **10s**; origin check against `CorsOrigins` |

Server extensions:

- **Introspection** enabled
- **Automatic persisted queries** (APQ cache size 100)
- **Query document cache** size 1000
- **Fixed complexity limit** `20000`

WebSocket init requires an authenticated user already present on the request context (`GetUserID`). Origin validation allows exact matches, single-`*` wildcards, same-host origins with `http`/`https`/`ws`/`wss` wrappers, and `*` allow-all.

## Authentication and context

Private GraphQL traffic goes through `AuthTokenRequired`, which accepts either:

1. **Bearer API token** — `Authorization: Bearer <token>`
2. **Browser session cookie** — `auth` session with claims `uid`, `prm` (privileges), `tid` (user type), etc.

`ServeGraphql` injects into the request context:

| Context key | Value |
|---|---|
| `userID` | `uint64` from middleware |
| `userType` | session type string (`local`, `oauth`, or token-derived type) |
| `userPermissions` | privilege string slice |

Resolvers call `validatePermission(ctx, "<resource>.<action>")`. Having the `.admin` sibling privilege (for example `flows.admin` when checking `flows.view`) grants admin scope. Flow-scoped operations use `validatePermissionWithFlowID`, which also requires the flow owner unless the caller is admin.

Some mutations additionally require a **user session** (`local` or `oauth`), not a pure API-token identity — notably API token CRUD, favorites, and flow templates.

<Warning>
`createAPIToken` rejects creation when `CookieSigningSalt` is empty or the default value `"salt"`. TTL must be between **60** and **94608000** seconds.
</Warning>

## Architecture

```mermaid
flowchart TB
  subgraph clients [Clients]
    UI["React UI<br/>Apollo + graphql-ws"]
    API["API clients<br/>Bearer token"]
  end

  subgraph http [Gin /api/v1]
    MW["AuthTokenRequired"]
    GQL["GraphqlService<br/>/graphql"]
    PLAY["Playground<br/>/graphql/playground"]
  end

  subgraph gql [gqlgen]
    SCH["schema.graphqls"]
    RES["schema.resolvers.go"]
    CTX["graph context<br/>uid / tid / prm"]
  end

  subgraph runtime [Runtime deps]
    FC["FlowController"]
    PC["ProviderController"]
    KS["KnowledgeStore"]
    SUB["SubscriptionsController"]
    DB[(PostgreSQL)]
  end

  UI -->|HTTP queries/mutations| MW
  UI -->|WS subscriptions| MW
  API --> MW
  MW --> GQL
  GQL --> CTX --> RES
  RES --> SCH
  RES --> FC
  RES --> PC
  RES --> KS
  RES --> SUB
  RES --> DB
  FC --> SUB
  PLAY -.-> GQL
```

Resolver dependencies are injected via `graph.Resolver`: DB, config, logger, token cache, default prompter, provider controller, flow controller, subscriptions controller, knowledge store, and optional text anonymizer.

## Domain enums (high-signal)

| Enum | Values | Used by |
|---|---|---|
| `StatusType` | `created`, `running`, `waiting`, `finished`, `failed` | Flow, Task, Subtask, Assistant |
| `ProviderType` | `openai`, `anthropic`, `gemini`, `bedrock`, `ollama`, `custom`, `deepseek`, `glm`, `kimi`, `qwen` | Providers |
| `AgentType` | primary, reporter, generator, refiner, reflector, enricher, adviser, coder, memorist, searcher, installer, pentester, summarizer, tool_call_fixer, assistant | Logs / usage |
| `MessageLogType` | answer, report, thoughts, browser, terminal, file, search, advice, ask, input, done | Message / assistant logs |
| `ToolCallStatus` | `received`, `running`, `finished`, `failed` | Tool call logs |
| `UsageStatsPeriod` | `week`, `month`, `quarter` | Analytics queries |
| `ResultType` | `success`, `error` | Many mutations |
| `TokenStatus` | `active`, `revoked`, `expired` | API tokens |
| `KnowledgeDocType` | `answer`, `guide`, `code` | Knowledge docs |

## Query operations

### Providers and settings

| Field | Args | Returns | Permission |
|---|---|---|---|
| `providers` | — | `[Provider!]!` | `providers.view` |
| `settings` | — | `Settings!` | `settings.view` |
| `settingsProviders` | — | `ProvidersConfig!` | `settings.providers.view` |
| `settingsPrompts` | — | `PromptsConfig!` | `settings.prompts.view` |
| `settingsUser` | — | `UserPreferences!` | `settings.user.view` + user session |

`Settings` fields: `debug`, `askUser`, `version`, `dockerInside`, `isDevelopMode`, `assistantUseAgents`.

`ProvidersConfig` includes readiness flags per provider type, default agent configs, user-defined configs, and model manifests.

### Flows, tasks, logs

| Field | Args | Returns | Permission |
|---|---|---|---|
| `flows` | — | `[Flow!]` | `flows.view` (admin sees all) |
| `flow` | `flowId` | `Flow!` | `flows.view` + ownership |
| `assistants` | `flowId` | `[Assistant!]` | `assistants.view` |
| `tasks` | `flowId` | `[Task!]` | `tasks.view` (+ `subtasks.view` for nested) |
| `flowFiles` | `flowId` | `[FlowFile!]!` | `flow_files.view` |
| `screenshots` | `flowId` | `[Screenshot!]` | `screenshots.view` |
| `terminalLogs` | `flowId` | `[TerminalLog!]` | `termlogs.view` |
| `messageLogs` | `flowId` | `[MessageLog!]` | `msglogs.view` |
| `agentLogs` | `flowId` | `[AgentLog!]` | `agentlogs.view` |
| `searchLogs` | `flowId` | `[SearchLog!]` | `searchlogs.view` |
| `vectorStoreLogs` | `flowId` | `[VectorStoreLog!]` | `vecstorelogs.view` |
| `toolCallLogs` | `flowId` | `[ToolCallLog!]` | `toolcalls.view` |
| `assistantLogs` | `flowId`, `assistantId` | `[AssistantLog!]` | `assistantlogs.view` |

`Flow` includes `id`, `title`, `status`, optional `terminals`, `provider`, timestamps. Terminals load only when the caller has `containers.view`.

### Analytics

All require `usage.view` (flow-scoped variants also check flow ownership).

**Token usage**

| Field | Args | Returns |
|---|---|---|
| `usageStatsTotal` | — | `UsageStats!` |
| `usageStatsByPeriod` | `period` | `[DailyUsageStats!]!` |
| `usageStatsByProvider` | — | `[ProviderUsageStats!]!` |
| `usageStatsByModel` | — | `[ModelUsageStats!]!` |
| `usageStatsByAgentType` | — | `[AgentTypeUsageStats!]!` |
| `usageStatsByFlow` | `flowId` | `UsageStats!` |
| `usageStatsByAgentTypeForFlow` | `flowId` | `[AgentTypeUsageStats!]!` |
| `usageStatsByModelAgentsForFlow` | `flowId` | `[ModelAgentsUsageStats!]!` |

`UsageStats` fields: `totalUsageIn/Out`, `totalUsageCacheIn/Out`, `totalUsageCostIn/Out`.

**Toolcalls / flows / execution**

| Field | Returns |
|---|---|
| `toolcallsStatsTotal` | `ToolcallsStats!` |
| `toolcallsStatsByPeriod(period)` | `[DailyToolcallsStats!]!` |
| `toolcallsStatsByFunction` | `[FunctionToolcallsStats!]!` |
| `toolcallsStatsByFlow(flowId)` | `ToolcallsStats!` |
| `toolcallsStatsByFunctionForFlow(flowId)` | `[FunctionToolcallsStats!]!` |
| `flowsStatsTotal` | `FlowsStats!` |
| `flowsStatsByPeriod(period)` | `[DailyFlowsStats!]!` |
| `flowStatsByFlow(flowId)` | `FlowStats!` |
| `flowsExecutionStatsByPeriod(period)` | `[FlowExecutionStats!]!` |

### API tokens, templates, resources, knowledge

| Field | Args | Returns | Permission |
|---|---|---|---|
| `apiToken` | `tokenId` | `APIToken` | `settings.tokens.view` + user session |
| `apiTokens` | — | `[APIToken!]!` | same |
| `flowTemplate` | `templateId` | `FlowTemplate` | `templates.view` + user session |
| `flowTemplates` | — | `[FlowTemplate!]!` | same |
| `resources` | `path`, `recursive` | `[UserResource!]!` | `resources.view` |
| `knowledgeDocuments` | `filter`, `withContent` | `[KnowledgeDocument!]!` | `knowledge.view` |
| `knowledgeDocument` | `id` | `KnowledgeDocument!` | `knowledge.view` |
| `searchKnowledge` | `query`, `filter`, `limit` | `[KnowledgeDocumentWithScore!]!` | `knowledge.search` |

When `withContent` is `false`, document `content` is returned empty to reduce payload size. Admins search/list all documents; non-admins are scoped to their `userId`.

## Mutation operations

### Flow lifecycle

| Field | Args | Returns | Permission |
|---|---|---|---|
| `createFlow` | `modelProvider`, `input`, `resourceIds?` | `Flow!` | `flows.create` |
| `putUserInput` | `flowId`, `input`, `modelProvider?`, `resourceIds?` | `ResultType!` | `flows.edit` |
| `stopFlow` | `flowId` | `ResultType!` | `flows.edit` |
| `finishFlow` | `flowId` | `ResultType!` | `flows.edit` |
| `deleteFlow` | `flowId` | `ResultType!` | `flows.delete` |
| `renameFlow` | `flowId`, `title` | `ResultType!` | `flows.edit` |

`createFlow` requires non-empty `modelProvider` and `input`, resolves the provider via `ProvidersCtrl.GetProvider`, then `Controller.CreateFlow`. Optional `resourceIds` attach user resources when the caller has `resources.view`.

### Assistants

| Field | Args | Returns | Permission |
|---|---|---|---|
| `createAssistant` | `flowId`, `modelProvider`, `input`, `useAgents`, `resourceIds?` | `FlowAssistant!` | `assistants.create` (+ flow ownership when `flowId` set) |
| `callAssistant` | `flowId`, `assistantId`, `input`, `useAgents`, `resourceIds?` | `ResultType!` | `assistants.edit` |
| `stopAssistant` | `flowId`, `assistantId` | `Assistant!` | `assistants.edit` |
| `deleteAssistant` | `flowId`, `assistantId` | `ResultType!` | `assistants.delete` |

### Providers and prompts

| Field | Args | Returns | Permission |
|---|---|---|---|
| `testAgent` | `type`, `agentType`, `agent` | `AgentTestResult!` | `settings.providers.view` |
| `testProvider` | `type`, `agents` | `ProviderTestResult!` | `settings.providers.view` |
| `createProvider` | `name`, `type`, `agents` | `ProviderConfig!` | `settings.providers.edit` |
| `updateProvider` | `providerId`, `name`, `agents` | `ProviderConfig!` | `settings.providers.edit` |
| `deleteProvider` | `providerId` | `ResultType!` | `settings.providers.edit` |
| `validatePrompt` | `type`, `template` | `PromptValidationResult!` | `settings.prompts.edit` |
| `createPrompt` | `type`, `template` | `UserPrompt!` | `settings.prompts.edit` |
| `updatePrompt` | `promptId`, `template` | `UserPrompt!` | `settings.prompts.edit` |
| `deletePrompt` | `promptId` | `ResultType!` | `settings.prompts.edit` |

`AgentConfigInput` supports model, token limits, sampling (`temperature`, `topK`, `topP`, penalties), `reasoning` (`effort`, `maxTokens`), and `price`.

`PromptValidationErrorType`: `syntax_error`, `unauthorized_variable`, `rendering_failed`, `empty_template`, `variable_type_mismatch`, `unknown_type`.

### Tokens, favorites, templates, knowledge, anonymize

| Field | Notes | Permission |
|---|---|---|
| `createAPIToken` | Returns `APITokenWithSecret` (secret once) | `settings.tokens.create` + user session |
| `updateAPIToken` | name / status | `settings.tokens.edit` + user session |
| `deleteAPIToken` | returns `Boolean!` | `settings.tokens.delete` + user session |
| `addFavoriteFlow` / `deleteFavoriteFlow` | favorites list | `settings.user.edit` + user session |
| `createFlowTemplate` / `updateFlowTemplate` / `deleteFlowTemplate` | title + text | `templates.*` + user session |
| `createKnowledgeDocument` | re-embeds content | `knowledge.create` |
| `updateKnowledgeDocument` | non-null fields re-embed | `knowledge.edit` |
| `deleteKnowledgeDocument` | | `knowledge.delete` |
| `anonymizeText` | uses cloud anonymizer when configured | `anonymize.call` |

## Subscription operations

Subscriptions use the same `/api/v1/graphql` URL over WebSocket (`graphql-ws` protocol on the frontend). Publishers fan out through `SubscriptionsController` with buffered channels (length **50**, send timeout **5s**).

### Flow-scoped streams

Require the matching `*.subscribe` privilege and flow ownership (unless admin).

| Field | Payload | Permission |
|---|---|---|
| `taskCreated` / `taskUpdated` | `Task!` | `tasks.subscribe` |
| `assistantCreated` / `Updated` / `Deleted` | `Assistant!` | `assistants.subscribe` |
| `flowFileAdded` / `Updated` / `Deleted` | `FlowFile!` | `flow_files.subscribe` |
| `screenshotAdded` | `Screenshot!` | `screenshots.subscribe` |
| `terminalLogAdded` | `TerminalLog!` | `termlogs.subscribe` |
| `messageLogAdded` / `Updated` | `MessageLog!` | `msglogs.subscribe` |
| `agentLogAdded` | `AgentLog!` | `agentlogs.subscribe` |
| `searchLogAdded` | `SearchLog!` | `searchlogs.subscribe` |
| `vectorStoreLogAdded` | `VectorStoreLog!` | `vecstorelogs.subscribe` |
| `toolCallLogAdded` / `Updated` | `ToolCallLog!` | `toolcalls.subscribe` |
| `assistantLogAdded` / `Updated` | `AssistantLog!` | `assistantlogs.subscribe` |

`assistantLogUpdated` supports streaming chunks via `appendPart: Boolean!`. The UI concatenates partial `message` / `result` / `thinking` fields.

### Global / user-scoped streams

| Field | Permission | Admin behavior |
|---|---|---|
| `flowCreated` / `Updated` / `Deleted` | `flows.subscribe` | Admin uses broadcast channels; others receive own flows |
| `providerCreated` / `Updated` / `Deleted` | `settings.providers.subscribe` | user-scoped |
| `apiTokenCreated` / `Updated` / `Deleted` | `settings.tokens.subscribe` | user-scoped |
| `settingsUserUpdated` | `settings.user.subscribe` | user-scoped |
| `flowTemplateCreated` / `Updated` / `Deleted` | `templates.subscribe` | user-scoped |
| `resourceAdded` / `Updated` / `Deleted` | `resources.subscribe` | admin can use admin variants |
| `knowledgeDocumentCreated` / `Updated` / `Deleted` | `knowledge.subscribe` | admin can use admin variants |

## Request examples

<RequestExample>
```bash
# Query settings (session cookie or Bearer token)
curl -sS -X POST 'https://localhost:8443/api/v1/graphql' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <API_TOKEN>' \
  -d '{"query":"query { settings { version debug askUser dockerInside isDevelopMode assistantUseAgents } }"}'
```
</RequestExample>

<ResponseExample>
```json
{
  "data": {
    "settings": {
      "version": "…",
      "debug": false,
      "askUser": true,
      "dockerInside": true,
      "isDevelopMode": false,
      "assistantUseAgents": false
    }
  }
}
```
</ResponseExample>

<RequestExample>
```graphql
# Create a flow
mutation CreateFlow($provider: String!, $input: String!) {
  createFlow(modelProvider: $provider, input: $input) {
    id
    title
    status
    provider { name type }
    createdAt
  }
}
```
</RequestExample>

Variables:

```json
{
  "provider": "openai",
  "input": "Perform a scoped web application reconnaissance against example.com"
}
```

<RequestExample>
```graphql
# Live terminal + message stream for a flow
subscription FlowLogs($flowId: ID!) {
  terminalLogAdded(flowId: $flowId) {
    id type text terminal createdAt
  }
}
```
</RequestExample>

`modelProvider` is a **provider name** (built-in type name or user-defined provider profile name), not a raw model ID. Resolve available names via `providers` or `settingsProviders`.

## Frontend client

The React app uses Apollo Client (`frontend/src/lib/apollo.ts`):

| Concern | Behavior |
|---|---|
| HTTP endpoint | `${origin}/api/v1/graphql` with `credentials: 'include'` |
| WebSocket URL | `ws(s)://host/api/v1/graphql` via `graphql-ws` |
| Split link | Subscriptions → WS; everything else → HTTP |
| Retry | Infinite reconnect with exponential backoff (cap **30s**) |
| Cache | `InMemoryCache` with subscription-driven list updates |
| Streaming | `assistantLogUpdated` + `appendPart` accumulated client-side (50 ms throttle) |
| Auth errors | Dispatches `auth:refresh` on 401/403 / `UNAUTHENTICATED` / `FORBIDDEN` |

Frontend schema mirror: `frontend/graphql-schema.graphql`. Regenerated types: `pnpm run graphql:generate` (`graphql-codegen.ts` → `frontend/src/graphql/types.ts`).

## Schema ownership and codegen

:::files
backend/
  pkg/graph/
    schema.graphqls          # Source of truth
    schema.resolvers.go      # Resolver implementations
    generated.go             # gqlgen exec
    model/models_gen.go      # Generated models
    context.go               # Auth helpers
    subscriptions/           # Pub/sub controller
  gqlgen/gqlgen.yml
  pkg/server/services/graphql.go
  pkg/server/router.go       # Route mount
frontend/
  graphql-schema.graphql     # Client schema copy
  graphql-codegen.ts
  src/lib/apollo.ts
:::

After schema changes:

```bash
# Backend (from backend/)
go run github.com/99designs/gqlgen --config ./gqlgen/gqlgen.yml

# Frontend (from frontend/)
pnpm run graphql:generate
```

`gqlgen.yml` maps GraphQL `ID` primarily to Go `Int64` (with fallbacks). Input types for agent config reuse the same Go structs as output models.

## Permission map (compact)

| Resource prefix | Typical actions |
|---|---|
| `flows` | view, create, edit, delete, subscribe (+ `.admin`) |
| `tasks`, `subtasks` | view, subscribe |
| `assistants` | view, create, edit, delete, subscribe |
| `*_logs` / `termlogs` / `msglogs` / `agentlogs` / `searchlogs` / `vecstorelogs` / `toolcalls` / `screenshots` / `flow_files` | view, subscribe |
| `providers` | view |
| `settings` | view |
| `settings.providers` | view, edit, subscribe |
| `settings.prompts` | view, edit |
| `settings.tokens` | view, create, edit, delete, subscribe |
| `settings.user` | view, edit, subscribe |
| `templates` | view, create, edit, delete, subscribe |
| `resources` | view, subscribe |
| `knowledge` | view, create, edit, delete, search, subscribe |
| `usage` | view |
| `containers` | view (gates terminal hydration on Flow) |
| `anonymize` | call |

## Operational constraints and failure modes

| Condition | Behavior |
|---|---|
| Missing/invalid auth | HTTP 403 from Gin auth middleware (`ErrAuthRequired`) |
| Missing privilege | GraphQL error: `requested permission '…' not found` |
| Non-owner flow access | `not permitted` |
| Empty `modelProvider` / `input` on create | Resolver validation errors |
| Provider not found | Error from `ProvidersCtrl.GetProvider` |
| Complexity > 20000 | gqlgen complexity rejection |
| WebSocket origin not allowed | Upgrade rejected by `CheckOrigin` |
| Anonymizer unavailable | `anonymizeText` → `anonymizer is not available` |
| Default cookie salt | Token creation disabled |
| API token used for token CRUD | Rejected: non-user session not allowed |
| Subscription send backlog | Channel buffer 50; send drops after 5s timeout |

<Tip>
Playground is mounted under the public group at `/api/v1/graphql/playground` and points the GraphiQL client at `/api/v1/graphql`. Authenticated operations still require a valid session cookie or Bearer header on the actual GraphQL requests.
</Tip>

## Relation to REST

GraphQL is the primary real-time and UI surface; REST under `/api/v1` covers overlapping CRUD (flows, tasks, files, tokens, knowledge, etc.) and Swagger at `/api/v1/swagger`. Prefer GraphQL for:

- Multi-entity reads with nested selection sets
- Live log / status streams
- Provider testing and prompt validation in the settings UI

Prefer REST for simple scripted file/resource binary operations and OpenAPI tooling.

## Next

<CardGroup>
  <Card title="REST API" href="/rest-api">
    Gin routes under `/api/v1`, overlapping CRUD resources, and Swagger.
  </Card>
  <Card title="Authentication and API tokens" href="/auth-and-api-tokens">
    Session login, OAuth, Bearer tokens, and permission-scoped token lifecycle.
  </Card>
  <Card title="Flows, tasks, and subtasks" href="/flows-tasks-subtasks">
    Execution hierarchy, status states, and putUserInput / stop / finish boundaries.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    Provider wiring and profiles used by `modelProvider` and settings mutations.
  </Card>
  <Card title="Memory and knowledge" href="/memory-and-knowledge">
    Vector knowledge documents, embeddings, and searchKnowledge filters.
  </Card>
  <Card title="Development and testing" href="/development-and-testing">
    gqlgen regeneration, frontend graphql:generate, and contributor workflows.
  </Card>
</CardGroup>

---

## 19. Provider configuration schema

> Per-agent YAML and AgentsConfig fields: model, temperature, top_p, top_k, max_tokens, json mode, reasoning, price, extra_body; built-in provider config.yml baselines and UI testAgent/testProvider.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/19-provider-configuration-schema.md
- Generated: 2026-07-10T07:09:24.930Z

### Source Files

- `backend/pkg/providers/pconfig/config.go`
- `backend/pkg/providers/openai/config.yml`
- `backend/pkg/providers/anthropic/config.yml`
- `backend/pkg/providers/ollama/config.yml`
- `examples/configs/vllm-qwen3.5-27b-fp8.provider.yml`
- `backend/pkg/graph/schema.graphqls`
- `backend/pkg/server/models/providers.go`

---
title: "Provider configuration schema"
description: "Per-agent YAML and AgentsConfig fields: model, temperature, top_p, top_k, max_tokens, json mode, reasoning, price, extra_body; built-in provider config.yml baselines and UI testAgent/testProvider."
---

PentAGI maps every LLM call to a **per-agent** options block. Runtime ownership lives in `backend/pkg/providers/pconfig`: `ProviderConfig` holds thirteen `AgentConfig` slots; each slot becomes `llms.CallOption` values via `BuildOptions()`. Built-in providers embed a `config.yml` baseline; custom endpoints load YAML/JSON from `LLM_SERVER_CONFIG`; the UI and GraphQL persist user profiles as JSON on the `providers` table and expose the same agents through `AgentsConfig` / `AgentsConfigInput`.

## Config surfaces

| Surface | Format | Where it applies |
|---|---|---|
| Embedded `config.yml` | YAML | Default agent params for each built-in provider package (`openai`, `anthropic`, `gemini`, `bedrock`, `deepseek`, `glm`, `kimi`, `qwen`, `ollama`) |
| Embedded `models.yml` | YAML list | Optional model catalog + default prices for the Settings UI |
| File path (`LLM_SERVER_CONFIG`) | `.yml` / `.yaml` / `.json` | Custom OpenAI-compatible provider; empty path uses `EmptyProviderConfigRaw` |
| GraphQL `AgentsConfig` | camelCase GraphQL | Create/update/test user provider profiles in Settings |
| DB `providers.config` | JSON | Stored user-defined provider after `createProvider` / `updateProvider` |

Load entry points:

- `pconfig.LoadConfig(path, defaultOptions)` — file by extension
- `pconfig.LoadConfigData(data, defaultOptions)` — YAML first, then JSON
- `pconfig.LoadModelsConfigData(data)` — models catalog only

```text
YAML / GraphQL / DB JSON
        │
        ▼
  pconfig.ProviderConfig
  (simple … pentester)
        │
        ├── GetOptionsForType(agent) → []llms.CallOption
        ├── GetPriceInfoForType(agent) → *PriceInfo
        └── GetModelsMap() → model name per agent
        │
        ▼
  provider.Call / CallEx / CallWithTools
```

## Agent type keys

YAML and Go use **snake_case**. GraphQL and the Settings UI use **camelCase** for compound names.

| YAML / `ProviderOptionsType` | GraphQL `AgentConfigType` / field | Role (typical) |
|---|---|---|
| `simple` | `simple` | Lightweight completion / utility |
| `simple_json` | `simpleJson` | Structured JSON responses |
| `primary_agent` | `primaryAgent` | Flow orchestration |
| `assistant` | `assistant` | Interactive assistant mode |
| `generator` | `generator` | Subtask plan generation |
| `refiner` | `refiner` | Plan refinement |
| `adviser` | `adviser` | Advice / mentor calls |
| `reflector` | `reflector` | Reflection steps |
| `searcher` | `searcher` | Search-oriented calls |
| `enricher` | `enricher` | Context enrichment |
| `coder` | `coder` | Code generation |
| `installer` | `installer` | Install / setup tooling |
| `pentester` | `pentester` | Offensive analysis |

`pconfig.AllAgentTypes` is the authoritative ordered list of these thirteen keys.

### Fallbacks

- **`simple_json`**: if unset, uses `simple` options and appends `llms.WithJSONMode()`.
- **`assistant`**: if unset, uses `primary_agent`, else provider `defaultOptions`.
- **Legacy `agent`**: older configs with top-level `agent` are rewritten to `primary_agent`; if `assistant` is missing it also copies from `primary_agent`.

## AgentConfig fields

Full runtime schema (`pconfig.AgentConfig`). Only keys **present in the source document** are applied as call options (`BuildOptions` checks the unmarshaled `raw` map). Omitting a key leaves it unset rather than forcing a zero value into the provider request.

### Sampling and generation

<ParamField body="model" type="string">
Model id sent with `llms.WithModel`. Required for useful GraphQL profiles (`AgentConfigInput.model` is non-null). Ollama embedded baseline omits model names so the runtime/default model can supply them.
</ParamField>

<ParamField body="max_tokens" type="int">
Completion budget (`llms.WithMaxTokens`).
</ParamField>

<ParamField body="temperature" type="float">
Sampling temperature (`llms.WithTemperature`).
</ParamField>

<ParamField body="top_p" type="float">
Nucleus sampling (`llms.WithTopP`).
</ParamField>

<ParamField body="top_k" type="int">
Top-k sampling (`llms.WithTopK`). Common on local/vLLM configs; less common on OpenAI-style cloud baselines.
</ParamField>

<ParamField body="min_p" type="float">
Min-p sampling (`llms.WithMinP`). YAML-only relative to GraphQL (not on `AgentConfig` / `AgentConfigInput`).
</ParamField>

<ParamField body="n" type="int">
Number of completions (`llms.WithN`). Present in most embedded YAML baselines; not exposed on GraphQL `AgentConfig`.
</ParamField>

<ParamField body="min_length" type="int">
Minimum generation length (`llms.WithMinLength`). Available in GraphQL.
</ParamField>

<ParamField body="max_length" type="int">
Maximum generation length (`llms.WithMaxLength`). Available in GraphQL.
</ParamField>

<ParamField body="repetition_penalty" type="float">
Repetition penalty (`llms.WithRepetitionPenalty`).
</ParamField>

<ParamField body="frequency_penalty" type="float">
Frequency penalty (`llms.WithFrequencyPenalty`).
</ParamField>

<ParamField body="presence_penalty" type="float">
Presence penalty (`llms.WithPresencePenalty`).
</ParamField>

### JSON mode

<ParamField body="json" type="bool">
When the key is present, enables `llms.WithJSONMode()`. Embedded baselines set `json: true` only under `simple_json`.
</ParamField>

<ParamField body="response_mime_type" type="string">
Optional MIME type via `llms.WithResponseMIMEType` when non-empty. YAML-only (not on GraphQL schema).
</ParamField>

### Reasoning

```yaml
reasoning:
  effort: medium   # low | medium | high  (OpenAI-style effort)
  max_tokens: 1024 # token budget for thinking (Anthropic-style / token path)
```

`BuildOptions` behavior:

- If `effort` is `low`, `medium`, or `high` → `llms.WithReasoning(effort, 0)`.
- Else if `max_tokens` is in `(0, 32000]` → `llms.WithReasoning(ReasoningNone, max_tokens)`.
- GraphQL enum `ReasoningEffort`: `high` | `medium` | `low`.

Provider baselines differ:

| Style | Typical providers | Example |
|---|---|---|
| Effort string | OpenAI, Gemini, DeepSeek (pro tiers) | `reasoning: { effort: medium }` |
| Token budget | Anthropic, Bedrock Claude-class | `reasoning: { max_tokens: 1024 }` |
| Off / external | Ollama sampling-only; Qwen/vLLM via `extra_body` | no `reasoning` block, or thinking flags in body |

### Price

```yaml
price:
  input: 1.1        # USD per 1M input tokens
  output: 4.4       # USD per 1M output tokens
  cache_read: 0.275 # USD per 1M cache-read tokens (optional)
  cache_write: 3.75 # USD per 1M cache-write tokens (optional)
```

`CallUsage.UpdateCost` converts token counts using these rates. If the provider already filled cost fields (for example OpenRouter upstream cost), prices are not overwritten. Without cache rates, all input tokens are billed at `input`. With cache rates, uncached tokens use `input`, plus `cache_read` / `cache_write` components.

Prices are for usage accounting and UI display; they do not change model sampling.

### Extra body (YAML / full schema)

```yaml
extra_body:
  chat_template_kwargs:
    enable_thinking: false
  # or provider-specific maps, e.g. DeepSeek:
  # thinking:
  #   type: disabled
```

When present, applied as `openai.WithExtraBody(extra_body)` — intended for OpenAI-compatible endpoints (vLLM, DeepSeek, custom aggregators). **Not** part of GraphQL `AgentConfig` / `AgentConfigInput`; set these in file-based custom configs under `examples/configs` or `LLM_SERVER_CONFIG`.

## GraphQL AgentsConfig vs full YAML

GraphQL types for Settings CRUD and tests:

| GraphQL field | YAML equivalent | Notes |
|---|---|---|
| `model` | `model` | Required in input |
| `maxTokens` | `max_tokens` | |
| `temperature` | `temperature` | |
| `topK` / `topP` | `top_k` / `top_p` | |
| `minLength` / `maxLength` | `min_length` / `max_length` | |
| `repetitionPenalty` / `frequencyPenalty` / `presencePenalty` | same snake_case | |
| `reasoning.effort` / `reasoning.maxTokens` | `reasoning.effort` / `reasoning.max_tokens` | |
| `price.input` … `cacheWrite` | `price.*` | All four floats required when price is sent |

**Not** on GraphQL `AgentConfig` today: `n`, `min_p`, `json`, `response_mime_type`, `extra_body`.

Implications:

- UI-created profiles cannot express thinking toggles that only live in `extra_body`; use custom YAML for those.
- `simple_json` in UI still maps to the agent slot; JSON-mode enforcement for missing `SimpleJSON` config still follows `buildSimpleJSONOptions` when options fall through to `simple` + JSON mode.
- `ConvertAgentConfigFromGqlModel` rebuilds a presence-aware `raw` map so only submitted fields become call options.

## Built-in config.yml baselines

Each provider package (except pure custom) embeds defaults:

:::files
backend/pkg/providers/
  openai/config.yml
  anthropic/config.yml
  gemini/config.yml
  bedrock/config.yml
  deepseek/config.yml
  glm/config.yml
  kimi/config.yml
  qwen/config.yml
  ollama/config.yml
  custom/  → file path or EmptyProviderConfigRaw
:::

| Provider | Baseline pattern |
|---|---|
| **openai** | Mixed GPT / o-series models; effort-based reasoning on agentic slots; `simple` / `simple_json` on cheaper models with temp/top_p; full `price` including `cache_read` |
| **anthropic** | Haiku for utility / JSON; Sonnet for most agents; Opus for generator; reasoning via `max_tokens`; cache read/write prices |
| **gemini** | Flash-lite for simple/JSON; Pro-preview with effort reasoning for primary/assistant/generator-class work |
| **bedrock** | Cross-family model ids (OpenAI-compat + Anthropic Claude); token reasoning on Claude slots |
| **deepseek** | Flash for workhorse with `extra_body.thinking.type: disabled`; Pro for multi-step agents with `reasoning.effort` (avoid temp/top_p on thinking Pro) |
| **glm / kimi / qwen** | Tiered model matrix + provider-specific thinking constraints (see package comments in each `config.yml`) |
| **ollama** | Sampling only (`temperature`, `top_p`, `n`, `max_tokens`); no embedded model names or prices |
| **custom** | Defaults: temp `1.0`, top_p `1.0`, n `1`, max_tokens `16384`, plus optional `LLM_SERVER_MODEL`; config from `LLM_SERVER_CONFIG` or empty skeleton |

`EmptyProviderConfigRaw` is a JSON skeleton of all thirteen agent keys with empty objects — used when custom has no config path.

`patchProviderConfig` fills any **nil** agent pointer from the type’s default config and copies that provider’s `defaultOptions` before create/test/run.

## Models catalog

Optional per-provider `models.yml` entries become `ModelConfig`:

| Field | Meaning |
|---|---|
| `name` | Model id |
| `description` | UI copy |
| `release_date` | `YYYY-MM-DD` |
| `thinking` | Whether the model is treated as a thinking/reasoning model in UI |
| `price` | Same shape as agent `price` |

Exposed under GraphQL `ProvidersModelsList` / `settingsProviders.models`. Custom providers may instead discover models over HTTP (`LoadModelsFromHTTP`).

## Minimal YAML examples

<CodeGroup>

```yaml title="Cloud-style agent (effort reasoning)"
primary_agent:
  model: o4-mini
  n: 1
  max_tokens: 16384
  reasoning:
    effort: medium
  price:
    input: 1.1
    output: 4.4
    cache_read: 0.275

simple_json:
  model: gpt-5.4-nano
  temperature: 0.5
  top_p: 0.5
  n: 1
  max_tokens: 4096
  json: true
```

```yaml title="Local vLLM / Qwen (extra_body thinking control)"
simple:
  model: "Qwen/Qwen3.5-27B-FP8"
  temperature: 0.7
  top_k: 20
  top_p: 0.8
  min_p: 0.0
  presence_penalty: 1.5
  repetition_penalty: 1.0
  n: 1
  max_tokens: 32768
  extra_body:
    chat_template_kwargs:
      enable_thinking: false

coder:
  model: "Qwen/Qwen3.5-27B-FP8"
  temperature: 0.6
  top_k: 20
  top_p: 0.95
  n: 1
  max_tokens: 32768
```

</CodeGroup>

Copy-paste full provider files live under `examples/configs/` (vLLM, Ollama, OpenRouter, DeepInfra, Azure, and others).

## Test agent and test provider

Settings → provider form calls GraphQL mutations before or after save. Backend path: resolver → `providerController.TestAgent` / `TestProvider` → temporary provider from patched config → `tester.TestProvider`.

### Mutations

:::endpoint POST /api/v1/graphql GraphQL mutation testAgent
**Permission:** `settings.providers.view`

**Arguments**

| Arg | Type | Description |
|---|---|---|
| `type` | `ProviderType!` | Backend transport (`openai`, `anthropic`, `custom`, …) |
| `agentType` | `AgentConfigType!` | Which agent slot to exercise |
| `agent` | `AgentConfigInput!` | Single-agent config |

**Behavior:** Builds a one-slot `ProviderConfig`, patches missing slots from defaults, constructs a temporary provider, runs the built-in test registry filtered to that agent type.

**Returns:** `AgentTestResult { tests: [TestResult!]! }`
:::

:::endpoint POST /api/v1/graphql GraphQL mutation testProvider
**Permission:** `settings.providers.view`

**Arguments**

| Arg | Type | Description |
|---|---|---|
| `type` | `ProviderType!` | Backend transport |
| `agents` | `AgentsConfigInput!` | All thirteen agent configs |

**Behavior:** Same as full-provider test over every agent type with default parallel workers.

**Returns:** `ProviderTestResult` with one `AgentTestResult` per agent key (`simple`, `simpleJson`, `primaryAgent`, …).
:::

### TestResult fields

| Field | Type | Meaning |
|---|---|---|
| `name` | `String!` | Case name from registry |
| `type` | `String!` | `completion`, `json`, tool-oriented types, etc. |
| `result` | `Boolean!` | Pass/fail |
| `reasoning` | `Boolean!` | Whether reasoning path was involved |
| `streaming` | `Boolean!` | Streaming case |
| `latency` | `Int` | Milliseconds when measured |
| `error` | `String` | Failure detail |

### Compatibility rules

- `simple_json` runs **only** JSON suite cases.
- All other agent types run **non-JSON** cases (completion / tools / messages).
- Built-in registry: `backend/pkg/providers/tester/testdata/tests.yml` (basic math/text, multi-message, JSON shape checks, tool calling).
- Streaming cases are skipped unless streaming mode is enabled on the tester options (default UI path uses non-verbose, parallel workers; streaming not forced on).

### UI flow

<Steps>
  <Step title="Edit agent parameters">
    Open Settings → Providers, pick a base type (must be ready in `ProvidersReadinessStatus`), set per-agent `model` and optional sampling / reasoning / price fields.
  </Step>
  <Step title="Test one agent or the whole profile">
    Run **test agent** (`testAgent`) for a single slot or **test provider** (`testProvider`) for all slots. Failures surface as `TestResult.error` with latency.
  </Step>
  <Step title="Save profile">
    `createProvider` / `updateProvider` require `settings.providers.edit`, patch nil agents from defaults, marshal JSON into `providers.config`.
  </Step>
  <Step title="Verify">
    Query `settingsProviders` for `userDefined`, readiness flags, and default baselines. Create a flow selecting the provider name only after tests pass for critical agents (`primary_agent`, `generator`, `pentester`, `simple_json`).
  </Step>
</Steps>

## Persistence and provider types

REST/DB model `models.Provider`:

| Column / field | Role |
|---|---|
| `type` | `ProviderType` enum: `openai`, `anthropic`, `gemini`, `bedrock`, `ollama`, `custom`, `deepseek`, `glm`, `kimi`, `qwen` |
| `name` | Unique display/runtime provider name for flow selection |
| `config` | JSON blob of the full `ProviderConfig` |
| `user_id` | Owner of user-defined profiles |

Invalid `ProviderType` values fail validation (`Valid()` whitelist) and surface as API errors (422 path on REST-style validation).

## Constraints and failure modes

| Symptom | Likely cause |
|---|---|
| Options ignored for a field | Key omitted from YAML/JSON; `BuildOptions` only applies keys present in `raw` |
| Reasoning not applied | Missing `reasoning` key, invalid effort, or `max_tokens` outside `(0, 32000]` |
| JSON tests fail only on `simple_json` | Model ignores JSON mode; or UI profile lost `json: true` (YAML-only flag) while fallback still adds JSON mode only when `SimpleJSON` options are empty |
| Thinking flags ignored in UI profile | `extra_body` not in GraphQL; use file config for custom/vLLM |
| `default provider config not found` | Provider type not loaded into `defaultConfigs` (credentials/env not enabling that type) |
| Custom provider empty agents | No `LLM_SERVER_CONFIG` → empty skeleton; set model via env and/or a full YAML file |
| Legacy `agent` key | Still accepted → mapped to `primary_agent` |
| Cost stays zero | No `price` on that agent and no upstream cost in usage metadata |
| Unsupported file extension | Only `.json`, `.yaml`, `.yml` |

<Warning>
GraphQL and UI profiles are a **subset** of the full `AgentConfig` schema. For `extra_body`, `min_p`, `n`, and explicit `json` flags, prefer YAML under `LLM_SERVER_CONFIG` or the examples in `examples/configs/`, then validate with live calls or the tester against type `custom`.
</Warning>

## Related pages

<CardGroup cols={2}>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    Env keys, readiness, and wiring cloud providers before editing agent schemas.
  </Card>
  <Card title="Local and custom providers" href="/local-and-custom-providers">
    `LLM_SERVER_*`, Ollama, legacy/preserve reasoning, aggregator endpoints.
  </Card>
  <Card title="Example provider configs" href="/example-provider-configs">
    Full copy-paste YAML for vLLM, Ollama, OpenRouter, DeepInfra, Azure.
  </Card>
  <Card title="Deploy with vLLM and Qwen" href="/vllm-qwen-deployment">
    Thinking vs non-thinking provider YAML and supervision for local models.
  </Card>
  <Card title="GraphQL API" href="/graphql-api">
    Full mutation/query surface including providers and test operations.
  </Card>
  <Card title="Agents and supervision" href="/agents-and-supervision">
    How agent slots map to execution roles and hard limits during flows.
  </Card>
</CardGroup>

---

## 20. Tools reference

> Named tool registry: terminal, file, browser, search engines, memory store/search, agent delegation tools, result tools, barrier tools, and assistant flow-control tools with argument shapes.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/20-tools-reference.md
- Generated: 2026-07-10T07:09:14.293Z

### Source Files

- `backend/pkg/tools/registry.go`
- `backend/pkg/tools/args.go`
- `backend/pkg/tools/tools.go`
- `backend/pkg/tools/executor.go`
- `backend/pkg/tools/flow_manager.go`
- `backend/docs/flow_execution.md`
- `backend/pkg/tools/registry_test.go`

---
title: "Tools reference"
description: "Named tool registry: terminal, file, browser, search engines, memory store/search, agent delegation tools, result tools, barrier tools, and assistant flow-control tools with argument shapes."
---

Named LLM tools live in `backend/pkg/tools`. Constants and JSON schemas are registered in `registryDefinitions`; argument structs live in `args.go`; per-agent tool bundles are assembled by `flowToolsExecutor.Get*Executor`; runtime dispatch, logging, summarization, and barrier detection run through `customExecutor.Execute`.

Every tool name maps to a `ToolType` used for observability and storage policy:

| `ToolType` | String | Role |
|---|---|---|
| `EnvironmentToolType` | `environment` | Docker terminal/file and assistant flow-control tools |
| `SearchNetworkToolType` | `search_network` | Browser and external search engines |
| `SearchVectorDbToolType` | `search_vector_db` | pgvector and Graphiti retrieval |
| `StoreVectorDbToolType` | `store_vector_db` | Guide/answer/code store tools |
| `AgentToolType` | `agent` | Specialist delegation tools |
| `StoreAgentResultToolType` | `store_agent_result` | Specialist result and plan outputs |
| `BarrierToolType` | `barrier` | Primary-agent exit tools (`done`, `ask`) |

<Note>
Executor-level **barriers** are broader than `BarrierToolType`. Each agent executor marks its terminal result tool (for example `code_result`, `hack_result`, `subtask_list`) in `customExecutor.barriers` so a successful call ends that agent turn.
</Note>

## Registry catalog

| Name | Type | Schema struct | Summary |
|---|---|---|---|
| `terminal` | environment | `TerminalAction` | Run a command in the flow Docker container |
| `file` | environment | `FileAction` | Read or write a file by absolute path |
| `browser` | search_network | `Browser` | Fetch page content (markdown/html/links) |
| `google` | search_network | `SearchAction` | Google CSE short query |
| `duckduckgo` | search_network | `SearchAction` | Anonymous DuckDuckGo query |
| `tavily` | search_network | `SearchAction` | Detailed Tavily research |
| `traversaal` | search_network | `SearchAction` | Traversaal Q&A + links |
| `perplexity` | search_network | `SearchAction` | LLM-augmented research report |
| `searxng` | search_network | `SearchAction` | Privacy meta-search |
| `sploitus` | search_network | `SploitusAction` | Exploit/PoC aggregator |
| `search_in_memory` | search_vector_db | `SearchInMemoryAction` | Multi-query vector memory search |
| `search_guide` / `store_guide` | search/store vector | `SearchGuideAction` / `StoreGuideAction` | Guide memory |
| `search_answer` / `store_answer` | search/store vector | `SearchAnswerAction` / `StoreAnswerAction` | Answer memory |
| `search_code` / `store_code` | search/store vector | `SearchCodeAction` / `StoreCodeAction` | Code sample memory |
| `graphiti_search` | search_vector_db | `GraphitiSearchAction` | Temporal knowledge graph search |
| `search` | agent | `ComplexSearch` | Delegate to searcher agent |
| `coder` | agent | `CoderAction` | Delegate to coder agent |
| `pentester` | agent | `PentesterAction` | Delegate to pentester agent |
| `maintenance` | agent | `MaintenanceAction` | Delegate to installer/DevOps agent |
| `memorist` | agent | `MemoristAction` | Delegate to memorist agent |
| `advice` | agent | `AskAdvice` | Delegate to adviser/mentor |
| `search_result` | store_agent_result | `SearchResult` | Searcher terminal output |
| `code_result` | store_agent_result | `CodeResult` | Coder terminal output |
| `hack_result` | store_agent_result | `HackResult` | Pentester terminal output |
| `maintenance_result` | store_agent_result | `TaskResult` | Installer terminal output |
| `memorist_result` | store_agent_result | `MemoristResult` | Memorist terminal output |
| `enricher_result` | store_agent_result | `EnricherResult` | Enricher terminal output |
| `report_result` | store_agent_result | `TaskResult` | Reporter terminal output |
| `subtask_list` | store_agent_result | `SubtaskList` | Generator full plan |
| `subtask_patch` | store_agent_result | `SubtaskPatch` | Refiner delta plan |
| `done` | barrier | `Done` | Finish subtask success/failure |
| `ask` | barrier | `AskUser` | Pause for user input (`ASK_USER`) |
| `get_flow_status` | environment | `GetFlowStatusAction` | Assistant flow status query |
| `stop_flow` | environment | `StopFlowAction` | Cancel running automation task |
| `submit_flow_input` | environment | `SubmitFlowInputAction` | Answer `ask` or start a new task |
| `patch_flow_subtasks` | environment | `PatchFlowSubtasksAction` | Patch planned subtasks |
| `wait_flow_completion` | environment | `WaitFlowCompletionAction` | Block until running task ends |

## Argument conventions

Most tools require a `message` engagement-log field (1–2 short sentences in the engagement language). Technical payloads (`query`, `question`, `result`, stored knowledge) are English-only so shared indexes stay searchable.

Boolean/integer JSON fields use custom `Bool` / `Int64` wrappers so models can pass loose types safely.

### Environment tools

<ParamField body="terminal.input" type="string" required>
Command to run in the primary Docker container. One command per call.
</ParamField>
<ParamField body="terminal.cwd" type="string" required>
Working directory inside the container.
</ParamField>
<ParamField body="terminal.detach" type="boolean" required>
`true` for interactive/long-running processes (shells, listeners, servers); no stdout capture. `false` for batch commands that must finish and return output.
</ParamField>
<ParamField body="terminal.timeout" type="integer" required>
Seconds. `0` or non-positive uses server default (`TERMINAL_TOOL_TIMEOUT`, default `1200`). Explicit values accepted up to `10800` (3 hours); out-of-range values fall back to the server default.
</ParamField>
<ParamField body="terminal.message" type="string" required>
Engagement-log commentary for the command.
</ParamField>

```json
{
  "input": "timeout 55 nmap -sV 10.0.0.5",
  "cwd": "/work",
  "detach": false,
  "timeout": 60,
  "message": "Service scan on the target before web enumeration."
}
```

<ParamField body="file.action" type="string" required>
`read_file` or `write_file`.
</ParamField>
<ParamField body="file.path" type="string" required>
Absolute path in the container.
</ParamField>
<ParamField body="file.content" type="string">
Raw file body for `write_file`.
</ParamField>
<ParamField body="file.message" type="string" required>
Engagement-log commentary.
</ParamField>

User uploads and resources are synced under `/work/uploads` and `/work/resources` on the primary container.

### Browser and search engines

| Field | Tools | Constraints |
|---|---|---|
| `url` | `browser` | Required target URL |
| `action` | `browser` | `markdown` \| `html` \| `links` |
| `query` | engines | English technical query |
| `max_results` | engines | Min 1; max 10 (default 5). Sploitus max 25 (default 10) |
| `exploit_type` | `sploitus` | Optional `exploits` (default) or `tools` |
| `sort` | `sploitus` | Optional `default` \| `date` \| `score` |
| `message` | all | Engagement-log commentary |

Network tools register only when `IsAvailable()` is true (scraper URLs, API keys, feature flags). Missing engines are omitted from the agent tool list rather than failing at call time.

### Memory store and search

| Tool | Required filters / fields |
|---|---|
| `search_in_memory` | `questions` (1–5 English queries); optional `task_id`, `subtask_id` hard filters |
| `search_guide` / `store_guide` | `type`: `install` \| `configure` \| `use` \| `pentest` \| `development` \| `other` |
| `search_answer` / `store_answer` | `type`: `guide` \| `vulnerability` \| `code` \| `tool` \| `other` |
| `search_code` | `lang` (markdown language id, e.g. `python`) |
| `store_code` | `code`, `question`, `lang`, `explanation`, `description` |
| Store tools | Anonymize IPs, domains, credentials, paths, API keys before store |

Multi-query search merges, deduplicates, and ranks by relevance. Embeddings and pgvector availability gate these tools.

### Graphiti search

`graphiti_search` is optional and requires an enabled Graphiti client.

<ParamField body="search_type" type="string" required>
One of: `temporal_window`, `entity_relationships`, `diverse_results`, `episode_context`, `successful_tools`, `recent_context`, `entity_by_label`.
</ParamField>
<ParamField body="query" type="string" required>
English natural-language graph query.
</ParamField>

Optional fields depend on type: `time_start`/`time_end` (ISO 8601) for `temporal_window`; `center_node_uuid` and `max_depth` (default 2, max 3) for `entity_relationships`; `diversity_level` (`low`/`medium`/`high`); `min_mentions`; `recency_window` (`1h`/`6h`/`24h`/`7d`); `node_labels`; `edge_types`; `max_results`.

If Graphiti is disabled, the handler returns an informational string instead of an error.

### Agent delegation tools

| Tool | Payload | Result tool |
|---|---|---|
| `search` | `question` (English research brief) | `search_result` |
| `coder` | `question` (dev task) | `code_result` |
| `pentester` | `question` (pentest task) | `hack_result` |
| `maintenance` | `question` (env/tool setup) | `maintenance_result` |
| `memorist` | `question`; optional `task_id`/`subtask_id` | `memorist_result` |
| `advice` | `question`; optional `code`, `output` | (mentor reply; no result tool) |

Delegation tools are `AgentToolType` and create Langfuse agent observations. Result tools are executor barriers for the specialist turn.

### Planning and report tools

| Tool | Agent | Shape |
|---|---|---|
| `subtask_list` | Generator | `subtasks[]` with `title` + `description`; barrier |
| `subtask_patch` | Refiner | `operations[]` of `add` / `remove` / `modify` / `reorder`; empty ops = no change; barrier |
| `report_result` | Reporter | `success`, `result`, `message` (`TaskResult`); barrier |
| `enricher_result` | Enricher | `result`, `message`; barrier |

`SubtaskOperation` rules:

- `add`: requires `title` and `description`; optional `after_id` (null/0 = start)
- `remove` / `modify` / `reorder`: require `id`
- `modify`: optional new `title`/`description`
- `reorder`: uses `after_id` for new position

### Barrier tools (primary agent)

| Tool | Fields | Effect |
|---|---|---|
| `done` | `success`, `result`, `message` | Completes the current subtask |
| `ask` | `message` | Pauses for user input |

`ask` is registered only when `ASK_USER=true` (default `false`). Both are primary-agent barriers and use span observations in Langfuse.

### Assistant flow-control tools

Always present for the assistant executor: `get_flow_status`. The remaining tools appear only when the corresponding `FlowManagerHandlers` callback is non-nil.

| Tool | Key args | Behavior |
|---|---|---|
| `get_flow_status` | `detail`, optional `task_id`, `verbose` | `summary` \| `tasks` \| `subtasks` \| `running` \| `planned` |
| `stop_flow` | `reason`, `message` | Cancels running task; flow → waiting. No-op if nothing running |
| `submit_flow_input` | `input`, `message` | Answers an `ask` checkpoint, or starts a new task when idle. Errors if a task is running |
| `patch_flow_subtasks` | `task_id`, `operations`, `message` | Delta-patch planned subtasks; can reset an active/waiting subtask to `created`. Errors if a task is running |
| `wait_flow_completion` | `timeout` (seconds), `message` | Wait for running task. Default 60s when ≤0; cap 3600s. Returns immediately if nothing running |

`get_flow_status` message limits: 10 recent agent messages by default, 50 with `verbose=true`. Operation timeout for stop/status paths is 15 seconds.

## Per-agent tool bundles

`flowToolsExecutor` builds a filtered definition/handler map per role. Optional tools appear only when backends are available.

| Executor | Core tools | Optional |
|---|---|---|
| **Primary** | `done`, `advice`, `coder`, `maintenance`, `memorist`, `pentester`, `search` | `ask` if `ASK_USER` |
| **Installer** | `maintenance_result`, `advice`, `memorist`, `search`, `terminal`, `file` | `browser`, `store_guide`/`search_guide` |
| **Coder** | `code_result`, `advice`, `maintenance`, `memorist`, `search`, `terminal`, `file` | `browser`, `search_code`/`store_code`, `graphiti_search` |
| **Pentester** | `hack_result`, `advice`, `coder`, `maintenance`, `memorist`, `search`, `terminal`, `file` | `browser`, guides, `graphiti_search`, `sploitus` |
| **Searcher** | `search_result`, `memorist` | all network engines, `browser`, `search_answer`/`store_answer` |
| **Generator** | `subtask_list`, `memorist`, `search`, `terminal`, `file` | `browser` |
| **Refiner** | `subtask_patch`, `memorist`, `search`, `terminal`, `file` | `browser` |
| **Memorist** | `memorist_result`, `terminal`, `file` | `search_in_memory`, `graphiti_search` |
| **Enricher** | `enricher_result`, `terminal`, `file` | `search_in_memory`, `graphiti_search`, `browser` |
| **Reporter** | `report_result` only | — |
| **Assistant** (`UseAgents=true`) | `terminal`, `file`, agent tools | `browser`, flow-control tools |
| **Assistant** (`UseAgents=false`) | `terminal`, `file`, direct memory/search engines | `browser`, flow-control tools |

Primary agent does **not** get direct `terminal`/`file`/network tools; it orchestrates via specialists.

## Runtime behavior

### Execution path

```text
LLM tool call
  → customExecutor.Execute
      → resolve handler by name
      → observation by ToolType (tool / agent / span / noop for vector search)
      → msglog + toolcall log
      → handler(ctx, name, args)
      → optional summarize/truncate large results
      → optional store into long-term memory
      → update msglog result
```

Unknown tool names return a soft string error (`function '…' not found`) rather than panicking. Invalid JSON args return a soft fix-it message so the model can retry.

### Result size handling

- Default result size budget before summarization: **16 KB** (`DefaultResultSizeLimit`)
- Summarization allowed for: `terminal`, `browser` (when a summarizer is configured)
- Without summarizer, oversized summarizable results are head/tail truncated at 2× the limit

### Memory auto-store allowlist

Successful results from these tools may be stored in vector memory when the store is available: `terminal`, `file`, `search`, `google`, `duckduckgo`, `tavily`, `traversaal`, `perplexity`, `searxng`, `sploitus`, `maintenance`, `coder`, `pentester`, `advice`.

### Terminal timeouts

| Setting | Value |
|---|---|
| `TERMINAL_TOOL_TIMEOUT` default | 1200 seconds |
| Hard max explicit timeout | 10800 seconds (3 hours) |
| Extra exec buffer | +5 seconds on the configured default |
| Detached commands | No stdout/stderr capture; return confirmation only |

### External / disabled functions

Flows can supply `Functions` with:

- `Disabled[]` — disable named tools per context (`agent`, `adviser`, `coder`, `searcher`, `generator`, `memorist`, `enricher`, `reporter`, `assistant`)
- `Function[]` — external HTTP tools with URL, optional timeout, context scopes, and JSON schema

## Failure modes

| Condition | Behavior |
|---|---|
| Tool not in executor map | Soft string: function not found |
| Network engine not configured | Tool omitted from definitions |
| Graphiti disabled | Soft informational string on call |
| pgvector/embedder unavailable | Memory tools omitted |
| Terminal/file with no container | Handler error / prepare failure |
| `submit_flow_input` / `patch_flow_subtasks` while task running | Error: cancel first |
| `wait_flow_completion` timeout | Soft string suggesting `get_flow_status` detail `running` |
| `stop_flow` with no running task | Informational no-op |
| Tool handler error | Soft-swallowed for some tools (e.g. terminal); toolcall log marked failed when dispatch fails hard |

## Related pages

<CardGroup>
  <Card title="Tools and sandbox execution" href="/tools-and-sandbox">
    Tool categories, Docker isolation, timeouts, and default images.
  </Card>
  <Card title="Agents and supervision" href="/agents-and-supervision">
    Specialist agents, monitor, planning step, and tool-call hard limits.
  </Card>
  <Card title="Memory and knowledge" href="/memory-and-knowledge">
    pgvector tools, embeddings, chain summarizer, flow files under /work.
  </Card>
  <Card title="Search engines" href="/search-engines">
    Enable DuckDuckGo, Google, Tavily, Traversaal, Perplexity, Sploitus, Searxng.
  </Card>
  <Card title="Knowledge graph" href="/knowledge-graph">
    Graphiti enablement and graphiti_search behavior.
  </Card>
  <Card title="Flows, tasks, and subtasks" href="/flows-tasks-subtasks">
    Lifecycle, Generator/Refiner plans, and putUserInput / stopFlow boundaries.
  </Card>
</CardGroup>

---

## 21. Prompts and templates

> PromptType enum, embedded .tmpl templates, validation via validatePrompt, user prompt CRUD, and how agent templates bind to execution context variables.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/21-prompts-and-templates.md
- Generated: 2026-07-10T07:09:29.381Z

### Source Files

- `backend/pkg/templates/templates.go`
- `backend/pkg/templates/prompts`
- `backend/pkg/graph/schema.graphqls`
- `backend/pkg/server/services/prompts.go`
- `frontend/src/pages/settings/settings-prompts.tsx`
- `backend/docs/prompt_engineering_pentagi.md`
- `backend/pkg/controller/prompter.go`

---
title: "Prompts and templates"
description: "PromptType enum, embedded .tmpl templates, validation via validatePrompt, user prompt CRUD, and how agent templates bind to execution context variables."
---

PentAGI drives agents and system helpers through Go `text/template` files under `backend/pkg/templates/prompts/*.tmpl`, selected by a shared `PromptType` string. Embedded defaults ship in the binary; per-user overrides live in the `prompts` table and merge at flow/assistant start. GraphQL is the primary management surface (Settings UI); REST exposes a parallel CRUD path under `/api/v1/prompts`.

## Architecture

```text
  Settings UI / GraphQL mutations          Flow or Assistant start
  ───────────────────────────────          ───────────────────────
  validatePrompt(type, template)           newUserPrompter(userID)
           │                                        │
           ▼                                        ▼
  validator.ValidatePrompt                 LoadDefaultPromptsMap()
  (syntax, allowed vars, dry-run)          + DB GetUserPrompts overlay
           │                                        │
           ▼                                        ▼
  CreateUserPrompt / UpdateUserPrompt      templates.NewFlowPrompter
                                                    │
  Agent / helper runtime                            ▼
  ─────────────────────                    prompter.RenderTemplate(type, params)
  handlers + helpers build map[string]any  → Go text/template Execute
  keys ⊆ PromptVariables[type]
```

| Layer | Role |
| --- | --- |
| `templates.PromptType` | Canonical type strings (`primary_agent`, `question_pentester`, …) |
| `//go:embed prompts/*.tmpl` | Compiled-in default bodies |
| `PromptVariables` | Whitelist of top-level `{{.Var}}` names per type |
| `Prompter` | `GetTemplate` / `RenderTemplate` / `DumpTemplates` |
| `defaultPrompter` | Reads embed FS only |
| `flowPrompter` | Map of defaults + user overrides for a session |
| `validator.ValidatePrompt` | Pre-save checks for custom bodies |
| `prompts` table | One row per `(user_id, type)` customization |

Graphiti uses a separate embed (`graphiti/*.tmpl`) via `ReadGraphitiTemplate`; those files are not user-editable `PromptType` entries.

## PromptType catalog

Types are declared in Go (`templates.PromptType`), GraphQL (`enum PromptType`), Postgres (`PROMPT_TYPE`), and as `prompts/<type>.tmpl` filenames. Current set (39 templates):

| Category | Types |
| --- | --- |
| System-only agents | `primary_agent`, `assistant`, `summarizer` |
| Specialist system + human | `pentester` / `question_pentester`, `coder` / `question_coder`, `installer` / `question_installer`, `searcher` / `question_searcher`, `memorist` / `question_memorist`, `adviser` / `question_adviser`, `generator` / `subtasks_generator`, `refiner` / `subtasks_refiner`, `reporter` / `task_reporter`, `reflector` / `question_reflector`, `enricher` / `question_enricher`, `toolcall_fixer` / `input_toolcall_fixer` |
| Tools / descriptors | `flow_descriptor`, `task_descriptor`, `execution_logs`, `full_execution_context`, `short_execution_context`, `image_chooser`, `language_chooser` |
| Tool-call ID probes | `tool_call_id_collector`, `tool_call_id_detector` |
| Supervision | `question_execution_monitor`, `question_task_planner`, `task_assignment_wrapper` |

`GetDefaultPrompts()` groups these into `AgentsPrompts` and `ToolsPrompts` for the Settings UI and GraphQL `settingsPrompts` query.

<Note>
REST `models.PromptType.Valid()` currently accepts the historical set through `tool_call_id_detector` but not the three supervision types. Prefer GraphQL for editing `question_execution_monitor`, `question_task_planner`, and `task_assignment_wrapper`.
</Note>

## Embedded templates and rendering

Templates use standard Go `text/template` syntax (`{{.Var}}`, `{{if}}`, `{{range}}`, etc.). Bodies are loaded from the embed FS:

```go
//go:embed prompts/*.tmpl
var promptTemplates embed.FS
```

| API | Behavior |
| --- | --- |
| `NewDefaultPrompter()` | Always reads embed FS by `prompts/<type>.tmpl` |
| `LoadDefaultPromptsMap()` | Fresh map of all defaults (safe to mutate for overlay) |
| `NewFlowPrompter(map)` | Session map after user overlay |
| `RenderPrompt(name, body, params)` | `template.Parse` + `Execute` into a buffer |
| `GetTemplate` miss | `ErrTemplateNotFound` |

Empty user rows are skipped during overlay so a blank DB body cannot replace a default and surface as a missing template mid-agent.

## Allowed variables (`PromptVariables`)

Each `PromptType` has an authorized top-level variable list. Custom templates may use **only** those names. Common system-agent variables:

| Variable | Typical content |
| --- | --- |
| `*ToolName` fields | Concrete registry names (`terminal`, `search`, `pentester`, result tools, barrier tools) |
| `ExecutionContext` | Rendered short/full task–subtask narrative |
| `DockerImage`, `Cwd`, `ContainerPorts` | Sandbox environment |
| `Lang`, `CurrentTime` | Engagement language and clock |
| `UserFiles` | Listing of `/work` uploads and resources |
| `SummarizationToolName`, `SummarizedContentPrefix` | Summarizer protocol markers |
| `GraphitiEnabled`, `GraphitiSearchToolName` | Optional knowledge-graph tools |
| `ToolPlaceholder` | Trailing tool-schema injection marker |
| `AskUserEnabled` / flow-manager tool names | Feature-gated assistant/orchestrator tools |

Human / question templates usually take a small set (`Question`, `Task`, `Subtasks`, `Code`, `Output`, …). Context builders (`full_execution_context`, `short_execution_context`) take `Task`, `Tasks`, `Subtask`, `CompletedSubtasks`, `PlannedSubtasks` as real `database.Task` / subtask structs (field access like `{{.Task.Input}}`).

Missing a variable from `PromptVariables` fails validation as `unauthorized_variable`. Using a variable the runtime never fills is allowed at save time only if it is on the whitelist; dry-run uses dummy data that includes the full mock map.

## Validation (`validatePrompt`)

`validator.ValidatePrompt(promptType, prompt)` runs before GraphQL create/update:

1. **Empty** — reject blank/whitespace templates (`empty_template`).
2. **Parse** — AST parse with common template funcs registered; syntax failures → `syntax_error`.
3. **Variable whitelist** — extract top-level fields; any name not in `PromptVariables[type]` → `unauthorized_variable`.
4. **Dry-run render** — `RenderPrompt` with `CreateDummyTemplateData()` (tool name constants, sample tasks, Docker fields, barrier tools). Failure → `rendering_failed` (or type-mismatch messaging).

GraphQL maps those to `PromptValidationErrorType`:

| Enum value | Meaning |
| --- | --- |
| `syntax_error` | Unclosed `}}`, bad actions, etc. |
| `unauthorized_variable` | Variable not declared for this type |
| `rendering_failed` | Execute failed against mock data |
| `empty_template` | No body |
| `variable_type_mismatch` | Reserved / mapped from type mismatch |
| `unknown_type` | Fallback / unknown prompt type |

Permissions: `settings.prompts.edit` for `validatePrompt`, `createPrompt`, `updatePrompt`, `deletePrompt`; `settings.prompts.view` for `settingsPrompts`.

<Warning>
REST `PUT /api/v1/prompts/{promptType}` does **not** call `ValidatePrompt`. It only checks type membership and non-empty body via model validators. Prefer GraphQL (or validate first) when editing agent templates.
</Warning>

## User prompt CRUD

### GraphQL (Settings UI)

| Operation | Args | Effect |
| --- | --- | --- |
| `settingsPrompts` | — | `default` tree (agents + tools with `type`, `template`, `variables`) + `userDefined[]` |
| `validatePrompt` | `type`, `template` | Dry validation only |
| `createPrompt` | `type`, `template` | Insert after validation |
| `updatePrompt` | `promptId`, `template` | Update owned row after validation |
| `deletePrompt` | `promptId` | Remove customization (falls back to default) |

`UserPrompt` fields: `id`, `type`, `template`, `createdAt`, `updatedAt`.

UI routes:

- `/settings/prompts` — agent and tool tables, Custom/Default badges, reset via delete
- `/settings/prompts/:promptId` — system/human tabs, variable chips, validate, save (create or update), diff vs default, reset

### REST (`/api/v1/prompts`)

| Method | Path | Privilege |
| --- | --- | --- |
| `GET` | `/prompts/` | `settings.prompts.view` |
| `GET` | `/prompts/{promptType}` | `settings.prompts.view` |
| `PUT` | `/prompts/{promptType}` | `settings.prompts.edit` (upsert body `{ "prompt": "..." }`) |
| `POST` | `/prompts/{promptType}/default` | `settings.prompts.edit` (write embed default into DB) |
| `DELETE` | `/prompts/{promptType}` | `settings.prompts.edit` |

Scope is always the authenticated `user_id`. List supports table query filters (`type`, `prompt`, timestamps) and optional group-by.

### Overlay at runtime

On flow and assistant worker start:

```go
prompter, err := newUserPrompter(ctx, db, userID)
// LoadDefaultPromptsMap + GetUserPrompts → NewFlowPrompter
```

Database load failure aborts session creation (no silent fallback to defaults-only). Overrides apply immediately to **new** flows/assistants; in-flight workers keep the prompter they were given at start.

## How templates bind to execution context

Providers hold a `Prompter` and call `RenderTemplate` with maps whose keys match `PromptVariables`. Typical pattern in agent handlers:

1. Build **system** params (tool names, Docker, `ExecutionContext`, language, flags).
2. Build **human/question** params (`Question`, task fields, code/output, …).
3. Render both templates.
4. Pass rendered strings into the LLM message chain for that agent type.

Execution context itself is produced by templates:

| Template | Used for |
| --- | --- |
| `full_execution_context` | Rich task/subtask state for summarization-style consumers |
| `short_execution_context` | Compact narrative injected as `ExecutionContext` into specialists |
| `execution_logs` | Format message-log history for refiner/reporter human prompts |

Example specialist binding (conceptual):

```go
system, _ := prompter.RenderTemplate(templates.PromptTypePentester, map[string]any{
  "HackResultToolName": tools.HackResultToolName,
  "TerminalToolName":   tools.TerminalToolName,
  // ... other PromptVariables for pentester ...
  "ExecutionContext":   shortContext,
  "Lang":               language,
  "DockerImage":        image,
  "Cwd":                docker.WorkFolderPathInContainer,
  "UserFiles":          userFilesListing,
})
human, _ := prompter.RenderTemplate(templates.PromptTypeQuestionPentester, map[string]any{
  "Question": question,
})
```

Tool-name variables stay provider-neutral: they are constants from the tools registry, not model-vendor strings. Feature flags (`GraphitiEnabled`, `AskUserEnabled`, `FlowManagerEnabled`) gate optional sections in templates via `{{if}}`.

## Template authoring conventions

In-repo guidance (`backend/docs/prompt_engineering_pentagi.md`) and the embedded agents share a structure:

- Role and authorization framing first
- Semantic XML sections (`<memory_protocol>`, `<container_constraints>`, `<team_specialists>`, …)
- Dual language policy: engagement log in `{{.Lang}}`, technical tool payloads in English
- Mandatory structured tool calls; result tools complete the subtask
- Summarization protocol: treat summaries as history, never invent tool output as plain text
- Use only whitelisted `{{.Var}}` names for the target `PromptType`

```markdown
# [AGENT ROLE]

## OPERATIONAL ENVIRONMENT
<container_constraints>
Image: {{.DockerImage}}
Cwd: {{.Cwd}}
</container_constraints>

## EXECUTION CONTEXT
{{.ExecutionContext}}

{{.ToolPlaceholder}}
```

Flow-level engagement text (what the user types when creating a flow) is separate from these agent templates; see sample engagement prompts under `examples/prompts/`.

## Permissions and storage

| Privilege | Capability |
| --- | --- |
| `settings.prompts.view` | Read defaults + user-defined list |
| `settings.prompts.edit` | Validate, create, update, delete, REST reset |
| `settings.prompts.admin` | Role seed privilege (admin bundle) |

Table: `prompts` (`id`, `type` as `PROMPT_TYPE`, `user_id`, `prompt`, timestamps). Enum migrations track template additions (tool-call ID types, then supervision types).

## Troubleshooting

| Symptom | Check |
| --- | --- |
| `unauthorized_variable: […]` | Variable not in `PromptVariables` for that type; remove or fix spelling |
| `syntax_error` / unclosed braces | Balance `{{` / `}}`; avoid unknown funcs |
| `rendering_failed` | Wrong field path on a struct (e.g. `.Task.Foo`); align with mock/`database` shapes |
| Create fails GraphQL, works REST | REST skips template validation — still fix variables before flows |
| Custom prompt not used | Confirm save succeeded; start a **new** flow/assistant; ensure non-empty body |
| Flow create fails “failed to load user prompts” | DB error on `GetUserPrompts` — fix DB rather than expecting default-only fallback |
| REST rejects supervision type | Use GraphQL for types missing from REST `Valid()` switch |
| Graphiti sections missing | `GraphitiEnabled` is false when the Graphiti client is disabled |

## Related pages

<CardGroup cols={2}>
  <Card title="Agents and supervision" href="/agents-and-supervision">
    Specialist agents, execution monitor, planning, and tool-call limits that consume these templates.
  </Card>
  <Card title="Flows, tasks, and subtasks" href="/flows-tasks-subtasks">
    Where execution context, generator/refiner human prompts, and assistant mode attach.
  </Card>
  <Card title="Tools reference" href="/tools-reference">
    Registry names injected as `*ToolName` template variables.
  </Card>
  <Card title="GraphQL API" href="/graphql-api">
    `settingsPrompts`, `validatePrompt`, and prompt mutations.
  </Card>
  <Card title="REST API" href="/rest-api">
    `/api/v1/prompts` list, get, put, reset, delete.
  </Card>
  <Card title="Sample pentest prompts" href="/sample-pentest-prompts">
    Flow input examples (engagement text), not agent `.tmpl` bodies.
  </Card>
  <Card title="Memory and knowledge" href="/memory-and-knowledge">
    Memory tools and summarizer budgets referenced from agent prompts.
  </Card>
  <Card title="Provider configuration schema" href="/provider-config-schema">
    Per-agent model settings paired with prompt types.
  </Card>
</CardGroup>

---

## 22. Example provider configs

> Copy-paste YAML from examples/configs for vLLM, Ollama, OpenRouter, DeepInfra, Azure, and cloud-compatible endpoints mapped to agent config keys.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/22-example-provider-configs.md
- Generated: 2026-07-10T07:09:49.187Z

### Source Files

- `examples/configs/vllm-qwen3.5-27b-fp8.provider.yml`
- `examples/configs/custom-openai.provider.yml`
- `examples/configs/openrouter.provider.yml`
- `examples/configs/deepinfra.provider.yml`
- `examples/configs/azure-openai.provider.yml`
- `examples/configs/ollama-qwen332b-fp16-tc.provider.yml`
- `examples/tests/openai-report.md`

---
title: "Example provider configs"
description: "Copy-paste YAML from examples/configs for vLLM, Ollama, OpenRouter, DeepInfra, Azure, and cloud-compatible endpoints mapped to agent config keys."
---

`examples/configs/*.provider.yml` files are full `ProviderConfig` YAML maps: each top-level key is an agent slot (`simple`, `primary_agent`, `coder`, …), and each value is an `AgentConfig` (`model`, sampling, `reasoning`, `price`, `extra_body`). At runtime, `LoadConfig` / `LoadConfigData` parse `.yml`/`.yaml` or `.json` into `backend/pkg/providers/pconfig.ProviderConfig`. Connection details stay in env (`LLM_SERVER_*` or `OLLAMA_SERVER_*`); the YAML only assigns models and call options per agent.

Docker images bake the same files into `/opt/pentagi/conf/`. Compose can also bind-mount host files via `PENTAGI_LLM_SERVER_CONFIG_PATH` → `/opt/pentagi/conf/custom.provider.yml` and `PENTAGI_OLLAMA_SERVER_CONFIG_PATH` → `/opt/pentagi/conf/ollama.provider.yml`.

## Catalog

| File | Endpoint style | Typical env path | Notes |
|------|----------------|------------------|-------|
| `vllm-qwen3.5-27b-fp8.provider.yml` | OpenAI-compatible vLLM | `LLM_SERVER_*` | Thinking on primary roles; `enable_thinking: false` on utility agents |
| `vllm-qwen3.5-27b-fp8-no-think.provider.yml` | vLLM | `LLM_SERVER_*` | Thinking disabled on all agents |
| `vllm-qwen3.6-27b-fp8*.provider.yml` | vLLM | `LLM_SERVER_*` | Same pattern for Qwen3.6-27B-FP8 |
| `vllm-qwen3.6-35b-a3b-fp8*.provider.yml` | vLLM | `LLM_SERVER_*` | MoE 35B-A3B FP8 |
| `vllm-qwen332b-fp16.provider.yml` | vLLM | `LLM_SERVER_*` | Single-model `Qwen/Qwen3-32B` |
| `ollama-qwen332b-fp16-tc.provider.yml` | Ollama local | `OLLAMA_SERVER_*` | Minimal single-model map (`qwen3:32b-fp16-tc`) |
| `ollama-qwq32b-fp16-tc.provider.yml` | Ollama local | `OLLAMA_SERVER_*` | Single-model `qwq:32b-fp16-tc` |
| `ollama-llama318b.provider.yml` | Ollama local | `OLLAMA_SERVER_*` | `llama3.1:8b` |
| `ollama-llama318b-instruct.provider.yml` | Ollama local | `OLLAMA_SERVER_*` | `llama3.1:8b-instruct-q8_0` |
| `ollama-cloud.provider.yml` | Ollama Cloud | `OLLAMA_SERVER_*` | Multi-model paid-tier assignments |
| `openrouter.provider.yml` | Aggregator | `LLM_SERVER_*` | Mixed vendors via OpenRouter model IDs |
| `deepinfra.provider.yml` | Aggregator | `LLM_SERVER_*` | Mixed DeepInfra model IDs + `price` |
| `custom-openai.provider.yml` | OpenAI API | `LLM_SERVER_*` | Unverified-org friendly (`o3-mini` vs o1/o3/o4-mini) |
| `azure-openai.provider.yml` | Azure OpenAI-compatible | `LLM_SERVER_*` | Deployment names as `model`; higher `max_tokens` |
| `deepseek.provider.yml` | DeepSeek / custom | `LLM_SERVER_*` or native DeepSeek | `extra_body.thinking` + `reasoning` |
| `moonshot.provider.yml` | Moonshot / custom | `LLM_SERVER_*` | Often needs `LLM_SERVER_PRESERVE_REASONING=true` |
| `novita.provider.yml` | Novita aggregator | `LLM_SERVER_*` | Prefixed model IDs |

All of the above (except host-only edits) ship under `/opt/pentagi/conf/` in the image.

## Agent keys

Every example file defines the same 13 agent slots. Keys must match `ProviderConfig` YAML tags exactly:

| Key | Role |
|-----|------|
| `simple` | Lightweight calls |
| `simple_json` | Structured JSON (`json: true`) |
| `primary_agent` | Flow orchestration |
| `assistant` | Assistant mode |
| `generator` | Subtask plan generation |
| `refiner` | Plan refinement |
| `adviser` | Advisory / analysis |
| `reflector` | Reflection loops |
| `searcher` | Search / gather |
| `enricher` | Context enrichment |
| `coder` | Code generation |
| `installer` | Tool/setup work |
| `pentester` | Exploitation / pentest steps |

Missing keys fall back to provider defaults when the runtime builds options; example files intentionally fill all slots.

## Per-agent fields

Common `AgentConfig` fields used in examples:

| Field | Type | Purpose |
|-------|------|---------|
| `model` | string | Model or deployment id sent to the API |
| `temperature`, `top_p`, `top_k`, `min_p` | number | Sampling |
| `presence_penalty`, `repetition_penalty`, `frequency_penalty` | number | Penalties (vLLM/Qwen samples use these heavily) |
| `n` | int | Completions per call (examples use `1`) |
| `max_tokens` | int | Output budget |
| `json` | bool | Force JSON mode (`simple_json`) |
| `reasoning.effort` | string | e.g. `low` / `medium` / `high` |
| `reasoning.max_tokens` | int | Reasoning token budget (some aggregators) |
| `price.input` / `price.output` | float | USD per **1M** tokens (cost accounting) |
| `price.cache_read` / `price.cache_write` | float | Optional cache rates |
| `extra_body` | object | Provider-specific request merge (e.g. `chat_template_kwargs`, `thinking`) |

Full field list and UI test surfaces: [Provider configuration schema](/provider-config-schema).

## Wire a config into the stack

<Steps>
  <Step title="Pick the YAML">
    Copy from the repo (`examples/configs/…`) or use the baked path `/opt/pentagi/conf/<name>.provider.yml` inside the container.
  </Step>
  <Step title="Point env at the file">
    Custom OpenAI-compatible backends:

    ```bash
    LLM_SERVER_URL=https://…/v1
    LLM_SERVER_KEY=…
    LLM_SERVER_MODEL=                 # leave empty when models come from YAML
    LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/openrouter.provider.yml
    LLM_SERVER_PROVIDER=              # optional LiteLLM prefix, e.g. openrouter
    LLM_SERVER_LEGACY_REASONING=false
    LLM_SERVER_PRESERVE_REASONING=false
    ```

    Ollama:

    ```bash
    OLLAMA_SERVER_URL=http://ollama-server:11434   # or https://ollama.com
    OLLAMA_SERVER_API_KEY=                         # required for Ollama Cloud
    OLLAMA_SERVER_MODEL=
    OLLAMA_SERVER_CONFIG_PATH=/opt/pentagi/conf/ollama-qwen332b-fp16-tc.provider.yml
    ```
  </Step>
  <Step title="Mount host overrides (optional)">
    ```bash
    PENTAGI_LLM_SERVER_CONFIG_PATH=./my-custom.provider.yml
    PENTAGI_OLLAMA_SERVER_CONFIG_PATH=./my-ollama.provider.yml
    ```

    Compose mounts these to `/opt/pentagi/conf/custom.provider.yml` and `/opt/pentagi/conf/ollama.provider.yml`. Set `LLM_SERVER_CONFIG_PATH` / `OLLAMA_SERVER_CONFIG_PATH` to those in-container paths when using the mounts.
  </Step>
  <Step title="Verify with ctester">
    ```bash
    docker exec -it pentagi /opt/pentagi/bin/ctester \
      -config /opt/pentagi/conf/openrouter.provider.yml

    # From a backend checkout
    go run ./cmd/ctester/*.go \
      -config ../examples/configs/deepinfra.provider.yml \
      -report ../test-report.md
    ```

    Sample run reports live under `examples/tests/` (e.g. `openai-report.md`, `openrouter-report.md`).
  </Step>
</Steps>

## OpenAI-compatible custom endpoints

Use `LLM_SERVER_*` for any OpenAI chat-completions compatible API (OpenAI, Azure OpenAI-compatible gateways, vLLM, OpenRouter, DeepInfra, Novita, LiteLLM, etc.). Leave `LLM_SERVER_MODEL` empty when the YAML sets every agent `model`.

### OpenRouter

Endpoint: `https://openrouter.ai/api/v1`. Models use vendor-prefixed IDs.

| Agent | Model in `openrouter.provider.yml` |
|-------|--------------------------------------|
| `simple` / `simple_json` / `reflector` / `enricher` | `openai/gpt-4.1-mini` |
| `primary_agent` / `assistant` | `openai/gpt-5` |
| `generator` / `coder` | `anthropic/claude-sonnet-4.5` |
| `refiner` / `adviser` | `google/gemini-2.5-pro` |
| `searcher` | `x-ai/grok-3-mini` |
| `installer` | `google/gemini-2.5-flash` |
| `pentester` | `moonshotai/kimi-k2-0905` |

```bash
LLM_SERVER_URL=https://openrouter.ai/api/v1
LLM_SERVER_KEY=sk-or-…
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/openrouter.provider.yml
```

```yaml
# examples/configs/openrouter.provider.yml (excerpt)
primary_agent:
  model: "openai/gpt-5"
  n: 1
  max_tokens: 6000
  reasoning:
    effort: medium
  price:
    input: 1.25
    output: 10.0

generator:
  model: "anthropic/claude-sonnet-4.5"
  n: 1
  max_tokens: 12000
  reasoning:
    max_tokens: 4000
  price:
    input: 3.0
    output: 15.0
```

### DeepInfra

Endpoint: `https://api.deepinfra.com/v1/openai`. Same agent map pattern; different model ids and prices.

| Agent | Model in `deepinfra.provider.yml` |
|-------|-------------------------------------|
| `simple` / `simple_json` / `reflector` | `Qwen/Qwen3-Next-80B-A3B-Instruct` |
| `primary_agent` / `assistant` / `pentester` | `moonshotai/Kimi-K2-Instruct-0905` |
| `generator` / `adviser` | `google/gemini-2.5-pro` |
| `refiner` | `deepseek-ai/DeepSeek-R1-0528-Turbo` |
| `searcher` / `enricher` | `Qwen/Qwen3-32B` |
| `coder` | `anthropic/claude-4-sonnet` |
| `installer` | `google/gemini-2.5-flash` |

```bash
LLM_SERVER_URL=https://api.deepinfra.com/v1/openai
LLM_SERVER_KEY=…
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/deepinfra.provider.yml
```

### Custom OpenAI (unverified org)

`custom-openai.provider.yml` maps heavy agents to `o3-mini` and lighter work to `gpt-4.1-mini` / `gpt-4.1`, with `reasoning.effort` and `price` blocks.

```bash
LLM_SERVER_URL=https://api.openai.com/v1
LLM_SERVER_KEY=sk-…
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/custom-openai.provider.yml
LLM_SERVER_LEGACY_REASONING=true
```

```yaml
# examples/configs/custom-openai.provider.yml (excerpt)
simple:
  model: "gpt-4.1-mini"
  temperature: 0.5
  top_p: 0.5
  n: 1
  max_tokens: 3000
  price:
    input: 0.4
    output: 1.6

primary_agent:
  model: "o3-mini"
  n: 1
  max_tokens: 4000
  reasoning:
    effort: low
  price:
    input: 1.1
    output: 4.4

coder:
  model: "gpt-4.1"
  temperature: 0.2
  top_p: 0.1
  n: 1
  max_tokens: 6000
  price:
    input: 2.0
    output: 8.0
```

### Azure OpenAI–compatible

`azure-openai.provider.yml` uses deployment-style names (`gpt-4.1-mini`, `o4-mini`, `gpt-4.1`) and larger `max_tokens` than the custom OpenAI sample. Point `LLM_SERVER_URL` at your Azure (or Azure-compatible) base URL that speaks chat completions.

```bash
LLM_SERVER_URL=https://YOUR_RESOURCE.openai.azure.com/openai/v1
LLM_SERVER_KEY=…
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/azure-openai.provider.yml
LLM_SERVER_LEGACY_REASONING=true
```

```yaml
# examples/configs/azure-openai.provider.yml (excerpt)
generator:
  model: "o4-mini"
  n: 1
  max_tokens: 32768
  price:
    input: 1.1
    output: 4.4

pentester:
  model: "o4-mini"
  n: 1
  max_tokens: 8192
  price:
    input: 1.1
    output: 4.4
```

### LiteLLM proxy

Keep the same YAML; set `LLM_SERVER_PROVIDER` so model names are prefixed (e.g. `openrouter`, `deepinfra`, `moonshot`, `novita`). Empty `LLM_SERVER_PROVIDER` means direct API ids as written in the file.

## vLLM (local / air-gapped)

vLLM examples assume an OpenAI-compatible server (typically `…/v1`) and a single model id for every agent. Sampling follows Qwen guidance: higher temp/`top_p` for reasoning agents; lower temp and `presence_penalty: 0.0` for `coder` / `installer` / `pentester`.

### Thinking vs non-thinking

| File | Behavior |
|------|----------|
| `vllm-qwen3.5-27b-fp8.provider.yml` | Primary roles think by default; `simple`, `simple_json`, `reflector`, `searcher`, `enricher` set `extra_body.chat_template_kwargs.enable_thinking: false` |
| `vllm-qwen3.5-27b-fp8-no-think.provider.yml` | All agents set `enable_thinking: false` |

Same dual-file pattern exists for Qwen3.6-27B-FP8 and Qwen3.6-35B-A3B-FP8.

```bash
LLM_SERVER_URL=http://host.docker.internal:8000/v1
LLM_SERVER_KEY=not-needed
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/vllm-qwen3.5-27b-fp8.provider.yml
```

```yaml
# examples/configs/vllm-qwen3.5-27b-fp8.provider.yml (excerpt)
simple:
  model: "Qwen/Qwen3.5-27B-FP8"
  temperature: 0.7
  top_k: 20
  top_p: 0.8
  min_p: 0.0
  presence_penalty: 1.5
  repetition_penalty: 1.0
  n: 1
  max_tokens: 32768
  extra_body:
    chat_template_kwargs:
      enable_thinking: false

primary_agent:
  model: "Qwen/Qwen3.5-27B-FP8"
  temperature: 1.0
  top_k: 20
  top_p: 0.95
  min_p: 0.0
  presence_penalty: 1.5
  repetition_penalty: 1.0
  n: 1
  max_tokens: 32768

coder:
  model: "Qwen/Qwen3.5-27B-FP8"
  temperature: 0.6
  top_k: 20
  top_p: 0.95
  min_p: 0.0
  presence_penalty: 0.0
  repetition_penalty: 1.0
  n: 1
  max_tokens: 32768
```

Hardware matrix, vLLM serve flags, and supervision knobs: [Deploy with vLLM and Qwen](/vllm-qwen-deployment).

## Ollama

Ollama uses `OLLAMA_SERVER_*` (not `LLM_SERVER_*`). Config path: `OLLAMA_SERVER_CONFIG_PATH`.

### Local single-model maps

`ollama-qwen332b-fp16-tc.provider.yml` is the minimal template—same model and `max_tokens` on every agent:

```yaml
# examples/configs/ollama-qwen332b-fp16-tc.provider.yml (complete)
simple:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

simple_json:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

primary_agent:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

assistant:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

generator:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

refiner:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

adviser:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

reflector:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

searcher:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

enricher:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

coder:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

installer:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000

pentester:
  model: "qwen3:32b-fp16-tc"
  n: 1
  max_tokens: 40000
```

```bash
OLLAMA_SERVER_URL=http://ollama-server:11434
OLLAMA_SERVER_CONFIG_PATH=/opt/pentagi/conf/ollama-qwen332b-fp16-tc.provider.yml
OLLAMA_SERVER_PULL_MODELS_ENABLED=false
OLLAMA_SERVER_LOAD_MODELS_ENABLED=false
```

### Ollama Cloud multi-model

`ollama-cloud.provider.yml` assigns different `:cloud` models per agent (e.g. `qwen3-coder-next:cloud` for `primary_agent` / `coder` / `pentester`, `qwen3.5:397b-cloud` for `searcher`).

```bash
OLLAMA_SERVER_URL=https://ollama.com
OLLAMA_SERVER_API_KEY=…
OLLAMA_SERVER_CONFIG_PATH=/opt/pentagi/conf/ollama-cloud.provider.yml
```

## Other cloud-compatible samples

| File | When to use |
|------|-------------|
| `deepseek.provider.yml` | DeepSeek models with `extra_body.thinking.type` enabled/disabled per agent and `reasoning.effort` |
| `moonshot.provider.yml` | Kimi / Moonshot; pair with `LLM_SERVER_PRESERVE_REASONING=true` if the API requires reasoning content on tool turns |
| `novita.provider.yml` | Novita OpenAI-compatible gateway (`https://api.novita.ai/openai`) |

These still load through the custom config path when using `LLM_SERVER_CONFIG_PATH`, or through the matching native provider when you set that provider’s own env keys.

## Copy-paste starter (host file)

Minimal bootstrap for a custom host-mounted OpenAI-compatible profile:

```bash
curl -o example.custom.provider.yml \
  https://raw.githubusercontent.com/vxcontrol/pentagi/master/examples/configs/custom-openai.provider.yml
curl -o example.ollama.provider.yml \
  https://raw.githubusercontent.com/vxcontrol/pentagi/master/examples/configs/ollama-llama318b.provider.yml
```

Compose defaults mount those filenames when `PENTAGI_*_CONFIG_PATH` is unset.

## Failure modes

| Symptom | Check |
|---------|--------|
| Config ignored | Path empty or unreadable; extension not `.yml`/`.yaml`/`.json` |
| Wrong models | `LLM_SERVER_MODEL` overriding intent; set empty and rely on YAML |
| LiteLLM 404 on model | Set `LLM_SERVER_PROVIDER` to the proxy prefix |
| OpenAI reasoning errors | `LLM_SERVER_LEGACY_REASONING=true` for OpenAI-style reasoning |
| Moonshot tool-call errors about missing reasoning | `LLM_SERVER_PRESERVE_REASONING=true` |
| Ollama Cloud 401 | Missing `OLLAMA_SERVER_API_KEY` |
| Mount not visible in container | Use in-container path under `/opt/pentagi/conf/…` in `*_CONFIG_PATH` |
| Agent tests fail | Re-run `ctester -config …`; compare with `examples/tests/*-report.md` |

## Related pages

<CardGroup cols={2}>
  <Card title="Local and custom providers" href="/local-and-custom-providers">
    LLM_SERVER_* and OLLAMA_SERVER_* wiring, legacy/preserve reasoning flags, aggregators.
  </Card>
  <Card title="Provider configuration schema" href="/provider-config-schema">
    Full AgentsConfig / AgentConfig fields, baselines, and UI testAgent/testProvider.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    Built-in provider keys and UI profiles for OpenAI, Anthropic, Gemini, and others.
  </Card>
  <Card title="Deploy with vLLM and Qwen" href="/vllm-qwen-deployment">
    Hardware matrix, vLLM serve flags, thinking vs non-thinking YAML, supervision for smaller models.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Authoritative Config struct and .env.example for all provider and path keys.
  </Card>
  <Card title="Development and testing" href="/development-and-testing">
    ctester, ftester, and contributor workflows for validating provider YAML.
  </Card>
</CardGroup>

---

## 23. Sample pentest prompts

> Ready flow inputs from examples/prompts: base web pentest checklist and scope-of-work style engagement prompts with expected report-oriented outcomes.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/23-sample-pentest-prompts.md
- Generated: 2026-07-10T07:09:58.365Z

### Source Files

- `examples/prompts/base_web_pentest.md`
- `examples/prompts/scope_of_work_pentest.md`
- `examples/reports/openai_base_web_pentest.md`
- `examples/reports/ollama_qwen3_32b_fp16_base_web_pentest.md`
- `frontend/src/pages/flows/new-flow.tsx`
- `README.md`

---
title: "Sample pentest prompts"
description: "Ready flow inputs from examples/prompts: base web pentest checklist and scope-of-work style engagement prompts with expected report-oriented outcomes."
---

`examples/prompts/` ships two ready-to-paste flow inputs: a web vulnerability checklist (`base_web_pentest.md`) and an engagement scope boundary template (`scope_of_work_pentest.md`). Both become the free-text `input` on `createFlow` (UI, GraphQL, or REST). Sample Markdown reports under `examples/reports/` show how the base checklist expands into task/subtask results and a final findings summary across different LLM providers.

:::files
examples/
├── prompts/
│   ├── base_web_pentest.md          # Web OWASP-style action plan ({{YOUR_TARGET_URL}})
│   └── scope_of_work_pentest.md     # Authorization + scope XML template
└── reports/
    ├── openai_base_web_pentest.md   # Structured Input/Result report (OpenAI run)
    ├── openai_base_web_pentest.pdf
    ├── ollama_qwen3_32b_fp16_base_web_pentest.md  # Nested task tree report (Ollama Qwen3 32B)
    └── ollama_qwen3_32b_fp16_base_web_pentest.pdf
:::

<Warning>
Only assess systems you own or are explicitly authorized to test. The scope template is guidance for agent instructions; it does not replace legal authorization, customer approval, or runtime enforcement of target allowlists.
</Warning>

## Prompt inventory

| File | Role | Placeholder(s) | Typical use |
| --- | --- | --- | --- |
| `examples/prompts/base_web_pentest.md` | Endpoint mapping + per-input vuln checks | `{{YOUR_TARGET_URL}}` | First automation flow for a single web app |
| `examples/prompts/scope_of_work_pentest.md` | Engagement boundaries + pre-action scope protocol | `{{WHO_AUTHORIZED_THE_TEST}}`, targets, rates, credentials, windows | Prefix or wrap any methodology prompt before recon |

These are **user flow inputs**, not agent system templates. System/agent templates (`.tmpl`, `PromptType`, Settings → Prompts) live on a separate surface; see [Prompts and templates](/prompts-reference).

## Base web pentest checklist

`base_web_pentest.md` opens with a single objective, then a two-phase action plan the generator can decompose into tasks and subtasks.

**Objective line**

```text
You need to find critical or valuable vulnerability in a Web Application {{YOUR_TARGET_URL}}
```

Replace `{{YOUR_TARGET_URL}}` with a concrete host (for example `http://10.10.10.10:8080` as in the sample reports).

### Phase 1 — Collect all endpoints

| Step | Agent focus |
| --- | --- |
| Application exploration | Navigate pages, exercise features, include authenticated paths |
| Identify scenarios | Map user journeys and trust boundaries |
| Document endpoints | URLs, inputs, parameters; prioritize file upload features |

### Phase 2 — Per-endpoint checks

| Check | Tools / techniques called out in the prompt |
| --- | --- |
| Path traversal | Linux assumption; read sensitive files such as `/etc/passwd`; focus download/view paths |
| CSRF | Token presence; token-less requests; POST→GET; bypass techniques |
| XSS | Front-end stack; marker `XSS_TEST_123`; reflection context; unusual tags/handlers |
| SQL injection | Login/search/URL params; `sqlmap` with cookies/tamper scripts as needed |
| Command injection | Time-based detection; `${IFS}` / backtick syntax; `commix` |
| SSRF | OOB via Interactsh-class tools; uploads, PDF, webhooks, import/export |
| XXE | XML forms; `.xml` / `.svg` / `.docx` uploads; Office document rezip path |
| Unsafe file download/upload | Executable extensions; double extension / null byte; `Content-Type` and filename params; path prediction under `/uploads/` |

The checklist is methodology-first (map → systematic checks → evidence), not a flat payload dump. That matches the repo’s pentesting prompt methodology: scope, map, prioritize, validate, report.

## Scope-of-work engagement template

`scope_of_work_pentest.md` defines an XML-shaped instruction block you paste **before or around** a methodology prompt when the engagement needs hard boundaries.

### Structure

| Block | Purpose |
| --- | --- |
| `<authorization_summary>` | Who authorized, date, engagement/ticket name |
| `<allowed_targets>` | Exact hostnames, URLs, CIDRs, approved subdomain patterns |
| `<out_of_scope_targets>` | Third parties, production systems, shared infrastructure |
| `<allowed_actions>` | Passive recon, auth testing, validation, evidence for the report |
| `<forbidden_actions>` | Off-scope testing, broad mass scans, third-party exploit, persistence/exfil beyond plan |
| `<rate_limits_and_windows>` | Request rate, approved test window, blackout periods |
| `<credentials_and_test_accounts>` | Test roles and credential handling rules |
| `<stop_conditions>` | Stop on ambiguous/out-of-scope destinations; stop before disruption |
| `<evidence_expectations>` | Intent, target, timestamp, result; artifacts under `/work`; redact secrets |
| `<scope_check_protocol>` | Eight pre-action rules: extract destinations, match allowlist, strip multi-target out-of-scope entries, validate generated scripts/exploits, record newly discovered hosts without testing them, ask when ambiguous |

### Backend validation ideas (not implemented)

The template file also documents **future** validation ideas only: normalize allow/deny targets, validate tool target flags, per-tool mass-scanner policy, block ambiguous destinations, re-inject scope after a blocked attempt. Treat these as design notes, not current runtime behavior.

### Minimal filled example

```text
You are operating inside an authorized security assessment. Stay strictly within the scope below.

<engagement_scope>
  <authorization_summary>
  Security team ticket SEC-2048, 2026-04-01, staging web app assessment
  </authorization_summary>

  <allowed_targets>
  - https://app.staging.example.com
  - 10.10.10.0/28
  </allowed_targets>

  <out_of_scope_targets>
  - *.production.example.com
  - payment-gateway.vendor.example
  </out_of_scope_targets>

  <allowed_actions>
  - Passive reconnaissance against allowed targets
  - Vulnerability validation against allowed targets only
  - Evidence collection needed for the final report
  </allowed_actions>

  <forbidden_actions>
  - Do not test any target that is not listed or clearly derived from the allowed scope.
  - Do not persist access, exfiltrate sensitive data, or modify data beyond the approved test plan.
  </forbidden_actions>

  <rate_limits_and_windows>
  - Max 10 rps; weekdays 09:00–18:00 UTC only
  </rate_limits_and_windows>

  <credentials_and_test_accounts>
  - Role: standard user; credentials via /work/uploads/creds.txt; do not store secrets in the final report
  </credentials_and_test_accounts>

  <stop_conditions>
  - Stop before interacting with a target that is not provably in scope.
  - Stop and ask for clarification when the scope cannot be determined from the current evidence.
  </stop_conditions>

  <evidence_expectations>
  - Record command intent, target, timestamp, and result summary for each meaningful action.
  - Save screenshots, request/response excerpts, and reproduction notes under /work.
  </evidence_expectations>
</engagement_scope>

You need to find critical or valuable vulnerability in a Web Application https://app.staging.example.com
...paste remainder of base_web_pentest.md checklist...
```

## Run a sample prompt as a flow

### Prerequisites

- At least one configured LLM provider (BYOK keys or local/custom endpoint)
- Stack reachable (typically `https://localhost:8443` after compose up)
- Authorized target reachable from the agent Docker sandbox network
- Optional: search engines, Graphiti, or attached resources under `/work` for richer context

### UI path

<Steps>
  <Step title="Open New flow">
    Sidebar → **Flows** → **New Flow**. Mode tabs: **Automation** (end-to-end goal) or **Assistant** (interactive; optional **Use Agents**).
  </Step>
  <Step title="Select provider and paste input">
    Choose the provider. Paste the substituted prompt into the message box. Placeholder in automation mode: `Describe what you would like PentAGI to test...`. Form requires non-empty `message` and `providerName`.
  </Step>
  <Step title="Optional: template or resources">
    Use the form’s template picker to prefill a saved **FlowTemplate** (`title` + `text`). Attach user resources so they appear under `/work/resources/` and in the `{{.UserFiles}}` task-files block.
  </Step>
  <Step title="Submit and monitor">
    Submit creates the flow and navigates to `/flows/{id}`. Watch tasks/subtasks, toolcalls, and terminal logs. When results accumulate, use **Report** to open, copy, or download Markdown/PDF.
  </Step>
</Steps>

Save repeatable text via **Templates** (`createFlowTemplate` with `title` and `text`), then reapply on New Flow. Example files under `examples/prompts/` are not auto-seeded; copy them into a template or paste once.

### API path

GraphQL mutation surface:

```graphql
mutation CreateFlow($provider: String!, $input: String!) {
  createFlow(modelProvider: $provider, input: $input) {
    id
    title
    status
    createdAt
  }
}
```

REST create payload (`POST /api/v1/flows/`):

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `input` | string | yes | Full prompt text (scope + checklist after substitution) |
| `provider` | string | yes | Provider name (for example `openai`, or a custom profile name) |
| `resource_ids` | uint64[] | no | Attached user resource IDs |
| `functions` | object | no | Optional tool function overrides |

Follow-up input on a waiting flow uses GraphQL `putUserInput` or REST `PUT /api/v1/flows/{flowID}` with `action: "input"`.

<RequestExample>
```bash
# REST create (Bearer token from Settings → API Tokens)
curl -sS -X POST "https://localhost:8443/api/v1/flows/" \
  -H "Authorization: Bearer $PENTAGI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "openai",
    "input": "You need to find critical or valuable vulnerability in a Web Application http://10.10.10.10:8080\n\nYou have to use the following action plan:\n..."
  }'
```
</RequestExample>

Provider choice is BYOK/BYOC: the same prompt text works with cloud providers, Ollama, vLLM, or OpenAI-compatible endpoints once configured. Outcomes vary with model capability and sandbox reachability, not with a hard-coded vendor path.

## Expected report-oriented outcomes

### What a successful base-web run produces

| Artifact | Where | Content pattern |
| --- | --- | --- |
| Flow title / primary task | Flow page | Goal derived from the prompt (often includes host and check themes) |
| Subtasks | Tasks tree | Mapping, then per-check or exploit follow-ons |
| Tool activity | Toolcalls / terminal | `sqlmap`, `commix`, curl, crawlers, etc., as the checklist suggests |
| Intermediate results | Task `result` | Input plan + Result narrative per step |
| Final report | Report menu (MD/PDF) | Confirmed findings, non-findings, recommendations |
| Evidence files | Flow Files / `/work` | Screenshots, request excerpts, downloaded artifacts when the agent saves them |

### Sample report shapes (same checklist, different providers)

Both sample reports use the base checklist against `http://10.10.10.10:8080`. They illustrate **format and depth variance**, not a guaranteed finding set for your target.

| Sample | Path | Structure | Outcome sketch |
| --- | --- | --- | --- |
| OpenAI run | `examples/reports/openai_base_web_pentest.md` (+ PDF) | Numbered steps with **Input** / **Result** pairs; closes with identified vs non-vulnerable features | Focus on sorting `order` parameter: boolean/error/time-based SQLi, CSRF on sort form; XSS/path traversal/command injection/SSRF/XXE/file handling reported as not found; some steps hit connectivity issues mid-run |
| Ollama Qwen3 32B FP16 | `examples/reports/ollama_qwen3_32b_fp16_base_web_pentest.md` (+ PDF) | Nested checked task tree (flow → assessment → subtasks) | Checklist mirrored in TOC, then exploit chain narrative: XXE on `/registration`, backup `db.sql.gz` credentials, privilege escalation, reverse shell — deeper exploitation path than the OpenAI sample |

Report sections typically include:

1. **Reproduction of the user action plan** (endpoint map + checks a–h)
2. **Per-subtask Input/Result or exploit write-ups**
3. **Findings summary** with impact and recommendations
4. **Negative results** (what was tested and not vulnerable) when the agent completes the full matrix

<Tip>
Compare your run’s Report export to these samples for structure. Do not expect the same CVEs or exploit chain on a different host or model; treat samples as format references and smoke-test baselines.
</Tip>

## Prompt composition patterns

### Good first inputs (minimal)

README guidance for early flows:

- Target system or URL  
- Assessment type  
- Scope limits  
- Expected deliverable (report, hypothesis validation)

```text
Assess https://target.example for common web application vulnerabilities.
Focus on authentication, file handling, and injection issues.
Stay within the provided target only and summarize confirmed findings with reproduction steps.
```

### Layered composition

| Layer | Source | Order |
| --- | --- | --- |
| 1. Scope + stop protocol | `scope_of_work_pentest.md` (filled) | First |
| 2. Methodology checklist | `base_web_pentest.md` (URL substituted) | Second |
| 3. Stack-specific constraints | Manual notes (auth cookies path, tech stack, no DoS) | Optional third |
| 4. File context | Uploads/resources at `/work/uploads/`, `/work/resources/` | Via form attach, not paste |

### Methodology checklist for custom prompts

When refining beyond the stock files:

1. Explicit scope, authorization, success criteria  
2. Map application first (roles, routes, params, uploads, integrations)  
3. Prioritize surfaces; avoid “test everything at once” without ordering  
4. Validate with reproducible evidence before deep exploit  
5. Finish with report-ready impact, prerequisites, and next steps  

Agent-system prompt engineering (roles, XML protocols, `{{.ExecutionContext}}`) is documented in `backend/docs/prompt_engineering_pentagi.md` and the prompts reference; engagement inputs should stay short, hierarchical Markdown/XML, and tool-aware without duplicating system template boilerplate.

## Constraints and failure modes

| Symptom | Likely cause | Mitigation |
| --- | --- | --- |
| Empty form submit rejected | `message` / `input` required and min length 1 after trim | Paste full substituted prompt |
| Provider errors on create | Missing/invalid provider or keys | Configure and test provider before flows |
| Host unreachable in report steps | Target not routable from sandbox | Fix Docker network/DNS; use worker-node topology if needed |
| Broad off-scope scanning | Scope not in user input; no runtime allowlist | Always prefix with filled scope template; monitor toolcalls |
| Shallow or incomplete report | Weak model, truncated run, or vague goal | Prefer stronger models for pentester/reporter; narrow objective; use Assistant to redirect |
| Findings differ from sample PDFs | Different app/model/network | Expected; use samples only for structure |
| Secrets in final report | Agent dumped credentials | Instruct redaction in scope `evidence_expectations`; review before sharing PDFs |

## Verification

After a sample-prompt run:

1. Flow status progresses and tasks/subtasks appear for map + check themes.  
2. Toolcalls include network and scanner activity against **only** allowed targets.  
3. Report export lists confirmed findings and/or explicit non-findings with remediation notes.  
4. Optional: evidence files under the flow Files tab or `/work` paths referenced in the narrative.

## Related pages

<CardGroup cols={2}>
  <Card title="Quickstart" href="/quickstart">
    First flow: provider key, compose up, create flow, verify UI health.
  </Card>
  <Card title="Flows, tasks, and subtasks" href="/flows-tasks-subtasks">
    Lifecycle, Generator/Refiner plans, putUserInput and finish boundaries.
  </Card>
  <Card title="Agents and supervision" href="/agents-and-supervision">
    Specialist agents including reporter, limits, and planning step.
  </Card>
  <Card title="Tools and sandbox execution" href="/tools-and-sandbox">
    Docker-isolated terminal/file tools used by the checklist.
  </Card>
  <Card title="Prompts and templates" href="/prompts-reference">
    System PromptType templates vs user flow inputs and CRUD.
  </Card>
  <Card title="GraphQL API" href="/graphql-api">
    createFlow, putUserInput, createFlowTemplate, subscriptions.
  </Card>
  <Card title="REST API" href="/rest-api">
    POST /api/v1/flows and patch actions for programmatic runs.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    BYOK wiring so sample prompts run on your chosen models.
  </Card>
</CardGroup>

---

## 24. Deploy with vLLM and Qwen

> Air-gapped style local inference: hardware matrix, vLLM serve flags, LLM_SERVER_* wiring, thinking vs non-thinking provider YAML, and supervision flags recommended for sub-32B models.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/24-deploy-with-vllm-and-qwen.md
- Generated: 2026-07-10T07:10:32.658Z

### Source Files

- `examples/guides/vllm-qwen35-27b-fp8.md`
- `examples/configs/vllm-qwen3.5-27b-fp8.provider.yml`
- `examples/configs/vllm-qwen3.5-27b-fp8-no-think.provider.yml`
- `examples/configs/vllm-qwen3.6-27b-fp8.provider.yml`
- `.env.example`
- `backend/pkg/providers/custom/custom.go`
- `README.md`

---
title: "Deploy with vLLM and Qwen"
description: "Air-gapped style local inference: hardware matrix, vLLM serve flags, LLM_SERVER_* wiring, thinking vs non-thinking provider YAML, and supervision flags recommended for sub-32B models."
---

PentAGI runs fully local multi-agent flows by pointing the **Custom** OpenAI-compatible provider at a vLLM OpenAI API server (`/v1`) serving Qwen dense FP8 weights. The integration path is `backend/pkg/providers/custom` plus `LLM_SERVER_*` (or a UI Custom provider), agent sampling from `examples/configs/vllm-*.provider.yml`, and optional execution monitor / planning flags for models under ~32B parameters.

```text
┌─────────────────────┐     OpenAI /v1      ┌──────────────────────────┐
│  PentAGI (Custom)   │ ──────────────────► │  vLLM serve              │
│  LLM_SERVER_*  or   │  chat/completions   │  Qwen/*-FP8              │
│  UI provider YAML   │ ◄────────────────── │  reasoning + tool parsers│
└──────────┬──────────┘                     └──────────────────────────┘
           │ agents: primary, pentester, coder, …
           │ supervision: EXECUTION_MONITOR_*, AGENT_PLANNING_STEP_*
           ▼
    Docker sandboxes / tools / pgvector memory
```

## Supported example configs

| File | Model | Thinking default |
| --- | --- | --- |
| `examples/configs/vllm-qwen3.5-27b-fp8.provider.yml` | `Qwen/Qwen3.5-27B-FP8` | On for primary/coding; off for simple/searcher/enricher/reflector |
| `examples/configs/vllm-qwen3.5-27b-fp8-no-think.provider.yml` | same | Off for all agents |
| `examples/configs/vllm-qwen3.6-27b-fp8.provider.yml` | `Qwen/Qwen3.6-27B-FP8` | Same pattern as 3.5 thinking |
| `examples/configs/vllm-qwen3.6-27b-fp8-no-think.provider.yml` | same | Off for all agents |
| `examples/configs/vllm-qwen3.6-35b-a3b-fp8.provider.yml` | `Qwen/Qwen3.6-35B-A3B-FP8` | Thinking default |
| `examples/configs/vllm-qwen3.6-35b-a3b-fp8-no-think.provider.yml` | same | Non-thinking |
| `examples/configs/vllm-qwen332b-fp16.provider.yml` | `Qwen/Qwen3-32B` | Sampling-only (no `chat_template_kwargs`) |

Primary operational guide for hardware, serve flags, and curl checks: `examples/guides/vllm-qwen35-27b-fp8.md`.

## Model and hardware matrix

**Qwen3.5-27B-FP8** (guide baseline): 27B dense, hybrid ~75% Gated DeltaNet / ~25% Gated Attention, native context **262,144** tokens (YaRN up to ~1,010,000), FP8 W8A8 block size 128. Treat as a VLM family member: the vision encoder still consumes VRAM unless skipped with `--language-model-only`.

FP8 W8A8 acceleration needs GPU **Compute Capability ≥ 8.9** (Ada Lovelace, Hopper, Blackwell). On Ampere (A100, RTX 3090, etc.) FP8 falls back to W8A16 via Marlin with lower throughput.

| Configuration | Total VRAM | Max context | FP8 mode | Notes |
| --- | --- | --- | --- | --- |
| 2× RTX 5090 (64 GB) | 64 GB | ≤131k | W8A8 | Good |
| **4× RTX 5090 (128 GB)** | **128 GB** | **262k native** | **W8A8** | **Tested ~30 GB/GPU @ util 0.75** |
| 1× H100 SXM (80 GB) | 80 GB | 262k | W8A8 | Single GPU |
| 2× H100 SXM (160 GB) | 160 GB | 262k | W8A8 | Excellent |
| 4× A100 80GB (320 GB) | 320 GB | 262k | W8A16 | Slower fallback |

### Host prerequisites

- Linux (Ubuntu 22.04+ recommended)
- CUDA 12.1+, NVIDIA drivers 535+
- Python 3.9–3.12
- Multi-GPU: NCCL **2.27.3+**; Blackwell tensor-parallel needs `NCCL_P2P_DISABLE=1`

```bash
nvidia-smi
nvcc --version
```

## Install vLLM

The guide states `qwen3_5` architecture is **not** recognized in stable vLLM at time of writing; use **nightly** until vLLM v0.17.0+ lands that support.

<Tabs>
<Tab title="uv">
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
uv pip install vllm --torch-backend=auto --extra-index-url https://wheels.vllm.ai/nightly
python -c "import vllm; print(vllm.__version__)"
```
</Tab>
<Tab title="pip">
```bash
pip install vllm --pre --extra-index-url https://wheels.vllm.ai/nightly
python -c "import vllm; print(vllm.__version__)"
```
</Tab>
<Tab title="Docker">
```bash
docker pull vllm/vllm-openai:nightly
```
</Tab>
</Tabs>

## vLLM serve flags

Recommended parameters (4× RTX 5090 tested profile):

| Flag | Value | Role |
| --- | --- | --- |
| `--model` | `Qwen/Qwen3.5-27B-FP8` | Hugging Face id (swap for 3.6 / 35B-A3B variants) |
| `--tensor-parallel-size` | `4` (or omit for 1 GPU) | One shard per GPU |
| `--max-model-len` | `262144` | Native context |
| `--max-num-batched-tokens` | `4096` | Lower inter-token latency for chat |
| `--block-size` | `128` | Match FP8 block size |
| `--gpu-memory-utilization` | `0.75` | KV-cache headroom |
| `--language-model-only` | flag | Skip vision encoder; reclaim ~2–4 GB KV |
| `--enable-prefix-caching` | flag | Cache repeated system prompts |
| `--reasoning-parser` | `qwen3` | Strip/parse thinking for Qwen3.x |
| `--tool-call-parser` | `qwen3_xml` | Avoid infinite `!!!!` with long contexts |
| `--attention-backend` | `FLASHINFER` | Ada / Hopper / Blackwell |
| `--speculative-config` | `{"method":"qwen3_next_mtp","num_speculative_tokens":1}` | MTP; keep tokens at **1** |
| `-O3` | flag | `torch.compile` max opt |

<CodeGroup>
```bash title="Single GPU (e.g. H100 / H200)"
vllm serve Qwen/Qwen3.5-27B-FP8 \
  --max-model-len 262144 \
  --max-num-batched-tokens 4096 \
  --block-size 128 \
  --gpu-memory-utilization 0.75 \
  --language-model-only \
  --enable-prefix-caching \
  --reasoning-parser qwen3 \
  --tool-call-parser qwen3_xml \
  --attention-backend FLASHINFER \
  --speculative-config '{"method":"qwen3_next_mtp","num_speculative_tokens":1}' \
  -O3 \
  --host 127.0.0.1 \
  --port 8000
```

```bash title="Multi-GPU 4× RTX 5090 (Blackwell)"
NCCL_P2P_DISABLE=1 vllm serve Qwen/Qwen3.5-27B-FP8 \
  --tensor-parallel-size 4 \
  --max-model-len 262144 \
  --max-num-batched-tokens 4096 \
  --block-size 128 \
  --gpu-memory-utilization 0.75 \
  --language-model-only \
  --enable-prefix-caching \
  --reasoning-parser qwen3 \
  --tool-call-parser qwen3_xml \
  --attention-backend FLASHINFER \
  --speculative-config '{"method":"qwen3_next_mtp","num_speculative_tokens":1}' \
  -O3 \
  --host 127.0.0.1 \
  --port 8000
```
</CodeGroup>

<Warning>
`NCCL_P2P_DISABLE=1` is required for Blackwell (RTX 5090) when `tensor-parallel-size > 1` to avoid NCCL hangs. Prefer NCCL 2.27.3+.
</Warning>

Optional server-wide non-thinking default:

```bash
vllm serve Qwen/Qwen3.5-27B-FP8 \
  --default-chat-template-kwargs '{"enable_thinking": false}' \
  # ... remaining flags
```

Per-request control still uses `chat_template_kwargs.enable_thinking` (not a root-level field).

### Multi-turn thinking hygiene

Historical assistant turns should keep **final answers only**, not prior `<think>...</think>` blocks. vLLM’s chat template handles this when you use the stock OpenAI path; custom history builders must strip thinking tags.

### Extended context (YaRN)

Native 262k is enough for most flows. For ~1M tokens:

```bash
VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 vllm serve Qwen/Qwen3.5-27B-FP8 \
  --hf-overrides '{"text_config": {"rope_parameters": {"mrope_interleaved": true, "mrope_section": [11, 11, 10], "rope_type": "yarn", "rope_theta": 10000000, "partial_rotary_factor": 0.25, "factor": 4.0, "original_max_position_embeddings": 262144}}}' \
  --max-model-len 1010000 \
  # ... other parameters
```

YaRN uses a **static** scale (short prompts can pay a quality cost). Prefer `factor=2.0` for ~524k if you need half that extension.

## Verify the inference server

<Steps>
<Step title="Thinking mode (default)">
```bash
curl "http://127.0.0.1:8000/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen/Qwen3.5-27B-FP8",
    "messages": [{"role": "user", "content": "hey! what is the weather in Moscow?"}],
    "temperature": 1.0,
    "top_k": 20,
    "top_p": 0.95,
    "min_p": 0.0,
    "presence_penalty": 1.5,
    "repetition_penalty": 1.0
  }'
```
Expect `<think>` content in the stream/body when reasoning is on.
</Step>
<Step title="Non-thinking mode">
```bash
curl "http://127.0.0.1:8000/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen/Qwen3.5-27B-FP8",
    "messages": [{"role": "user", "content": "hey! what is the weather in Beijing?"}],
    "temperature": 0.7,
    "top_k": 20,
    "top_p": 0.8,
    "min_p": 0.0,
    "presence_penalty": 1.5,
    "repetition_penalty": 1.0,
    "chat_template_kwargs": {"enable_thinking": false}
  }'
```
Expect a direct answer without think tags.
</Step>
</Steps>

## Wire PentAGI: `LLM_SERVER_*` and Custom provider

The custom adapter (`backend/pkg/providers/custom/custom.go`) builds an OpenAI client from:

| Env | Config field | Purpose |
| --- | --- | --- |
| `LLM_SERVER_URL` | `LLMServerURL` | Base OpenAI API root (include `/v1`) |
| `LLM_SERVER_KEY` | `LLMServerKey` | Bearer token; use any non-empty placeholder if vLLM auth is off |
| `LLM_SERVER_MODEL` | `LLMServerModel` | Default model if agent YAML omits `model` |
| `LLM_SERVER_CONFIG_PATH` | `LLMServerConfig` | Path **inside the process** to AgentsConfig YAML |
| `LLM_SERVER_PROVIDER` | `LLMServerProvider` | Optional model name prefix (LiteLLM-style); usually empty for raw vLLM |
| `LLM_SERVER_LEGACY_REASONING` | `LLMServerLegacyReasoning` | Default `false` → modern structured reasoning opts |
| `LLM_SERVER_PRESERVE_REASONING` | `LLMServerPreserveReasoning` | Default `false`; set `true` if the backend requires `reasoning_content` on later turns |

Compose mounts the host YAML via `PENTAGI_LLM_SERVER_CONFIG_PATH` → container `/opt/pentagi/conf/custom.provider.yml` (see `docker-compose.yml`). Set the in-container path on `LLM_SERVER_CONFIG_PATH`.

### Example `.env` for host vLLM + Docker PentAGI

```bash
# Reachable FROM the pentagi container (not container-local loopback if vLLM is on the host)
LLM_SERVER_URL=http://<host-reachable-ip>:8000/v1
LLM_SERVER_KEY=dummy
LLM_SERVER_MODEL=Qwen/Qwen3.5-27B-FP8
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/custom.provider.yml
LLM_SERVER_LEGACY_REASONING=false
LLM_SERVER_PRESERVE_REASONING=false

# Host path bind-mounted into the container
PENTAGI_LLM_SERVER_CONFIG_PATH=./examples/configs/vllm-qwen3.5-27b-fp8.provider.yml
```

<Note>
If PentAGI runs in Docker and vLLM binds `127.0.0.1` on the host, the container cannot use `http://127.0.0.1:8000`. Bind vLLM to a host interface the container can reach, or use a host-gateway URL your Docker setup provides. UI “Base URL” must also resolve from the **PentAGI server process**, not only from the browser.
</Note>

Defaults when agent options omit sampling: temperature `1.0`, top_p `1.0`, n `1`, max_tokens `16384` (`BuildProviderConfig`). Provider YAML typically raises `max_tokens` to `32768`.

### UI Custom provider (no restart)

1. Open **Settings → Providers → Add Provider**
2. **Type**: Custom  
3. **Base URL**: `http://127.0.0.1:8000/v1` (or the server-reachable URL)  
4. **API Key**: `dummy`  
5. **Configuration**: paste contents of a `examples/configs/vllm-*.provider.yml`  
6. Save, then create a flow and select that provider  

Smoke task: `Scan localhost port 80` and watch tool/LLM logs.

## Thinking vs non-thinking provider YAML

Thinking is controlled with `extra_body.chat_template_kwargs.enable_thinking`, not a top-level API field.

| Mode | File pattern | Behavior |
| --- | --- | --- |
| **Thinking (quality)** | `vllm-*-fp8.provider.yml` | Primary/assistant/generator/refiner/adviser and coder/installer/pentester leave thinking **on** (no `extra_body`); simple / simple_json / searcher / enricher / reflector force `enable_thinking: false` |
| **Non-thinking (latency)** | `*-no-think.provider.yml` | Every agent sets `enable_thinking: false` |

### Sampling profiles (Qwen recommendations, applied in YAML)

| Mode | temperature | top_p | top_k | presence_penalty | Agents (thinking YAML) |
| --- | --- | --- | --- | --- | --- |
| Thinking, general | 1.0 | 0.95 | 20 | 1.5 | primary_agent, assistant, generator, refiner, adviser |
| Thinking, coding | 0.6 | 0.95 | 20 | 0.0 | coder, installer, pentester |
| Non-thinking, general | 0.7 | 0.8 | 20 | 1.5 | simple, simple_json, searcher, enricher |
| Non-thinking, reasoning-style | 1.0 | 0.95–1.0 | 20–40 | 1.5–2.0 | used heavily in `*-no-think` for specialist agents |

Shared defaults in examples: `repetition_penalty: 1.0`, `min_p: 0.0`, `n: 1`, `max_tokens: 32768`. Set `json: true` on `simple_json`.

Snippet (thinking config forces non-think only on lightweight agents):

```yaml
simple:
  model: "Qwen/Qwen3.5-27B-FP8"
  temperature: 0.7
  top_k: 20
  top_p: 0.8
  max_tokens: 32768
  extra_body:
    chat_template_kwargs:
      enable_thinking: false

primary_agent:
  model: "Qwen/Qwen3.5-27B-FP8"
  temperature: 1.0
  top_k: 20
  top_p: 0.95
  max_tokens: 32768
  # thinking enabled by default (no extra_body)

coder:
  model: "Qwen/Qwen3.5-27B-FP8"
  temperature: 0.6
  top_k: 20
  top_p: 0.95
  presence_penalty: 0.0
  max_tokens: 32768
```

## Supervision for sub-32B local models

README guidance for open-source models **&lt; 32B** (including Qwen3.5-27B-FP8): enable **execution monitoring** and **agent planning** for production-grade autonomous runs. Hard tool-call caps always apply.

| Variable | Default | Sub-32B recommendation |
| --- | --- | --- |
| `EXECUTION_MONITOR_ENABLED` | `false` | `true` — mentor (adviser) on same-tool / total-tool thresholds |
| `EXECUTION_MONITOR_SAME_TOOL_LIMIT` | `5` | start at `5`; raise for exploratory tasks |
| `EXECUTION_MONITOR_TOTAL_TOOL_LIMIT` | `10` | start at `10` |
| `AGENT_PLANNING_STEP_ENABLED` | `false` | `true` — 3–7 step plan before pentester/coder/installer |
| `MAX_GENERAL_AGENT_TOOL_CALLS` | `100` | keep or raise for long engagements |
| `MAX_LIMITED_AGENT_TOOL_CALLS` | `20` | keep defaults unless limited agents thrash |

```bash
EXECUTION_MONITOR_ENABLED=true
EXECUTION_MONITOR_SAME_TOOL_LIMIT=5
EXECUTION_MONITOR_TOTAL_TOOL_LIMIT=10
AGENT_PLANNING_STEP_ENABLED=true
MAX_GENERAL_AGENT_TOOL_CALLS=100
MAX_LIMITED_AGENT_TOOL_CALLS=20
```

**Trade-offs** (repo-reported for Qwen3.5-27B-FP8): ~2–3× tokens and wall time; ~2× result quality / less looping. Prefer the **thinking** provider YAML so adviser/planner use higher reasoning sampling; optionally give `adviser` a stronger model later without changing the rest of the stack.

Always-on layers (no env toggle): reflector after failed tool-call generations, barrier tools (`done`, `ask`), and hard max iterations for general vs limited agents.

## Performance (repo benchmarks)

On **4× RTX 5090**, guide figures for the 27B FP8 stack:

| Metric | Value |
| --- | --- |
| Prompt processing | ~13,000 tok/s |
| Completion | ~650 tok/s |
| Concurrent flows | ~12 stable |
| VRAM | ~30 GB/GPU at `--gpu-memory-utilization 0.75` |
| Context | full 262k when VRAM allows |

`examples/tests/vllm-qwen332b-fp16-report.md` also records high agent tool-call pass rates for `Qwen/Qwen3.5-27B-FP8` under the project’s agent test harness (not a substitute for end-to-end pentest QA).

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| `Unknown architecture 'qwen3_5'` | Stable vLLM | Install nightly wheels / image |
| Multi-GPU hang | Blackwell P2P | `NCCL_P2P_DISABLE=1`; upgrade `nvidia-nccl-cu12` |
| `enable_thinking` ignored | Wrong JSON shape | Nest under `chat_template_kwargs` (YAML: `extra_body.chat_template_kwargs`) |
| Infinite `!!!!` | Wrong tool parser | `--tool-call-parser qwen3_xml` (not `qwen3_coder`) |
| OOM | Context / util too high | Lower `--max-model-len` (e.g. `131072`) or `--gpu-memory-utilization` |
| Speculative decode errors | Unstable MTP depth | `num_speculative_tokens: 1` only |
| Custom provider empty / wrong models | Config path not mounted | Align `PENTAGI_LLM_SERVER_CONFIG_PATH` host file with `LLM_SERVER_CONFIG_PATH` container path |
| Connection refused from Docker | URL is container loopback | Point `LLM_SERVER_URL` at a host-reachable address |

## Air-gapped checklist

1. Pre-pull or offline-cache `Qwen/Qwen3.5-27B-FP8` (or chosen variant) and the vLLM nightly image/wheels.  
2. Run vLLM on the GPU host; no cloud LLM keys required.  
3. Configure Custom provider / `LLM_SERVER_*` only.  
4. Use thinking YAML + supervision flags for &lt;32B quality.  
5. Optionally disable external search engines if the network must stay closed (search tools fail closed without keys/endpoints; see search configuration pages).

## Related pages

<CardGroup>
<Card title="Local and custom providers" href="/local-and-custom-providers">
OpenAI-compatible endpoints, LLM_SERVER_* flags, Ollama, aggregators.
</Card>
<Card title="Example provider configs" href="/example-provider-configs">
Copy-paste YAML for vLLM, Ollama, OpenRouter, DeepInfra, Azure.
</Card>
<Card title="Provider configuration schema" href="/provider-config-schema">
AgentsConfig fields: model, sampling, json, reasoning, extra_body.
</Card>
<Card title="Agents and supervision" href="/agents-and-supervision">
Specialist agents, mentor monitor, planning step, tool-call limits.
</Card>
<Card title="Environment variables" href="/environment-variables">
Full Config / .env.example including supervision and custom LLM keys.
</Card>
<Card title="Installation" href="/installation">
Docker Compose core stack, volumes, SSL, overlays.
</Card>
</CardGroup>

---

## 25. Development and testing

> Backend go build and test, frontend pnpm scripts, gqlgen and graphql:generate, ctester container tests, etester embeddings, ftester tool calling, and local compose for contributors.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/25-development-and-testing.md
- Generated: 2026-07-10T07:13:57.064Z

### Source Files

- `CLAUDE.md`
- `backend/go.mod`
- `frontend/package.json`
- `backend/cmd/ctester/main.go`
- `backend/cmd/etester/main.go`
- `backend/cmd/ftester/main.go`
- `backend/gqlgen/gqlgen.yml`
- `README.md`

---
title: "Development and testing"
description: "Backend go build and test, frontend pnpm scripts, gqlgen and graphql:generate, ctester container tests, etester embeddings, ftester tool calling, and local compose for contributors."
---

PentAGI is a monorepo: `backend/` is a Go 1.24 module (`module pentagi`) that builds the API server and tester binaries; `frontend/` is a React + TypeScript Vite app managed with `pnpm@10`. Contributors run unit tests with `go test` and Vitest, regenerate GraphQL stubs after schema edits, and use three in-tree utilities—`ctester`, `etester`, and `ftester`—to validate LLM agents, embeddings, and tool execution against a live or mock stack.

## Prerequisites

| Tool | Role |
|---|---|
| Go (matches `backend/go.mod`, currently `go 1.24.1`) | Backend build, tests, gqlgen |
| Node.js + pnpm 10 | Frontend install, dev server, Vitest, GraphQL codegen |
| Docker + Docker Compose | Core stack, sandboxed workers, image builds |
| PostgreSQL with pgvector | Database for local API and embedding tests |
| commitlint | Conventional commits (repo expectation) |
| Optional: `swag`, `golangci-lint`, `sqlc`, `fern-api` | REST docs, lint, ORM gen, Langfuse SDK gen |

Copy `.env.example` to `.env` and set at least `DATABASE_URL` and one LLM provider key before running the API or tester binaries. Provider and search keys are BYOK: point env vars at any compatible endpoint; no hosted vendor is required for local development.

## Repository layout

```text
pentagi/
├── backend/
│   ├── cmd/pentagi/     # Main API server
│   ├── cmd/ctester/     # LLM agent / provider config tester
│   ├── cmd/etester/     # Embedding + pgvector tester
│   ├── cmd/ftester/     # Tool and agent function tester
│   ├── cmd/installer/   # Interactive TUI deploy wizard
│   ├── gqlgen/gqlgen.yml
│   ├── pkg/graph/       # schema.graphqls + gqlgen output
│   ├── sqlc/            # SQL → Go generation
│   └── migrations/sql/  # goose migrations (auto-run at startup)
├── frontend/
│   ├── package.json     # pnpm scripts
│   ├── graphql-codegen.ts
│   └── src/graphql/     # Generated Apollo types (do not hand-edit)
├── docker-compose.yml   # Core stack
├── docker-compose-*.yml # Observability, Langfuse, Graphiti overlays
└── Dockerfile           # Multi-stage: UI + pentagi/ctester/etester/ftester
```

## Backend build, test, and lint

Run from `backend/`:

```bash
go mod download
go build -trimpath -o pentagi ./cmd/pentagi
go test ./...
go test ./pkg/foo/... -v -run TestName
golangci-lint run --timeout=5m
```

The production `Dockerfile` builds with `CGO_ENABLED=0` and `-trimpath`, and emits four binaries under `/opt/pentagi/bin/`: `pentagi`, `ctester`, `etester`, and `ftester`. Locally, the same packages can be run with `go run`.

### Local API process

```bash
cd backend
# Load env (or export vars from .env)
go run cmd/pentagi/main.go
```

Useful local overrides (often set in `.vscode/launch.json`):

| Variable | Purpose | Typical local value |
|---|---|---|
| `DATABASE_URL` | Postgres connection | `postgres://postgres:postgres@localhost:5432/pentagidb?sslmode=disable` |
| `DOCKER_HOST` | Docker SDK socket | macOS Docker Desktop raw socket path |
| `SERVER_PORT` | HTTP listen port | `8443` (default) |
| `SERVER_USE_SSL` | TLS for the API | `false` for plain local HTTP |

First start downloads Docker worker images and can take several minutes.

### Connection pool defaults

PentAGI opens two pools to the same Postgres instance:

| Pool | Env var | Default |
|---|---|---|
| Shared `sql.DB` (sqlc / GORM) | `DATABASE_MAX_OPEN_CONNS` | `25` |
| Shared `pgxpool` (pgvector) | `DATABASE_VECTOR_MAX_CONNS` | `10` |

Also: `DATABASE_MAX_IDLE_CONNS` (default `5`). Defaults target roughly ten parallel flows against the stock `vxcontrol/pgvector` image (`max_connections=100`).

## Code generation

### GraphQL (gqlgen)

Schema lives in `backend/pkg/graph/schema.graphqls`. Config is `backend/gqlgen/gqlgen.yml`:

- **Schema glob:** `pkg/graph/*.graphqls`
- **Exec output:** `pkg/graph/generated.go`
- **Models:** `pkg/graph/model/models_gen.go`
- **Resolvers:** `follow-schema` layout → `pkg/graph/{name}.resolvers.go`
- **ID scalar:** primarily `graphql.Int64`

After schema changes:

```bash
cd backend
go run github.com/99designs/gqlgen --config ./gqlgen/gqlgen.yml
```

### Swagger (REST)

Install once, then regenerate after handler annotation changes:

```bash
go install github.com/swaggo/swag/cmd/swag@v1.8.7
cd backend
swag init -g ../../pkg/server/router.go -o pkg/server/docs/ \
  --parseDependency --parseInternal --parseDepth 2 -d cmd/pentagi
```

Swagger UI is served at `/api/v1/swagger` when the API is running.

### sqlc

```bash
cd backend
docker run --rm -v $(pwd):/src -w /src --network pentagi-network \
  -e DATABASE_URL="{URL}" \
  sqlc/sqlc:1.27.0 generate -f sqlc/sqlc.yml
```

### Langfuse SDK (optional)

```bash
pnpm add -g fern-api
cd backend && fern generate --local
```

### Frontend GraphQL types

`frontend/graphql-codegen.ts` reads the backend schema and `./graphql-schema.graphql` documents, then writes Apollo hooks to `src/graphql/types.ts` (post-write Prettier):

```bash
cd frontend
pnpm install
pnpm run graphql:generate
```

Regenerate after any backend schema or frontend operation document change. Do not hand-edit `src/graphql/types.ts`.

## Frontend scripts

Package manager: `pnpm@10.32.1` (`packageManager` field in `package.json`).

| Script | Command | Purpose |
|---|---|---|
| Install | `pnpm install` | Dependencies |
| Dev | `pnpm run dev` | Vite dev server (default host `0.0.0.0`, port `8000`) |
| Build | `pnpm run build` | `tsc && vite build` |
| Test | `pnpm run test` | `vitest run` |
| Coverage | `pnpm run test:coverage` | Vitest + v8 coverage |
| Watch tests | `pnpm run test:watch` | Interactive Vitest |
| Lint | `pnpm run lint` / `lint:fix` | ESLint on `src/**/*.{ts,tsx,js,jsx}` |
| Format | `pnpm run prettier` / `prettier:fix` | Prettier check / write |
| GraphQL | `pnpm run graphql:generate` | Codegen from schema |
| SSL | `pnpm run ssl:generate` | Dev certs (also auto on `dev`) |

Vite env (frontend launch config):

| Variable | Meaning | Notes |
|---|---|---|
| `VITE_API_URL` | Backend host:port | Omit scheme (`localhost:8080`, not `http://…`) |
| `VITE_USE_HTTPS` | Frontend HTTPS | Default `false` |
| `VITE_PORT` | Dev port | Default `8000` |
| `VITE_HOST` | Bind address | Default `0.0.0.0` |

## Local stack with Compose

From the repo root:

```bash
# Core services (API, UI assets in image, pgvector, scraper, …)
docker compose up -d

# Optional overlays
docker compose -f docker-compose.yml -f docker-compose-observability.yml up -d
docker compose -f docker-compose.yml -f docker-compose-langfuse.yml up -d
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml up -d

# Local image
docker build -t local/pentagi:latest .
```

Compose stack URL: `https://localhost:8443`. Create Docker networks via the core file first if overlays fail with missing network names.

### Contributor local workflows

<Tabs>
  <Tab title="Split process (API + UI)">
    1. Start Postgres (pgvector) and ensure Docker is available.
    2. From `backend/`: configure `.env`, run `go run cmd/pentagi/main.go`.
    3. From `frontend/`: `pnpm install && pnpm run dev`.
    4. Point `VITE_API_URL` at the backend listen address.
  </Tab>
  <Tab title="Full Compose">
    1. Copy `.env.example` → `.env` with DB and at least one LLM key.
    2. `docker compose up -d`.
    3. Open `https://localhost:8443`, change default admin credentials, create a flow.
    4. Use `docker exec -it pentagi /opt/pentagi/bin/{ctester,etester,ftester}` for in-container tests.
  </Tab>
</Tabs>

## Utility binaries

| Binary | Package | Purpose |
|---|---|---|
| `ctester` | `backend/cmd/ctester` | Parallel LLM agent capability tests and markdown reports |
| `etester` | `backend/cmd/etester` | Embedding provider + pgvector ops (`test`, `info`, `flush`, `reindex`, `search`) |
| `ftester` | `backend/cmd/ftester` | Invoke tools/agents with mock or real flow context |
| `installer` | `backend/cmd/installer` | TUI wizard for deploy-time `.env` and compose setup |

Image paths: `/opt/pentagi/bin/ctester`, `/opt/pentagi/bin/etester`, `/opt/pentagi/bin/ftester`.

## ctester — LLM agent testing

`ctester` loads `.env` (or `-env`), builds a provider of type `-type`, runs the shared tester suite, prints a summary, and optionally writes `-report`.

### Flags

| Flag | Default | Description |
|---|---|---|
| `-env` | `.env` | Environment file |
| `-type` | `custom` | `custom`, `openai`, `anthropic`, `gemini`, `bedrock`, `ollama`, `deepseek`, `glm`, `kimi`, `qwen` |
| `-name` | empty | Sets provider name used when building custom config |
| `-config` | empty | Provider YAML path (overrides `LLM_SERVER_CONFIG` / `OLLAMA_SERVER_CONFIG`) |
| `-tests` | empty | Custom tests YAML |
| `-report` | empty | Markdown report output path |
| `-agents` | `all` | Comma-separated agent option types |
| `-groups` | `all` | Comma-separated test groups |
| `-workers` | `4` | Parallel workers |
| `-verbose` | false | Verbose output |

**Agent names:** `simple`, `simple_json`, `primary_agent`, `assistant`, `generator`, `refiner`, `adviser`, `reflector`, `searcher`, `enricher`, `coder`, `installer`, `pentester`.

**Test groups:** `basic`, `advanced`, `json`, `knowledge` (all selected when `-groups all`).

### Local examples

```bash
cd backend
go run cmd/ctester/*.go -verbose
go run cmd/ctester/*.go -config ../examples/configs/openrouter.provider.yml -verbose
go run cmd/ctester/*.go -agents simple,simple_json,primary_agent -groups basic,advanced -verbose
go run cmd/ctester/*.go -config ../examples/configs/deepinfra.provider.yml -report ../test-report.md
```

### Docker examples

```bash
docker run --rm -v $(pwd)/.env:/opt/pentagi/.env \
  vxcontrol/pentagi /opt/pentagi/bin/ctester -verbose

docker exec -it pentagi /opt/pentagi/bin/ctester -type openai
docker exec -it pentagi /opt/pentagi/bin/ctester \
  -config /opt/pentagi/conf/openrouter.provider.yml -verbose
```

Sample reports for many providers live under `examples/tests/`. Example provider YAMLs ship in `examples/configs/` and are copied into the image at `/opt/pentagi/conf/`.

## etester — embeddings and pgvector

`etester` requires a working `DATABASE_URL` and embedding config from the environment. Default command is `test`.

### Commands

| Command | Behavior |
|---|---|
| `test` | Probe embedding provider and pgvector connectivity |
| `info` | Collection / embedding table statistics |
| `flush` | Delete all embedding documents (destructive) |
| `reindex` | Recalculate embeddings for all documents |
| `search` | Similarity search with optional filters |

### Flags

| Flag | Default | Description |
|---|---|---|
| `-env` | `.env` | Environment file |
| `-verbose` | false | Verbose output |
| `-help` | false | Help |

Search options (from `search -help` / README): `-query` (required), `-doc_type` (`answer`, `memory`, `guide`, `code`), `-flow_id`, `-answer_type`, `-guide_type`, `-limit` (default `3`), `-threshold` (default `0.7`).

```bash
cd backend
go run cmd/etester/main.go test -verbose
go run cmd/etester/main.go info
go run cmd/etester/main.go search -query "How to install PostgreSQL" -limit 5

# Destructive: after switching embedding provider
go run cmd/etester/main.go flush
go run cmd/etester/main.go reindex
```

```bash
docker exec -it pentagi /opt/pentagi/bin/etester test
docker exec -it pentagi /opt/pentagi/bin/etester search \
  -query "Security vulnerability" -doc_type guide -threshold 0.8
```

<Warning>
Changing embedding providers breaks vector compatibility. Run `flush` or `reindex` so the store is not mixed-dimension or mixed-semantic.
</Warning>

## ftester — tools and agent functions

`ftester` wires config, Postgres, Docker, provider controller, optional Langfuse/OTEL, and a tools executor. With `-flow 0` (default) it uses mock proxies; a non-zero `-flow` binds real flow/task/subtask context.

### Global flags

| Flag | Default | Description |
|---|---|---|
| `-env` | `.env` | Environment file |
| `-provider` | `custom` | `openai`, `anthropic`, `gemini`, `bedrock`, `ollama`, `deepseek`, `glm`, `kimi`, `qwen`, `custom` |
| `-flow` | `0` | Flow ID; `0` = mock mode |
| `-user` | `0` | User ID (admin is typically `1`) |
| `-task` | unset | Task ID when non-zero |
| `-subtask` | unset | Subtask ID when non-zero |

Usage pattern:

```bash
cd backend
go run cmd/ftester/main.go [function] -[arg] [value]
go run cmd/ftester/main.go [function] -help   # per-function params
go run cmd/ftester/main.go                    # list functions
```

### Function groups

| Group | Functions |
|---|---|
| Environment | `terminal`, `file` |
| Search | `browser`, `google`, `duckduckgo`, `tavily`, `traversaal`, `perplexity`, `sploitus`, `searxng` |
| Vector memory | `search_in_memory`, `search_guide`, `search_answer`, `search_code` |
| Agents | `advice`, `coder`, `maintenance`, `memorist`, `pentester`, `search` |
| Utility | `describe` (flows/tasks/subtasks inspection) |

### Examples

```bash
# Mock terminal
go run cmd/ftester/main.go terminal -command "ls -la" -message "List files"

# Real flow context
go run cmd/ftester/main.go -flow 123 terminal -command "whoami" -message "Check user"

# Agent in task/subtask scope
go run cmd/ftester/main.go -flow 123 -task 456 -subtask 789 \
  pentester -message "Find vulnerabilities"

# Stuck-flow diagnosis
go run cmd/ftester/main.go describe
go run cmd/ftester/main.go -flow 123 describe -verbose

# Config smoke tests
go run cmd/ftester/main.go google -query "pentesting tools"
go run cmd/ftester/main.go browser -url "https://example.com"
```

```bash
docker exec -it pentagi /opt/pentagi/bin/ftester -flow 123 describe
docker exec -it pentagi /opt/pentagi/bin/ftester -flow 123 \
  terminal -command "ps aux" -message "List processes"
```

When Langfuse / OTEL are configured, ftester calls emit traces and metrics (same observer path as the main server). Interactive mode prompts for missing args when you pass only a function name.

## Image build

Version helpers inject package version into binaries:

```bash
source ./scripts/version.sh
docker build \
  --build-arg PACKAGE_VER=$PACKAGE_VER \
  --build-arg PACKAGE_REV=$PACKAGE_REV \
  -t pentagi:$PACKAGE_VER .
```

Multi-platform: `docker buildx build --platform linux/amd64,linux/arm64 …`. The multi-stage Dockerfile compiles the frontend with GraphQL schema present, builds all four Go binaries with version ldflags, and stages configs under `/opt/pentagi/conf/`.

## Verification checklist

<Steps>
  <Step title="Backend unit tests">
    `cd backend && go test ./...` — all packages green.
  </Step>
  <Step title="Frontend unit tests and lint">
    `cd frontend && pnpm run test && pnpm run lint && pnpm run prettier` — Vitest and style gates pass.
  </Step>
  <Step title="Schema parity">
    After GraphQL edits: run gqlgen in `backend/`, then `pnpm run graphql:generate` in `frontend/`. Confirm resolvers compile and UI types update.
  </Step>
  <Step title="Provider readiness">
    `go run cmd/ctester/*.go -type <provider> -groups basic -verbose` (or Docker equivalent) before relying on a new model for flows.
  </Step>
  <Step title="Embeddings">
    `go run cmd/etester/main.go test -verbose` against the same `DATABASE_URL` the API uses.
  </Step>
  <Step title="Tools / sandbox">
    `go run cmd/ftester/main.go terminal -command "echo ok" -message "smoke"` with Docker available; use `-flow` when validating against a real engagement.
  </Step>
</Steps>

## Common failure modes

| Symptom | Likely cause | Action |
|---|---|---|
| Provider create fails in ctester | Missing API key / URL for `-type` | Set provider env vars or use `-config` YAML |
| etester cannot connect | Bad `DATABASE_URL` or no pgvector | Start Postgres; confirm extension |
| Poor vector search after model change | Mixed embedding spaces | `flush` / `reindex` |
| ftester Docker errors | No Docker socket / wrong `DOCKER_HOST` | Fix socket path (macOS raw socket often required) |
| Frontend cannot reach API | Wrong `VITE_API_URL` scheme or port | Host:port only; match backend listen |
| gqlgen / codegen drift | Schema edited without regen | Re-run gqlgen + `graphql:generate` |
| Overlay compose network missing | Core stack never started | `docker compose up` core first |

## Adding an LLM provider (dev impact)

When introducing a new provider type, update the Go adapter, register it in `pkg/providers`, whitelist it in `pkg/server/models/providers.go` `Valid()`, add config keys, ship a goose migration for `PROVIDER_TYPE`, add a frontend icon, and regenerate GraphQL/types if the API surface changes. Skipping the REST whitelist yields **422 Unprocessable Entity** on provider APIs.

## Related pages

<CardGroup cols={2}>
  <Card title="Contributing" href="/contributing">
    License-compatible dependencies, generate-licenses.sh, and PR expectations.
  </Card>
  <Card title="Installation" href="/installation">
    Compose stack, .env, SSL, volumes, and optional overlays.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Full Config / .env.example reference for local and production.
  </Card>
  <Card title="GraphQL API" href="/graphql-api">
    schema.graphqls operations that gqlgen and frontend codegen consume.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    Env wiring for OpenAI, Anthropic, Gemini, Bedrock, and others used by ctester.
  </Card>
  <Card title="Example provider configs" href="/example-provider-configs">
    YAML samples under examples/configs for local agent testing.
  </Card>
  <Card title="Interactive installer" href="/installer">
    TUI path when you prefer guided .env and compose setup over hand-editing.
  </Card>
  <Card title="Docker sandbox and workers" href="/docker-sandbox-workers">
    Socket, images, and multi-host constraints that ftester and the API share.
  </Card>
</CardGroup>

---

## 26. Contributing

> License-compatible dependency policy, generate-licenses.sh, PR expectations, and contributor workflow boundaries for backend and frontend changes.

- Page Markdown: https://grok-wiki.com/public/docs/vxcontrol-pentagi-eca0cf4de5f5/pages/26-contributing.md
- Generated: 2026-07-10T07:10:31.596Z

### Source Files

- `CONTRIBUTING.md`
- `CONTRIBUTORS.md`
- `scripts/generate-licenses.sh`
- `licenses/README.md`
- `LICENSE`
- `.github/PULL_REQUEST_TEMPLATE.md`
- `CLAUDE.md`

---
title: "Contributing"
description: "License-compatible dependency policy, generate-licenses.sh, PR expectations, and contributor workflow boundaries for backend and frontend changes."
---

PentAGI is an MIT-licensed monorepo (`backend/` Go API, `frontend/` React + TypeScript UI). Contributions must keep third-party dependencies on MIT-compatible licenses, refresh reports via `scripts/generate-licenses.sh`, and meet the checklist in `.github/PULL_REQUEST_TEMPLATE.md` before merge.

## Project license stack

| Artifact | Role |
|---|---|
| `LICENSE` | MIT License for project source (Copyright 2025 PentAGI Development Team) |
| `NOTICE` | Copyright notice; notes VXControl Cloud SDK AGPL-3.0 with a special exception for this official project |
| `EULA.md` | End-user terms for source, Docker images, and web UI; MIT governs source when terms conflict |
| `licenses/` | Generated third-party dependency license reports (see below) |
| Docker image | Ships `LICENSE`, `NOTICE`, `EULA`, and reports under `/opt/pentagi/licenses/` |

Contact for license questions: **info@pentagi.com** or **info@vxcontrol.com**.

<Note>
VXControl Cloud **service** access (threat intelligence, premium features) is separate from the MIT-licensed SDK code and may require a license key and cloud Terms of Service. Adding or changing that integration is not the same as adding a generic Go/npm dependency.
</Note>

## Dependency license policy

All new dependencies must use licenses compatible with the MIT project license.

### Approved

| License | Notes |
|---|---|
| MIT | Preferred |
| Apache-2.0 | Allowed |
| BSD-2-Clause, BSD-3-Clause | Allowed |
| ISC | Allowed |
| MPL-2.0 | Allowed **if used without modification** |
| 0BSD | Public-domain style; allowed |

### Incompatible

| License | Notes |
|---|---|
| GPL, LGPL, AGPL | Incompatible without a special exception (do not introduce copyleft into the main dependency tree) |
| CC-BY-SA | Not for code; may be acceptable for data assets only |
| Proprietary / commercial | Not allowed as a default dependency |

Do not add a dependency until its SPDX license is in the approved set. If a library is dual-licensed, pin usage to an approved license and document that choice in the PR.

## Generate and review license reports

`scripts/generate-licenses.sh` collects backend and frontend dependency license data into `licenses/`. Generated report files are listed in `licenses/.gitignore`; keep the script and `licenses/README.md` under version control.

### Tools

| Tool | Package surface | Install / use |
|---|---|---|
| `go list -m all` | Go modules | Built into Go; always used by the script |
| `go-licenses` | Go license CSV | `go install github.com/google/go-licenses@latest` |
| `pnpm ls --prod --json` | Frontend prod tree | Requires `pnpm install` in `frontend/` |
| `license-checker` | Frontend license JSON/CSV | Global CLI; optional for local runs |
| `osv-scanner` | Security + license scan | Recommended pre-merge gate |

### Outputs

After a successful local run from the repo root:

```text
licenses/
├── backend-dependencies.txt   # go list -m all
├── backend-licenses.csv       # go-licenses csv ./cmd/pentagi
├── frontend-dependencies.json # pnpm ls --prod --json
├── frontend-licenses.json     # license-checker --production --json
└── frontend-licenses.csv      # license-checker --production --csv
```

Docker multi-stage builds regenerate the same class of reports and copy them into the runtime image:

| Build stage | Path in image |
|---|---|
| Frontend (`license-checker`) | `/opt/pentagi/licenses/frontend/` (`licenses.json`, `licenses.csv`) |
| Backend (`go list`, `go-licenses`) | `/opt/pentagi/licenses/backend/` (`dependencies.txt`, `licenses.csv`) |

### Pre-merge procedure for dependency changes

<Steps>
  <Step title="Tidy and lock dependencies">
    ```bash
    cd backend && go mod tidy
    cd ../frontend && pnpm install
    ```
    Commit `go.mod` / `go.sum` and `pnpm-lock.yaml` with the code change when either changes.
  </Step>
  <Step title="Generate license reports">
    ```bash
    ./scripts/generate-licenses.sh
    ```
    Install `go-licenses` first if backend CSV generation is skipped. Run `pnpm install` in `frontend/` before expecting frontend reports.
  </Step>
  <Step title="Scan for incompatible licenses">
    ```bash
    osv-scanner scan --experimental-licenses="MIT,Apache-2.0,BSD-2-Clause,BSD-3-Clause,ISC,MPL-2.0" backend
    osv-scanner scan --experimental-licenses="MIT,Apache-2.0,BSD-2-Clause,BSD-3-Clause,ISC,MPL-2.0" frontend
    ```
    Resolve any license or vulnerability findings before requesting review.
  </Step>
  <Step title="Document the dependency in the PR">
    Use the PR **Security Considerations** and **Compatibility** sections: name the package, license, and why it is required. Mark breaking or security-sensitive dependency bumps clearly.
  </Step>
</Steps>

## Contributor workflow boundaries

### Monorepo layout

| Path | Stack | Typical change surface |
|---|---|---|
| `backend/` | Go 1.24, Gin REST, gqlgen GraphQL | API, agents, providers, tools, Docker sandbox, DB migrations |
| `frontend/` | React, TypeScript, pnpm, Apollo | UI, settings, GraphQL operations |
| `observability/` | Compose / monitoring configs | Optional Grafana/OTEL stack only |
| `examples/` | Provider YAML, prompts, guides | Copy-paste configs and sample flow inputs |
| `scripts/` | Shell helpers | `generate-licenses.sh`, entrypoint, version helpers |
| `.github/` | CI, issue/PR templates | Workflow and contribution UX |

### Backend change boundaries

Run from `backend/`:

```bash
go mod download
go build -trimpath -o pentagi ./cmd/pentagi
go test ./...
golangci-lint run --timeout=5m
```

Regenerate when schemas or REST annotations change:

| Change | Command |
|---|---|
| `pkg/graph/schema.graphqls` | `go run github.com/99designs/gqlgen --config ./gqlgen/gqlgen.yml` |
| REST swag annotations | `swag init -g ../../pkg/server/router.go -o pkg/server/docs/ --parseDependency --parseInternal --parseDepth 2 -d cmd/pentagi` |
| SQL migrations | Add goose SQL under `backend/migrations/sql/` (applied at startup) |

Utility binaries for local verification (not production entrypoints): `cmd/ctester`, `cmd/ftester`, `cmd/etester`, `cmd/installer`.

PR checklist expects `go fmt` and `go vet` for Go code.

### Frontend change boundaries

Run from `frontend/`:

```bash
pnpm install
pnpm run lint
pnpm run prettier
pnpm run test
pnpm run build
```

| Change | Command / rule |
|---|---|
| `frontend/src/graphql/*.graphql` | `pnpm run graphql:generate` |
| `frontend/src/graphql/` generated types | Do **not** hand-edit; regenerate |
| Dev server | `pnpm run dev` → `http://localhost:8000` |

PR checklist expects `pnpm run lint` for TypeScript/JavaScript.

### Adding a new LLM provider

Cross-cutting changes must land together or the API rejects the type:

1. Implement `provider.Provider` under `backend/pkg/providers/<name>/`.
2. Register type constants and wiring in `pkg/providers/provider/provider.go` and `pkg/providers/providers.go`.
3. Add the type to `Valid()` in `pkg/server/models/providers.go` — missing this returns **422 Unprocessable Entity**.
4. Add env keys in `pkg/config/config.go` (for example `<NAME>_API_KEY`, `<NAME>_SERVER_URL`).
5. Add `PROVIDER_TYPE` via a goose migration in `backend/migrations/sql/`.
6. Add icon + registration under `frontend/src/components/icons/`.
7. Update GraphQL schema / frontend settings if the UI must expose the provider.
8. Run license generation and `osv-scanner` if new modules were introduced.

Keep providers BYOK/BYOC: configuration is env keys, optional server URLs, and YAML agent configs — do not hard-code a single hosted vendor as required.

### Password and token-related changes

When touching registration, password reset, or API token secrets, enforce both backend and frontend validation:

- Minimum **12** characters
- At least one uppercase, one lowercase, one number, one special character
- Reject common weak passwords (for example `password`, `123456`)
- Never rely on frontend-only checks

### Acceptable-use constraint for product changes

`EULA.md` restricts use to authorized penetration testing and research. Features that assume unauthorized access, or that ship attack payloads outside sandboxed tool execution, are out of scope for contribution.

## Pull requests

Incomplete PRs may be closed at maintainers' discretion. Use `.github/PULL_REQUEST_TEMPLATE.md` in full.

### Required narrative

| Section | Expectation |
|---|---|
| **Problem / Solution** | Enough design detail for review without reading the full diff |
| **Closes #** | Link issues when applicable |
| **Type of Change** | Bug, feature, breaking, docs, config, test, or security |
| **Areas Affected** | Core, agents, tools, memory, monitoring, Langfuse, integrations, docs, infra |
| **Testing and Verification** | Repro steps, test config (version, Docker, OS, LLM provider, features), results |
| **Security Considerations** | New deps, permissions, sandbox/network impact |
| **Performance Impact** | Especially for agents, memory, or heavy data paths |
| **Documentation Updates** | README, API docs, config docs, GraphQL schema, other |
| **Deployment Notes** | New env vars, migrations, compose changes |

### Checklist (from template)

**Code quality**

- Coding standards followed
- Docs and tests updated as needed
- New and existing tests pass
- `go fmt` / `go vet` (Go)
- `pnpm run lint` (TS/JS)

**Security**

- Security implications considered
- Security model preserved or improved
- Secrets not committed

**Compatibility**

- Backward compatible, or breaking changes marked and documented
- Dependencies updated correctly (license-compatible)

**Documentation**

- Clear docs and non-obvious comments
- API changes documented

### CI surface

`.github/workflows/ci.yml` runs on every branch push:

| Job step | What it runs |
|---|---|
| Frontend | `pnpm install --frozen-lockfile`, `prettier`, `lint`, `test` |
| Backend | `go mod download`, `golangci-lint`, `go test ./...`, linux/amd64 + arm64 build of `./cmd/pentagi` |
| Docker build/push | Only on `main` or version tags `vX.Y.Z`; multi-arch image `vxcontrol/pentagi` |

Many lint/test steps currently use `continue-on-error: true`. Treat local green checks as the merge bar even when CI soft-fails.

## Issues

Use the structured templates under `.github/ISSUE_TEMPLATE/`:

| Template | Use for |
|---|---|
| Bug report | Repro steps, component multi-select, system config, enabled features (Langfuse/Grafana/custom LLM) |
| Enhancement | Problem statement, proposed solution, optional technical approach and security notes |

Default assignee for both templates is the project lead (`asdek`).

## Contributor recognition

`CONTRIBUTORS.md` lists core team and external contributors with linked PRs. After a history rewrite on March 29, 2026 (licensing cleanup), individual commit history may not appear in GitHub's UI; contribution credit is preserved in that document. Point new significant work at a PR and, when maintainers update the list, at `CONTRIBUTORS.md`.

## Local stack for end-to-end verification

```bash
cp .env.example .env   # set DB + at least one LLM provider key
docker compose up -d
# optional overlays:
# docker compose -f docker-compose.yml -f docker-compose-observability.yml up -d
# docker compose -f docker-compose.yml -f docker-compose-langfuse.yml up -d
# docker compose -f docker-compose.yml -f docker-compose-graphiti.yml up -d
docker build -t local/pentagi:latest .
```

UI: `https://localhost:8443`. Prefer changing default admin credentials before any shared or long-lived environment.

## Troubleshooting

| Symptom | Likely cause | Action |
|---|---|---|
| `go-licenses` step skipped | Tool not on `PATH` | `go install github.com/google/go-licenses@latest` |
| Frontend license files empty | No `frontend/node_modules` | `pnpm install` in `frontend/`, re-run script |
| `osv-scanner` flags GPL/AGPL | Incompatible transitive dep | Replace package or pin an approved license path |
| Provider create returns 422 | Type not in `Valid()` whitelist | Update `pkg/server/models/providers.go` + migration |
| GraphQL types out of date | Schema or `.graphql` ops changed without codegen | Run gqlgen and/or `pnpm run graphql:generate` |
| PR closed for missing info | Template incomplete | Fill Problem/Solution, tests, security, areas affected |

## Related pages

<CardGroup>
  <Card title="Development and testing" href="/development-and-testing">
    Backend go build/test, frontend pnpm scripts, codegen, and helper binaries for contributors.
  </Card>
  <Card title="Configure LLM providers" href="/configure-llm-providers">
    Env keys and UI profiles when a contribution adds or changes a provider.
  </Card>
  <Card title="Environment variables" href="/environment-variables">
    Config keys and defaults that deployment notes in a PR must document.
  </Card>
  <Card title="Installation" href="/installation">
    Compose stack and volumes for local end-to-end verification of a change.
  </Card>
</CardGroup>

---
