humanbound-firewall 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- humanbound_firewall/__init__.py +97 -0
- humanbound_firewall/cache.py +51 -0
- humanbound_firewall/config.py +42 -0
- humanbound_firewall/firewall.py +565 -0
- humanbound_firewall/firewall_judge.py +75 -0
- humanbound_firewall/hbfw.py +566 -0
- humanbound_firewall/judge.py +84 -0
- humanbound_firewall/llm/__init__.py +59 -0
- humanbound_firewall/llm/azureopenai.py +116 -0
- humanbound_firewall/llm/claude.py +102 -0
- humanbound_firewall/llm/gemini.py +97 -0
- humanbound_firewall/llm/models.py +26 -0
- humanbound_firewall/llm/openai.py +111 -0
- humanbound_firewall/metrics.py +116 -0
- humanbound_firewall/models.py +92 -0
- humanbound_firewall/prompts/judge.txt +106 -0
- humanbound_firewall/py.typed +0 -0
- humanbound_firewall-0.2.0.dist-info/METADATA +224 -0
- humanbound_firewall-0.2.0.dist-info/RECORD +22 -0
- humanbound_firewall-0.2.0.dist-info/WHEEL +5 -0
- humanbound_firewall-0.2.0.dist-info/licenses/LICENSE +193 -0
- humanbound_firewall-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2024-2026 Humanbound
|
|
3
|
+
"""humanbound-firewall: Multi-tier firewall for AI agents.
|
|
4
|
+
|
|
5
|
+
Tier 0: Input sanitization
|
|
6
|
+
Tier 1: Basic attack detection (pre-trained, single-turn)
|
|
7
|
+
Tier 2: Agent-specific classification (trained, multi-turn, 3+ turns)
|
|
8
|
+
Tier 3: LLM-as-a-judge (handles uncertain cases)
|
|
9
|
+
|
|
10
|
+
Designed for low cold-import cost: importing this module loads only stdlib and
|
|
11
|
+
pydantic-adjacent dependencies. Heavy components (Firewall, HBFW, LLM clients)
|
|
12
|
+
are loaded lazily on first attribute access.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
__version__ = "0.2.0"
|
|
21
|
+
|
|
22
|
+
from .models import VERDICT_MAP, AgentConfig, Category, EvalResult, Turn, Verdict
|
|
23
|
+
|
|
24
|
+
_LAZY_ATTRS = {
|
|
25
|
+
"Firewall": ".firewall",
|
|
26
|
+
"AttackDetector": ".firewall",
|
|
27
|
+
"AttackDetectorEnsemble": ".firewall",
|
|
28
|
+
"Provider": ".llm",
|
|
29
|
+
"ProviderIntegration": ".llm",
|
|
30
|
+
"ProviderName": ".llm",
|
|
31
|
+
"get_llm_pinger": ".llm",
|
|
32
|
+
"get_llm_streamer": ".llm",
|
|
33
|
+
"HBFW": ".hbfw",
|
|
34
|
+
"load_model_class": ".hbfw",
|
|
35
|
+
"save_hbfw": ".hbfw",
|
|
36
|
+
"load_hbfw": ".hbfw",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __getattr__(name: str):
|
|
41
|
+
"""Lazy attribute loader (PEP 562).
|
|
42
|
+
|
|
43
|
+
Defers importing heavy submodules until first attribute access so that
|
|
44
|
+
bare `import humanbound_firewall` stays fast (< 200 ms target).
|
|
45
|
+
"""
|
|
46
|
+
if name in _LAZY_ATTRS:
|
|
47
|
+
import importlib
|
|
48
|
+
|
|
49
|
+
module = importlib.import_module(_LAZY_ATTRS[name], __name__)
|
|
50
|
+
value = getattr(module, name)
|
|
51
|
+
globals()[name] = value
|
|
52
|
+
return value
|
|
53
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def __dir__():
|
|
57
|
+
return sorted(set(globals()) | set(_LAZY_ATTRS))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if TYPE_CHECKING:
|
|
61
|
+
# Make the lazy names visible to static type checkers and IDEs.
|
|
62
|
+
from .firewall import AttackDetector, AttackDetectorEnsemble, Firewall
|
|
63
|
+
from .hbfw import HBFW, load_hbfw, load_model_class, save_hbfw
|
|
64
|
+
from .llm import (
|
|
65
|
+
Provider,
|
|
66
|
+
ProviderIntegration,
|
|
67
|
+
ProviderName,
|
|
68
|
+
get_llm_pinger,
|
|
69
|
+
get_llm_streamer,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"Firewall",
|
|
75
|
+
"AttackDetector",
|
|
76
|
+
"AttackDetectorEnsemble",
|
|
77
|
+
"EvalResult",
|
|
78
|
+
"AgentConfig",
|
|
79
|
+
"Verdict",
|
|
80
|
+
"Category",
|
|
81
|
+
"Turn",
|
|
82
|
+
"VERDICT_MAP",
|
|
83
|
+
"Provider",
|
|
84
|
+
"ProviderIntegration",
|
|
85
|
+
"ProviderName",
|
|
86
|
+
"get_llm_pinger",
|
|
87
|
+
"get_llm_streamer",
|
|
88
|
+
"HBFW",
|
|
89
|
+
"load_model_class",
|
|
90
|
+
"save_hbfw",
|
|
91
|
+
"load_hbfw",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# Backwards-compat shim: legacy imports (`import hb_firewall`) and legacy
|
|
95
|
+
# pickled `.hbfw` models that reference the `hb_firewall.*` module path
|
|
96
|
+
# continue to work. Scope: 0.2.x only — will be removed in 0.3.
|
|
97
|
+
sys.modules.setdefault("hb_firewall", sys.modules[__name__])
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2024-2026 Humanbound
|
|
3
|
+
"""Prompt cache — avoids rebuilding the system prompt on every evaluation."""
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from .judge import build_system_prompt
|
|
8
|
+
from .models import AgentConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PromptCache:
|
|
12
|
+
"""Caches the built system prompt to avoid recomputation.
|
|
13
|
+
|
|
14
|
+
The system prompt (agent scope + intents + few-shots) is constant across
|
|
15
|
+
evaluations. Only the user message and session context change. Caching
|
|
16
|
+
the base prompt saves ~1-2ms per call and enables provider-level caching
|
|
17
|
+
(Anthropic prompt caching, OpenAI prefix caching).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._base_prompt: str | None = None
|
|
22
|
+
self._config_hash: str | None = None
|
|
23
|
+
|
|
24
|
+
def get_or_build(self, config: AgentConfig) -> str:
|
|
25
|
+
"""Return cached base prompt or build and cache a new one."""
|
|
26
|
+
current_hash = self._hash_config(config)
|
|
27
|
+
|
|
28
|
+
if self._base_prompt is not None and self._config_hash == current_hash:
|
|
29
|
+
return self._base_prompt
|
|
30
|
+
|
|
31
|
+
# Build fresh prompt (without session context — that's added per-call)
|
|
32
|
+
self._base_prompt = build_system_prompt(config, session_turns=None)
|
|
33
|
+
self._config_hash = current_hash
|
|
34
|
+
|
|
35
|
+
return self._base_prompt
|
|
36
|
+
|
|
37
|
+
def invalidate(self):
|
|
38
|
+
"""Force rebuild on next call."""
|
|
39
|
+
self._base_prompt = None
|
|
40
|
+
self._config_hash = None
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _hash_config(config: AgentConfig) -> str:
|
|
44
|
+
"""Hash config fields that affect the system prompt."""
|
|
45
|
+
key = (
|
|
46
|
+
f"{config.business_scope}|{config.more_info}|"
|
|
47
|
+
f"{','.join(config.permitted_intents)}|"
|
|
48
|
+
f"{','.join(config.restricted_intents)}|"
|
|
49
|
+
f"{len(config.few_shots)}"
|
|
50
|
+
)
|
|
51
|
+
return hashlib.md5(key.encode()).hexdigest()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright (c) 2024-2026 Humanbound
|
|
3
|
+
"""YAML configuration loader."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from .models import AgentConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_config(path: str | Path) -> AgentConfig:
|
|
13
|
+
"""Load agent configuration from a YAML file."""
|
|
14
|
+
path = Path(path)
|
|
15
|
+
if not path.exists():
|
|
16
|
+
raise FileNotFoundError(f"Config file not found: {path}")
|
|
17
|
+
|
|
18
|
+
with open(path, encoding="utf-8") as f:
|
|
19
|
+
data = yaml.safe_load(f)
|
|
20
|
+
|
|
21
|
+
if not isinstance(data, dict):
|
|
22
|
+
raise ValueError(f"Invalid config format in {path}")
|
|
23
|
+
|
|
24
|
+
scope = data.get("scope", {})
|
|
25
|
+
intents = data.get("intents", {})
|
|
26
|
+
settings = data.get("settings", {})
|
|
27
|
+
|
|
28
|
+
return AgentConfig(
|
|
29
|
+
name=data.get("name", ""),
|
|
30
|
+
version=str(data.get("version", "1.0")),
|
|
31
|
+
business_scope=scope.get("business", ""),
|
|
32
|
+
more_info=scope.get("more_info", ""),
|
|
33
|
+
permitted_intents=intents.get("permitted", []),
|
|
34
|
+
restricted_intents=intents.get("restricted", []),
|
|
35
|
+
timeout=settings.get("timeout", 5),
|
|
36
|
+
mode=settings.get("mode", "block"),
|
|
37
|
+
session_window=settings.get("session_window", 5),
|
|
38
|
+
tier2_min_turns=settings.get("tier2_min_turns", 3),
|
|
39
|
+
risk_tolerance=settings.get("risk_tolerance", "medium"),
|
|
40
|
+
temperature=settings.get("temperature", 0.0),
|
|
41
|
+
few_shots=data.get("few_shots", []),
|
|
42
|
+
)
|