Guarded local and OpenRouter models
Pentest MCP can use OpenAI-compatible chat-completions models for report drafts and tool-using assessments. This includes loopback llama.cpp and Ollama servers and OpenRouter. An API assessment model is always treated as an untrusted control-plane input. It never receives a shell, filesystem, browser, repository, Docker socket, SSH agent, or arbitrary HTTP client from Pentest MCP.
The guard constrains actions requested through Pentest MCP. It does not sandbox the process that serves a local model. A model server launched under a user that can read or delete the operator's files retains those operating system permissions. Run a refusal-resistant or abliterated model server under a dedicated account, container, or VM before relying on the MCP guard.
Provider kinds
kind | Endpoint | Credential | local-agent | approved-provider |
|---|---|---|---|---|
openai-compatible | Required; ends in /chat/completions | OPENAI_COMPATIBLE_API_KEY; optional only on loopback | Loopback only | Loopback or HTTPS |
openrouter | Defaults to https://openrouter.ai/api/v1/chat/completions; remote overrides are pinned to openrouter.ai | OPENROUTER_API_KEY; a loopback test proxy uses only OPENROUTER_PROXY_API_KEY | Rejected before context is sent | Allowed when the engagement policy permits it |
Plain HTTP is accepted only for 127.0.0.1, localhost, or [::1]. Other endpoints require HTTPS. URLs containing credentials, query strings, or fragments are rejected. Provider redirects are rejected. Credential values stay in the host environment and are never read from the profile JSON.
The served model ID must exactly match the configured model. For llama.cpp, configure the ID returned by that server's /v1/models endpoint; this may be its absolute model path, or a stable value selected with llama-server --alias. A mismatch stops the run and leaves a guarded-run failure receipt. Tool-capable llama.cpp models need a compatible chat template and function calling enabled; current llama.cpp documents OpenAI-style tool calling through its Jinja chat-template path.
Current Ollama OpenAI compatibility supports tools but does not support the tool_choice request field. Pentest MCP omits tool_choice for all compatible providers; supplying tools without it retains the API's automatic selection behavior and works across current llama.cpp, Ollama, and OpenRouter implementations.
Local llama.cpp draft profile
Start the reviewed model server on loopback. Prefer an API key even on a workstation shared with other local users.
export OPENAI_COMPATIBLE_API_KEY='operator-generated-loopback-secret'{
"profiles": [
{
"id": "local-report-model",
"kind": "openai-compatible",
"mode": "draft",
"model": "exact-model-id-from-v1-models",
"endpoint": "http://127.0.0.1:8081/v1/chat/completions",
"maxTokens": 8192,
"reasoningEffort": "high"
}
]
}A loopback server without authentication also works when OPENAI_COMPATIBLE_API_KEY is unset. That configuration assumes the local host is single-tenant and the port cannot be reached by another user or container.
Guarded llama.cpp assessment profile
{
"profiles": [
{
"id": "local-guarded-assessor",
"kind": "openai-compatible",
"mode": "assessment",
"isolation": "guarded-mcp",
"model": "exact-model-id-from-v1-models",
"endpoint": "http://127.0.0.1:8081/v1/chat/completions",
"workingDirectory": "/private/empty/mcp-worker-cwd",
"mcpDataDir": "/private/pentest-worker-data",
"mcpNodeCommand": "/absolute/path/to/node",
"mcpServerEntry": "/absolute/path/to/pentest-mcp/dist/index.js",
"maxTokens": 8192,
"maxTotalTokens": 131072,
"maxIterations": 16,
"maxToolCalls": 24,
"maxCallsPerTool": 16,
"maxConsecutiveToolErrors": 3,
"maxTotalToolErrors": 6,
"toolResultMaxBytes": 65536,
"hostileModelProtection": false
}
]
}workingDirectory and mcpDataDir must be private, separate directories outside the Pentest MCP checkout and builder home. runAgentAssessment creates a new private child under mcpDataDir for each run.
OpenRouter profiles
Set the key in the Pentest MCP server environment:
export OPENROUTER_API_KEY='operator-provider-key'Report-only profile:
{
"id": "openrouter-report",
"kind": "openrouter",
"mode": "draft",
"model": "provider/exact-model-id",
"maxTokens": 8192
}Tool-using profile:
{
"id": "openrouter-guarded-assessor",
"kind": "openrouter",
"mode": "assessment",
"isolation": "guarded-mcp",
"model": "provider/exact-model-id",
"workingDirectory": "/private/empty/mcp-worker-cwd",
"mcpDataDir": "/private/pentest-worker-data",
"mcpNodeCommand": "/absolute/path/to/node",
"mcpServerEntry": "/absolute/path/to/pentest-mcp/dist/index.js",
"maxIterations": 16,
"maxTotalTokens": 131072,
"maxToolCalls": 24,
"maxCallsPerTool": 16,
"maxConsecutiveToolErrors": 3,
"maxTotalToolErrors": 6,
"toolResultMaxBytes": 65536,
"hostileModelProtection": false
}Use modelDataPolicy: "approved-provider" in the assessment plan. External-client work also requires providerClientApprovalReference. Confirm the client's data-processing terms and the selected provider's current retention, routing, and training settings before sending client data. Pentest MCP does not claim that OpenRouter provides zero retention.
Every OpenRouter request sets provider.data_collection: "deny", provider.zdr: true, provider.require_parameters: true, and provider.allow_fallbacks: false. This restricts routing to endpoints advertising the requested privacy and parameter behavior and prevents an automatic retry through another provider. These request controls do not replace the operator's review of OpenRouter account settings, metadata handling, model licensing, or client terms.
Enforced assessment boundary
At run start the host computes the exact intersection of:
- names in the saved plan's
allowedToolNames; - tools registered by the locked, single-engagement worker.
Only that intersection is sent as OpenAI function definitions. A model may still hallucinate another function name; the host records guardedModelToolDenied and terminates before an MCP call. Every permitted call then enters the normal Pentest MCP worker, where the SDK schema, plan allowlist, authoritative parent status, engagement time window, target scope, command bounds, and evidence logging are checked again.
The provider client has a fixed configured endpoint. OpenRouter credentials are sent only to https://openrouter.ai; a custom remote provider or proxy must use an openai-compatible profile and its separate credential. A loopback OpenRouter test proxy uses only OPENROUTER_PROXY_API_KEY. Model text cannot change the URL, headers, credential, model, MCP command, worker directory, engagement ID, parent registry, or allowed tool set. Tool output is untrusted target data and is truncated with its original byte count and SHA-256 digest before it returns to the model.
Optional hostile-model protection
hostileModelProtection defaults to false. Enable it per assessment profile when using an abliterated, refusal-resistant, experimental, or otherwise untrusted model:
{
"hostileModelProtection": true
}When enabled, Pentest MCP uses the greater of provider-reported and locally estimated token usage, rejects unknown finish reasons, and rejects a successful tool plus canonical argument set if the model requests it again. The worker enforces the same duplicate-action rule for plan-gated assessment tools. This setting is included in agentAssessmentStarted and guardedModelRunStarted receipts.
When disabled, intentional repeats are allowed within the configured call and iteration budgets, provider token counts are used when available, and unfamiliar provider finish reasons do not stop an otherwise well-formed response. Known incomplete terminal states such as length, content_filter, and error are still rejected.
The setting never disables authorization controls. Exact tool allowlists, engagement and target scope, parent status, testing windows, process deadlines, evidence logging, context quarantine, and report verification remain enforced for every assessment model.
Stop conditions
The guarded loop stops when any of these occurs:
- the operator cancels the run, the deadline passes, or the parent engagement closes;
- the provider reports a model ID different from the configured model;
- a tool is requested outside the exact allowlist;
- a tool-call ID is reused, arguments are not a JSON object, or the response is malformed;
- the total token, context-size, tool-call, per-tool call, iteration, consecutive-error, or total-error limit is reached;
- with
hostileModelProtection: true, the same successful tool and canonical argument set is requested again or the provider returns an unknown finish reason; - a tool request exceeds its time boundary; oversized results are replaced with a bounded prefix, original byte count, and SHA-256 digest;
- the provider returns neither a final response nor tool calls;
- the MCP inventory no longer contains a planned tool;
- an audit receipt cannot be written.
Limits are host counters. The model cannot change or reset them. Engagement checks remain active even if the client loop has a defect; duplicate suppression is an optional hostile-model heuristic.
Audit trail
The worker stores and later imports these guarded-loop records:
guardedModelRunStarted: profile/provider, requested model, endpoint origin, prompt and system-prompt digests, effective tool names, tool-schema digest, and all limits;guardedModelTurn: iteration, request-message digest and size, requested and reported model, finish reason, token usage when supplied, response/content digests, and every requested tool name with argument and call-ID digests;guardedModelToolDenied: rejected tool, digest, iteration, and fixed denial reason;guardedModelRunEnded: fixed stop reason, outcome, elapsed time, iterations, calls per tool, and error counters;- the existing
mcpCallStartedandmcpCallCompletedreceipts for every call that reaches MCP; - the normal tool evidence records, output digests, scope decisions, and parent import map.
Large evidence remains in the private worker store. Audit records use hashes rather than API keys, raw prompts, raw arguments, or complete provider responses.
Operating system isolation for hostile local models
Treat the model server binary, model weights, templates, and startup arguments as supply-chain inputs. Pin and review them. For a container deployment:
- use a reviewed image pinned by digest;
- run as a non-root UID with a read-only root filesystem;
- drop all Linux capabilities and enable
no-new-privileges; - mount model weights read-only and only a bounded scratch
tmpfswritable; - publish the API only on
127.0.0.1; - do not mount the repository, operator home, evidence directory, Docker socket, SSH agent, cloud credentials, API keys, or other client data;
- set CPU, memory, process, and file-size limits;
- restrict outbound network access when the runtime does not need it;
- keep the Pentest MCP worker and evidence store outside the model container.
A dedicated VM provides a stronger boundary when the model runtime, custom kernels, templates, or plugins are not trusted. Snapshot it, expose only the loopback or explicitly firewalled API route, and discard it after the engagement.
Pentest MCP does not attempt to detect whether a model is abliterated. Enhanced hostile-model heuristics are explicit and opt in because ordinary models and provider variants can produce legitimate repeats or unfamiliar finish reasons. No configuration makes a malicious model-serving process safe when that process already has broad host permissions.
Report quarantine
Use a separate mode: "draft" profile for report generation. The report writer receives a bounded evidence packet and no assessment MCP connection. It cannot call scanners to fill gaps. The assessor's final text is retained as unverified model output and is not inserted as a verified finding. publishAgentReport still requires analyst-selected evidence, verifier attribution, and explicit finding content.