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.
@@ -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
+ )