Affected Component
External Integrations (LLM/Search APIs)
Describe the bug
Summary
DOCKER_DEFAULT_IMAGE_FOR_PENTEST (and the equivalent DefaultImageForPentest template variable) is documented and configured as if it constrains which Docker image PentAGI uses for a flow. In practice, it is only ever injected as text into an LLM prompt (image_chooser prompt type). The LLM's raw text response is used directly as the image name, with no validation, no allow-list check, and no deterministic fallback if the model ignores the guidance. With a weaker/smaller model (tested with a local, self-hosted qwen2.5:7b-instruct via Ollama), the model reliably ignores the "prefer the pentest image" instruction and returns node:latest instead of the intended pentest image, even when the env var is correctly set and even after explicitly strengthening the prompt's wording.
This means DOCKER_DEFAULT_IMAGE_FOR_PENTEST currently functions as a hint, not a default or constraint, contrary to what its name and documentation imply.
Environment
- PentAGI commit:
879e87c (pinned, built from clean upstream source — see reproduction steps below)
- Deployment: self-hosted Docker Compose, Ubuntu 26.04 host
- LLM provider: local Ollama (
qwen2.5:7b-instruct, ~5.1GB, CPU-only inference)
DOCKER_DEFAULT_IMAGE_FOR_PENTEST=vxcontrol/kali-linux (also reproduces with the unset/default value, which per config.go defaults to the same image)
Steps to reproduce
- Configure a local/self-hosted LLM provider (in our case, Ollama running
qwen2.5:7b-instruct) as the active provider for a flow.
- Submit a flow whose task description could plausibly read as either a general/web task or a security-testing task — e.g. "Inspect Juice-Shop API CORS configuration."
- Observe the resulting flow's terminal container image via
docker ps or the Terminals column in the Flows UI.
Expected: the terminal container uses the configured pentest image (vxcontrol/kali-linux), consistent with DOCKER_DEFAULT_IMAGE_FOR_PENTEST.
Actual: the terminal container uses node:latest.
This was reproduced identically across two separate flows, submitted hours apart, on two different Docker images (see root-cause investigation below) — ruling out a one-off model hallucination.
Root cause (traced in source, not assumed)
In backend/pkg/providers/providers.go, function NewFlowProvider:
imageTmpl, err := prompter.RenderTemplate(templates.PromptTypeImageChooser, map[string]any{
"DefaultImage": pc.docker.GetDefaultImage(),
"DefaultImageForPentest": pc.defaultDockerImageForPentest,
"Input": input,
})
if err != nil {
return nil, fmt.Errorf("failed to get primary docker image template: %w", err)
}
image, err := prv.Call(ctx, pconfig.OptionsTypeSimple, imageTmpl)
if err != nil {
return nil, fmt.Errorf("failed to select primary docker image via llm call: %w", err)
}
image = strings.ToLower(strings.TrimSpace(image))
DOCKER_DEFAULT_IMAGE_FOR_PENTEST (surfaced here as pc.defaultDockerImageForPentest, sourced from config.go's DockerDefaultImageForPentest field) is used only as a template substitution value inside the image_chooser prompt (backend/pkg/templates/prompts/image_chooser.tmpl). The LLM's raw text response becomes the literal image string used for the flow's terminal container, with:
- No check that the returned string is one of the two configured images (
DefaultImage / DefaultImageForPentest) or any known-good value at all.
- No fallback if the LLM's answer doesn't match anything sensible.
- No way for a deployment to force deterministic behavior short of forking this function.
We initially suspected this was caused by a locally-built, unsourced/custom Docker image running our deployment (built from an unknown commit, with undocumented modifications). To rule this out, we:
- Cloned upstream
vxcontrol/pentagi fresh, checked out pinned commit 879e87c.
- Applied a documented, minimal prompt-strengthening edit to
image_chooser.tmpl (changing "for ambiguous or uncertain cases, use {{.DefaultImage}}" to "for ambiguous or uncertain cases, prefer {{.DefaultImageForPentest}} over {{.DefaultImage}}, since tasks on this platform are security-testing in nature").
- Built a clean image from this source (
docker build, no errors, verified the compiled binary contains the exact patched prompt string via strings /opt/pentagi/bin/pentagi | grep ...).
- Swapped the deployment onto this clean, verified image.
- Re-ran an equivalent flow.
Result: identical failure. node:latest was selected again, even with a stronger, verified, correctly-compiled prompt nudge in place. This confirms the bug is not caused by any custom/unsourced build — it's inherent to relying on unconstrained LLM output for a decision that has real operational consequences (a security-testing task running in a general-purpose image without the intended toolset).
Why this matters
For a platform whose stated purpose is security/pentest automation, the terminal container's toolset is not cosmetic — an agent that lands in node:latest instead of a pentest-tooling image cannot perform the intended work correctly, and the failure is silent: no error is raised, no warning is logged, the flow simply proceeds in the wrong environment. This is especially significant for deployments using smaller/local/self-hosted models (increasingly common given cost and data-locality motivations), where prompt-following reliability is measurably weaker than with frontier hosted models — exactly the population most likely to hit this bug in practice.
Suggested fix
DOCKER_DEFAULT_IMAGE_FOR_PENTEST should be enforced deterministically, not merely suggested. Two options, in order of preference:
- Skip the LLM call when the deployment's use case is unambiguous. Many deployments (like ours) are exclusively for security testing — for these, add a config option (e.g.
DOCKER_IMAGE_SELECTION_MODE=fixed vs. llm) that, when set, bypasses prv.Call(...) entirely and uses DefaultImageForPentest directly.
- At minimum, validate the LLM's response before trusting it. After receiving
image back from the model, check it against a small allow-list (DefaultImage, DefaultImageForPentest, or any explicitly configured additional images) and fall back to DefaultImageForPentest if the response doesn't match a known-good value. This preserves the "let the LLM decide when it's genuinely ambiguous" intent while eliminating the silent-wrong-environment failure mode.
Option 2 is likely the smaller, safer change and would meaningfully improve robustness without removing the flexibility the current design intends.
Additional notes
- We can provide full logs, the exact prompt template diff, and the flow IDs/timestamps from our reproduction if helpful.
- Happy to test a patch against our own deployment if one is proposed.
Steps to Reproduce
- Access PentAGI Web UI at https://:8443/ (self-hosted deployment)
- Configure an LLM provider using a local/self-hosted model (reproduced with Ollama running qwen2.5:7b-instruct, CPU-only). Confirm DOCKER_DEFAULT_IMAGE_FOR_PENTEST is set (we used the default, vxcontrol/kali-linux) in the deployment's .env.
- Start a new flow with a prompt describing a security-testing task that could also read as a general/web task, e.g. "Inspect Juice-Shop API CORS configuration."
- Observe the flow's assigned terminal container image via the Flows UI "Terminals" column, or docker ps --filter name=pentagi-terminal-
- Error occurs when the flow's terminal container is created: instead of the configured pentest image (vxcontrol/kali-linux), the container uses node:latest. No error, warning, or log entry is raised — the flow proceeds silently in the wrong environment. Reproduced identically across two separate flow submissions, hours apart.
System Configuration
PentAGI Version: custom build from pinned upstream commit 879e87c (not Docker Hub — built via docker build from a clean vxcontrol/pentagi checkout, with one documented prompt-template patch applied to image_chooser.tmpl — see reproduction steps)
Deployment Type:
Environment:
- Docker Version: 29.5.3 (build d1c06ef)
- Docker Compose Version: v5.1.4
- Host OS: Ubuntu 26.04
- Available Resources:
- RAM: 7.2GB
- CPU: 4 cores
- Disk Space: 56GB free
Enabled Features:
Active Integrations:
- LLM Provider: Custom (self-hosted Ollama — qwen2.5:7b-instruct)
- Search Systems: none configured (DuckDuckGo/Google/Traversaal/Tavily/Perplexity all unset)
Logs and Artifacts
No response
Screenshots or Recordings
No response
Verification
Affected Component
External Integrations (LLM/Search APIs)
Describe the bug
Summary
DOCKER_DEFAULT_IMAGE_FOR_PENTEST(and the equivalentDefaultImageForPentesttemplate variable) is documented and configured as if it constrains which Docker image PentAGI uses for a flow. In practice, it is only ever injected as text into an LLM prompt (image_chooserprompt type). The LLM's raw text response is used directly as the image name, with no validation, no allow-list check, and no deterministic fallback if the model ignores the guidance. With a weaker/smaller model (tested with a local, self-hostedqwen2.5:7b-instructvia Ollama), the model reliably ignores the "prefer the pentest image" instruction and returnsnode:latestinstead of the intended pentest image, even when the env var is correctly set and even after explicitly strengthening the prompt's wording.This means
DOCKER_DEFAULT_IMAGE_FOR_PENTESTcurrently functions as a hint, not a default or constraint, contrary to what its name and documentation imply.Environment
879e87c(pinned, built from clean upstream source — see reproduction steps below)qwen2.5:7b-instruct, ~5.1GB, CPU-only inference)DOCKER_DEFAULT_IMAGE_FOR_PENTEST=vxcontrol/kali-linux(also reproduces with the unset/default value, which perconfig.godefaults to the same image)Steps to reproduce
qwen2.5:7b-instruct) as the active provider for a flow.docker psor the Terminals column in the Flows UI.Expected: the terminal container uses the configured pentest image (
vxcontrol/kali-linux), consistent withDOCKER_DEFAULT_IMAGE_FOR_PENTEST.Actual: the terminal container uses
node:latest.This was reproduced identically across two separate flows, submitted hours apart, on two different Docker images (see root-cause investigation below) — ruling out a one-off model hallucination.
Root cause (traced in source, not assumed)
In
backend/pkg/providers/providers.go, functionNewFlowProvider:DOCKER_DEFAULT_IMAGE_FOR_PENTEST(surfaced here aspc.defaultDockerImageForPentest, sourced fromconfig.go'sDockerDefaultImageForPentestfield) is used only as a template substitution value inside theimage_chooserprompt (backend/pkg/templates/prompts/image_chooser.tmpl). The LLM's raw text response becomes the literal image string used for the flow's terminal container, with:DefaultImage/DefaultImageForPentest) or any known-good value at all.We initially suspected this was caused by a locally-built, unsourced/custom Docker image running our deployment (built from an unknown commit, with undocumented modifications). To rule this out, we:
vxcontrol/pentagifresh, checked out pinned commit879e87c.image_chooser.tmpl(changing "for ambiguous or uncertain cases, use{{.DefaultImage}}" to "for ambiguous or uncertain cases, prefer{{.DefaultImageForPentest}}over{{.DefaultImage}}, since tasks on this platform are security-testing in nature").docker build, no errors, verified the compiled binary contains the exact patched prompt string viastrings /opt/pentagi/bin/pentagi | grep ...).Result: identical failure.
node:latestwas selected again, even with a stronger, verified, correctly-compiled prompt nudge in place. This confirms the bug is not caused by any custom/unsourced build — it's inherent to relying on unconstrained LLM output for a decision that has real operational consequences (a security-testing task running in a general-purpose image without the intended toolset).Why this matters
For a platform whose stated purpose is security/pentest automation, the terminal container's toolset is not cosmetic — an agent that lands in
node:latestinstead of a pentest-tooling image cannot perform the intended work correctly, and the failure is silent: no error is raised, no warning is logged, the flow simply proceeds in the wrong environment. This is especially significant for deployments using smaller/local/self-hosted models (increasingly common given cost and data-locality motivations), where prompt-following reliability is measurably weaker than with frontier hosted models — exactly the population most likely to hit this bug in practice.Suggested fix
DOCKER_DEFAULT_IMAGE_FOR_PENTESTshould be enforced deterministically, not merely suggested. Two options, in order of preference:DOCKER_IMAGE_SELECTION_MODE=fixedvs.llm) that, when set, bypassesprv.Call(...)entirely and usesDefaultImageForPentestdirectly.imageback from the model, check it against a small allow-list (DefaultImage,DefaultImageForPentest, or any explicitly configured additional images) and fall back toDefaultImageForPentestif the response doesn't match a known-good value. This preserves the "let the LLM decide when it's genuinely ambiguous" intent while eliminating the silent-wrong-environment failure mode.Option 2 is likely the smaller, safer change and would meaningfully improve robustness without removing the flexibility the current design intends.
Additional notes
Steps to Reproduce
System Configuration
PentAGI Version: custom build from pinned upstream commit 879e87c (not Docker Hub — built via
docker buildfrom a clean vxcontrol/pentagi checkout, with one documented prompt-template patch applied to image_chooser.tmpl — see reproduction steps)Deployment Type:
Environment:
Enabled Features:
Active Integrations:
Logs and Artifacts
No response
Screenshots or Recordings
No response
Verification