Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/docs/agents/ollama.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,20 @@ agent = HackAgent(
)
```

### Non-Default Port

If your Ollama server does not listen on the default port `11434`, set one of
`OLLAMA_BASE_URL`, `OLLAMA_API_BASE` or `OLLAMA_HOST` (checked in that order).
HackAgent reads them when building the default endpoint for attacker, judge,
classifier and embedder models, so you don't have to override each one:

```bash
export OLLAMA_HOST="127.0.0.1:11435"
```

A value without a scheme defaults to `http://`, and a value without a port
falls back to `11434`.
Comment on lines +233 to +234

## Further Reading

- [Ollama Documentation](https://ollama.com/docs)
Expand Down
5 changes: 3 additions & 2 deletions hackagent/attacks/techniques/advprefix/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
DEFAULT_ATTACKER_IDENTIFIER,
DEFAULT_FILTER_LEN,
DEFAULT_JUDGE_IDENTIFIER,
DEFAULT_LOCAL_MODEL_ENDPOINT,
DEFAULT_OUTPUT_DIR,
DEFAULT_TIMEOUT,
DEFAULT_RUN_ID,
Expand Down Expand Up @@ -52,7 +53,7 @@
# --- Model Configurations ---
"generator": {
"identifier": DEFAULT_ATTACKER_IDENTIFIER,
"endpoint": "http://localhost:11434",
"endpoint": DEFAULT_LOCAL_MODEL_ENDPOINT,
"system_prompt": DEFAULT_ADVPREFIX_GENERATOR_SYSTEM_PROMPT,
"max_tokens": 50,
"guided_topk": 50,
Expand All @@ -61,7 +62,7 @@
"judges": [
{
"identifier": DEFAULT_JUDGE_IDENTIFIER,
"endpoint": "http://localhost:11434",
"endpoint": DEFAULT_LOCAL_MODEL_ENDPOINT,
"type": "harmbench",
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from hackagent.attacks.techniques.base import BaseAttack
from hackagent.attacks.shared.router_factory import create_router
from hackagent.attacks.shared.response_utils import extract_response_content
from hackagent.config import DEFAULT_EMBEDDER_OPENAI_ENDPOINT
from hackagent.router.router import AgentRouter
from hackagent.router.tracking.tracker import Tracker
from hackagent.server.client import AuthenticatedClient
Expand Down Expand Up @@ -229,7 +230,7 @@ def get_embeddings(
# Fall back to a placeholder so keyless local backends (e.g. Ollama) work:
# the OpenAI client requires a non-empty api_key, but local servers ignore it.
api_key = config.get("api_key") or os.environ.get("OPENAI_API_KEY") or "not-needed"
raw_endpoint = str(config.get("endpoint", "http://localhost:11434/v1")).strip()
raw_endpoint = str(config.get("endpoint", DEFAULT_EMBEDDER_OPENAI_ENDPOINT)).strip()
endpoint = raw_endpoint.rstrip("/")
if endpoint.lower().endswith("/embeddings"):
# OpenAI client expects API base and appends '/embeddings' internally.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pydantic import Field

from hackagent.attacks.techniques.config import ConfigBase, DEFAULT_CONFIG_BASE
from hackagent.config import DEFAULT_EMBEDDER_OPENAI_ENDPOINT


DEFAULT_INDIRECT_PROMPT_INJECTION_CONFIG: Dict[str, Any] = {
Expand Down Expand Up @@ -39,7 +40,7 @@
},
"embedder": {
"identifier": "nomic-embed-text",
"endpoint": "http://localhost:11434/v1",
"endpoint": DEFAULT_EMBEDDER_OPENAI_ENDPOINT,
"api_key": None,
},
},
Expand Down
7 changes: 5 additions & 2 deletions hackagent/attacks/techniques/pair/attack.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from hackagent.attacks.techniques.config import (
DEFAULT_ATTACKER_IDENTIFIER,
DEFAULT_JUDGE_IDENTIFIER,
DEFAULT_LOCAL_MODEL_ENDPOINT,
)
from hackagent.attacks.objectives import OBJECTIVES
from hackagent.attacks.shared.progress import create_progress_bar
Expand Down Expand Up @@ -285,7 +286,9 @@ def _initialize_attacker_router(self) -> Optional[AgentRouter]:
"identifier": attacker_config.get(
"identifier", DEFAULT_ATTACKER_IDENTIFIER
),
"endpoint": attacker_config.get("endpoint", "http://localhost:11434"),
"endpoint": attacker_config.get(
"endpoint", DEFAULT_LOCAL_MODEL_ENDPOINT
),
"agent_type": attacker_config.get("agent_type", "OLLAMA"),
"thinking": attacker_config.get("thinking"),
"max_tokens": attacker_config.get("max_tokens", 500),
Expand Down Expand Up @@ -334,7 +337,7 @@ def _initialize_judge_router(self) -> Optional[AgentRouter]:

router_config = {
"identifier": judge_config.get("identifier", DEFAULT_JUDGE_IDENTIFIER),
"endpoint": judge_config.get("endpoint", "http://localhost:11434"),
"endpoint": judge_config.get("endpoint", DEFAULT_LOCAL_MODEL_ENDPOINT),
"agent_type": judge_config.get("agent_type", "OLLAMA"),
"thinking": judge_config.get("thinking"),
"max_tokens": judge_config.get("max_tokens", 4096),
Expand Down
5 changes: 3 additions & 2 deletions hackagent/cli/commands/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

from hackagent.cli.config import CLIConfig
from hackagent.cli.utils import handle_errors
from hackagent.config import resolve_ollama_base_url

console = Console()

Expand Down Expand Up @@ -309,7 +310,7 @@ def _preflight_ollama_requirements(demo_cfg: dict) -> None:
)
raise click.ClickException("Ollama is not installed")

endpoint = demo_cfg.get("agent", {}).get("endpoint") or "http://localhost:11434"
endpoint = demo_cfg.get("agent", {}).get("endpoint") or resolve_ollama_base_url()
console.print(f"[cyan]🔎 Checking Ollama server at:[/cyan] {endpoint}")

if not _is_ollama_running(str(endpoint)):
Expand Down Expand Up @@ -504,7 +505,7 @@ def web_example():
"identifier": "gemma3:4b",
"type": "harmbench_variant",
"agent_type": "ollama",
"endpoint": "http://localhost:11434",
"endpoint": resolve_ollama_base_url(),
}
],
}
Expand Down
9 changes: 5 additions & 4 deletions hackagent/cli/commands/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

from __future__ import annotations

import os
import time
from typing import Any, Dict, List, Optional, Tuple

Expand All @@ -36,6 +35,7 @@
handle_errors,
load_config_file,
)
from hackagent.config import resolve_ollama_base_url
from hackagent.router.discovery.scanner import (
DEFAULT_PLANNER_MODEL,
PlannerError,
Expand Down Expand Up @@ -88,9 +88,10 @@ def _provider_endpoint(model: str) -> str:
"""Return the api_base URL for a LiteLLM ``model`` id (by provider prefix)."""
m = (model or "").strip()
prefix = m.split("/", 1)[0].lower() if "/" in m else ""
if prefix in ("ollama", "ollama_chat") or not prefix:
return os.environ.get("OLLAMA_API_BASE") or "http://localhost:11434"
return _PROVIDER_ENDPOINTS.get(prefix, "http://localhost:11434")
endpoint = _PROVIDER_ENDPOINTS.get(prefix)
if prefix in ("ollama", "ollama_chat") or endpoint is None:
return resolve_ollama_base_url()
return endpoint


def _extract_asr(results: Any) -> Optional[float]:
Expand Down
61 changes: 58 additions & 3 deletions hackagent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,54 @@

from __future__ import annotations

import os

# ---------------------------------------------------------------------------
# Local Ollama defaults (no API key required)
# ---------------------------------------------------------------------------

# Environment variables consulted (in order) to locate the local Ollama server
# before falling back to the upstream default host/port. ``OLLAMA_HOST`` is the
# variable Ollama itself honours, so a user who moved the server off 11434 gets
# picked up automatically.
OLLAMA_BASE_URL_ENV_VARS = ("OLLAMA_BASE_URL", "OLLAMA_API_BASE", "OLLAMA_HOST")

DEFAULT_OLLAMA_HOST = "localhost"
DEFAULT_OLLAMA_PORT = "11434"


def _normalize_ollama_base_url(raw: str) -> str:
"""Normalise an ``OLLAMA_HOST``-style value into a full base URL.

Accepts ``http://host:port``, ``host:port``, ``host`` and ``:port`` forms.
A missing scheme defaults to ``http``, a missing host to ``localhost`` and a
missing port to Ollama's default port (``https`` values keep the implicit
443 instead).
"""
value = raw.strip().rstrip("/")
scheme, sep, remainder = value.partition("://")
if not sep:
scheme, remainder = "http", value
remainder = remainder.lstrip("/")
authority, slash, path = remainder.partition("/")
if authority.startswith(":"):
authority = f"{DEFAULT_OLLAMA_HOST}{authority}"
# Only append the default port for a bare http host (no port, not IPv6).
# https values are left alone so they keep the implicit 443.
if scheme == "http" and ":" not in authority and not authority.endswith("]"):
authority = f"{authority}:{DEFAULT_OLLAMA_PORT}"
Comment on lines +52 to +55
return f"{scheme}://{authority}{slash}{path}"


def resolve_ollama_base_url() -> str:
"""Return the local Ollama base URL, honouring environment overrides."""
for env_var in OLLAMA_BASE_URL_ENV_VARS:
raw = os.environ.get(env_var, "").strip()
if raw:
return _normalize_ollama_base_url(raw)
return f"http://{DEFAULT_OLLAMA_HOST}:{DEFAULT_OLLAMA_PORT}"


# Local Ollama default model. Uncensored so it won't refuse to generate
# red-team prompts. Pull: ``ollama pull huihui_ai/gemma-4-abliterated:12b``.
DEFAULT_LOCAL_MODEL = "huihui_ai/gemma-4-abliterated:12b"
Expand All @@ -32,11 +76,11 @@
# Default local embedder served by Ollama (used by any attack that needs an
# embedder, e.g. the RAG Attack and AutoDAN-Turbo strategy retrieval).
DEFAULT_EMBEDDER_IDENTIFIER = "embeddinggemma"
DEFAULT_EMBEDDER_ENDPOINT = "http://localhost:11434"
DEFAULT_EMBEDDER_ENDPOINT = resolve_ollama_base_url()
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
DEFAULT_EMBEDDER_AGENT_TYPE = "OLLAMA"
# OpenAI-compatible base URL exposed by Ollama (used by the RAG Attack, which
# embeds through an OpenAI-compatible client and posts to ``/v1/embeddings``).
DEFAULT_EMBEDDER_OPENAI_ENDPOINT = "http://localhost:11434/v1"
DEFAULT_EMBEDDER_OPENAI_ENDPOINT = f"{resolve_ollama_base_url()}/v1"
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
# Ollama ignores the key but the OpenAI client requires a non-empty value.
DEFAULT_EMBEDDER_OPENAI_API_KEY = "ollama"

Expand All @@ -45,7 +89,7 @@
# form; callers that split identifier/endpoint/agent_type want DEFAULT_LOCAL_MODEL.
DEFAULT_LOCAL_LITELLM_MODEL = f"{OLLAMA_PROVIDER_PREFIX}/{DEFAULT_LOCAL_MODEL}"

DEFAULT_LOCAL_MODEL_ENDPOINT = "http://localhost:11434"
DEFAULT_LOCAL_MODEL_ENDPOINT = resolve_ollama_base_url()
DEFAULT_LOCAL_AGENT_TYPE = "OLLAMA"

# Local role identifiers — attacker / judge / category-classifier all default
Expand All @@ -69,12 +113,23 @@
DEFAULT_REMOTE_JUDGE_IDENTIFIER = "hackagent-judge"

__all__ = [
# ollama endpoint resolution
"OLLAMA_BASE_URL_ENV_VARS",
"DEFAULT_OLLAMA_HOST",
"DEFAULT_OLLAMA_PORT",
"resolve_ollama_base_url",
# local model
"DEFAULT_LOCAL_MODEL",
"OLLAMA_PROVIDER_PREFIX",
"DEFAULT_LOCAL_LITELLM_MODEL",
"DEFAULT_LOCAL_MODEL_ENDPOINT",
"DEFAULT_LOCAL_AGENT_TYPE",
# local embedder
"DEFAULT_EMBEDDER_IDENTIFIER",
"DEFAULT_EMBEDDER_ENDPOINT",
"DEFAULT_EMBEDDER_AGENT_TYPE",
"DEFAULT_EMBEDDER_OPENAI_ENDPOINT",
"DEFAULT_EMBEDDER_OPENAI_API_KEY",
# local roles
"DEFAULT_ATTACKER_IDENTIFIER",
"DEFAULT_JUDGE_IDENTIFIER",
Expand Down
5 changes: 2 additions & 3 deletions hackagent/router/_chat_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import os
from typing import Any, Dict, Optional

from hackagent.config import resolve_ollama_base_url
from hackagent.logger import get_logger
from hackagent.router import envelope as _envelope
from hackagent.router.provider_config import ProviderConfig
Expand All @@ -35,12 +36,10 @@
# These helpers cover the small adapter-class quirks that used to live
# in ``OpenAIAgent.__init__`` and ``OllamaAgent.__init__``.

_OLLAMA_DEFAULT_ENDPOINT = "http://localhost:11434"


def _normalise_ollama_endpoint(endpoint: Optional[str]) -> str:
"""Resolve & normalise the Ollama endpoint URL the way OllamaAgent did."""
resolved = endpoint or os.environ.get("OLLAMA_BASE_URL", _OLLAMA_DEFAULT_ENDPOINT)
resolved = endpoint or resolve_ollama_base_url()
resolved = resolved.rstrip("/")
for suffix in ("/api/generate", "/api/chat", "/api/tags", "/api/show", "/api"):
if resolved.endswith(suffix):
Expand Down
3 changes: 2 additions & 1 deletion hackagent/router/tracking/category_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,12 @@ def _resolve_config(config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
# Imported lazily to avoid a router↔attacks import cycle at load time.
from hackagent.attacks.techniques.config import (
DEFAULT_CATEGORY_CLASSIFIER_IDENTIFIER,
DEFAULT_CATEGORY_CLASSIFIER_ENDPOINT,
)

resolved: Dict[str, Any] = {
"identifier": DEFAULT_CATEGORY_CLASSIFIER_IDENTIFIER,
"endpoint": "http://localhost:11434",
"endpoint": DEFAULT_CATEGORY_CLASSIFIER_ENDPOINT,
"agent_type": "OLLAMA",
"api_key": None,
"max_tokens": 100,
Expand Down
69 changes: 69 additions & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Copyright 2026 - AI4I. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Unit tests for ``hackagent/config.py`` Ollama endpoint resolution."""

import os
import unittest
from unittest.mock import patch

from hackagent.config import resolve_ollama_base_url


class TestResolveOllamaBaseUrl(unittest.TestCase):
def _resolve(self, env):
with patch.dict(os.environ, env, clear=True):
return resolve_ollama_base_url()

def test_defaults_to_localhost_11434(self):
self.assertEqual(self._resolve({}), "http://localhost:11434")

def test_ollama_base_url_wins(self):
self.assertEqual(
self._resolve({"OLLAMA_BASE_URL": "http://ollama:11435"}),
"http://ollama:11435",
)

def test_ollama_api_base_is_used(self):
self.assertEqual(
self._resolve({"OLLAMA_API_BASE": "http://localhost:11500"}),
"http://localhost:11500",
)

def test_ollama_host_without_scheme(self):
self.assertEqual(
self._resolve({"OLLAMA_HOST": "127.0.0.1:11435"}),
"http://127.0.0.1:11435",
)

def test_ollama_host_port_only(self):
self.assertEqual(
self._resolve({"OLLAMA_HOST": ":11435"}), "http://localhost:11435"
)

def test_ollama_host_without_port_gets_default_port(self):
self.assertEqual(
self._resolve({"OLLAMA_HOST": "my-ollama"}), "http://my-ollama:11434"
)

def test_trailing_slash_is_stripped(self):
self.assertEqual(
self._resolve({"OLLAMA_BASE_URL": "http://localhost:11435/"}),
"http://localhost:11435",
)

def test_blank_env_var_falls_through(self):
self.assertEqual(
self._resolve({"OLLAMA_BASE_URL": " ", "OLLAMA_HOST": "host:11499"}),
"http://host:11499",
)

def test_https_scheme_keeps_implicit_port(self):
self.assertEqual(
self._resolve({"OLLAMA_HOST": "https://remote.example.com"}),
"https://remote.example.com",
)


if __name__ == "__main__":
unittest.main()