agent-tool-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,47 @@
1
+ """Agent Firewall — human-in-the-loop tool call approval."""
2
+
3
+ from agent_firewall.context import set_current_user
4
+ from agent_firewall.decorator import guarded_tool
5
+ from agent_firewall.policy_engine import PolicyEngine
6
+ from agent_firewall.server.app import create_app
7
+
8
+ # Guard modules — injection detection, PII masking, groundedness checking.
9
+ from agent_firewall.guards.injection_detector import InjectionScore, score_chunk
10
+ from agent_firewall.guards.pii_masker import (
11
+ PIIMasker,
12
+ mask_pii,
13
+ unmask_pii,
14
+ reset_pii_masker,
15
+ get_pii_mapping,
16
+ )
17
+ from agent_firewall.guards.groundedness_checker import (
18
+ ClaimResult,
19
+ GroundednessReport,
20
+ check_groundedness,
21
+ format_grounded_answer,
22
+ )
23
+ from agent_firewall.guards.pipeline import GuardPipeline, GuardResult
24
+
25
+ __all__ = [
26
+ "guarded_tool",
27
+ "set_current_user",
28
+ "PolicyEngine",
29
+ "create_app",
30
+ # Guards
31
+ "InjectionScore",
32
+ "score_chunk",
33
+ "PIIMasker",
34
+ "mask_pii",
35
+ "unmask_pii",
36
+ "reset_pii_masker",
37
+ "get_pii_mapping",
38
+ "ClaimResult",
39
+ "GroundednessReport",
40
+ "check_groundedness",
41
+ "format_grounded_answer",
42
+ "GuardPipeline",
43
+ "GuardResult",
44
+ ]
45
+
46
+ __version__ = "0.2.0"
47
+
agent_firewall/cli.py ADDED
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+ import shutil
6
+
7
+ import uvicorn
8
+
9
+ from agent_firewall.config import get_settings
10
+
11
+
12
+ def _example_policy_path() -> Path | None:
13
+ """Resolve firewall.example.yaml for wheel installs and local checkouts."""
14
+ candidates = [
15
+ Path(__file__).resolve().parent / "data" / "firewall.example.yaml",
16
+ Path.cwd() / "firewall.example.yaml",
17
+ Path(__file__).resolve().parents[1] / "firewall.example.yaml",
18
+ ]
19
+ return next((p for p in candidates if p.exists()), None)
20
+
21
+
22
+ def main(argv: list[str] | None = None) -> None:
23
+ parser = argparse.ArgumentParser(prog="agent-firewall", description="Agent Firewall CLI")
24
+ sub = parser.add_subparsers(dest="command", required=True)
25
+
26
+ serve = sub.add_parser("serve", help="Start API + approval dashboard")
27
+ serve.add_argument("--host", default="127.0.0.1")
28
+ serve.add_argument("--port", type=int, default=8000)
29
+ serve.add_argument("--db", default=None, help="SQLite DB path")
30
+ serve.add_argument("--policy", default=None, help="firewall.yaml path")
31
+ serve.add_argument("--reload", action="store_true")
32
+
33
+ init = sub.add_parser("init-policy", help="Copy firewall.example.yaml to ./firewall.yaml")
34
+ init.add_argument("--force", action="store_true")
35
+
36
+ args = parser.parse_args(argv)
37
+
38
+ if args.command == "init-policy":
39
+ example = _example_policy_path()
40
+ if example is None:
41
+ raise SystemExit("firewall.example.yaml not found")
42
+ dest = Path.cwd() / "firewall.yaml"
43
+ if dest.exists() and not args.force:
44
+ raise SystemExit(f"{dest} already exists (use --force to overwrite)")
45
+ shutil.copy(example, dest)
46
+ print(f"Wrote {dest}")
47
+ return
48
+
49
+ if args.command == "serve":
50
+ settings = get_settings(db_path=args.db, policy_path=args.policy)
51
+ if args.db:
52
+ import os
53
+
54
+ os.environ["FIREWALL_DB_PATH"] = args.db
55
+ if args.policy:
56
+ import os
57
+
58
+ os.environ["FIREWALL_POLICY_PATH"] = args.policy
59
+
60
+ policy_path = Path(settings.policy_path)
61
+ if not policy_path.exists():
62
+ print(
63
+ f"Warning: policy file not found at {policy_path.resolve()}. "
64
+ f"Run `agent-firewall init-policy` or pass --policy."
65
+ )
66
+
67
+ uvicorn.run(
68
+ "agent_firewall.server.app:create_app",
69
+ factory=True,
70
+ host=args.host,
71
+ port=args.port,
72
+ reload=args.reload,
73
+ )
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import requests
6
+
7
+ from agent_firewall.config import get_settings
8
+
9
+
10
+ class FirewallClientError(RuntimeError):
11
+ pass
12
+
13
+
14
+ class FirewallClient:
15
+ def __init__(self, base_url: str | None = None, timeout: float = 10.0):
16
+ settings = get_settings(firewall_url=base_url)
17
+ self.base_url = settings.firewall_url
18
+ self.timeout = timeout
19
+
20
+ def _url(self, path: str) -> str:
21
+ return f"{self.base_url}{path}"
22
+
23
+ def _request(self, method: str, path: str, **kwargs) -> Any:
24
+ try:
25
+ resp = requests.request(method, self._url(path), timeout=self.timeout, **kwargs)
26
+ except requests.RequestException as exc:
27
+ raise FirewallClientError(
28
+ f"Agent Firewall backend unreachable at {self.base_url}: {exc}"
29
+ ) from exc
30
+ if resp.status_code >= 400:
31
+ raise FirewallClientError(
32
+ f"{method} {path} failed ({resp.status_code}): {resp.text}"
33
+ )
34
+ if resp.content:
35
+ return resp.json()
36
+ return None
37
+
38
+ def create_pending(
39
+ self,
40
+ *,
41
+ tool_name: str,
42
+ payload: dict[str, Any],
43
+ risk_level: str,
44
+ matched_rule: str,
45
+ requested_by: str,
46
+ timeout_seconds: int,
47
+ ) -> int:
48
+ data = self._request(
49
+ "POST",
50
+ "/api/pending",
51
+ json={
52
+ "tool_name": tool_name,
53
+ "payload": payload,
54
+ "risk_level": risk_level,
55
+ "matched_rule": matched_rule,
56
+ "requested_by": requested_by,
57
+ "timeout_seconds": timeout_seconds,
58
+ },
59
+ )
60
+ return int(data["id"])
61
+
62
+ def get_pending(self, pending_id: int) -> dict[str, Any]:
63
+ return self._request("GET", f"/api/pending/{pending_id}")
64
+
65
+ def mark_executed(self, pending_id: int, error_message: str | None = None) -> dict[str, Any]:
66
+ return self._request(
67
+ "POST",
68
+ f"/api/pending/{pending_id}/executed",
69
+ json={"error_message": error_message},
70
+ )
71
+
72
+ def log_audit(
73
+ self,
74
+ *,
75
+ tool_name: str,
76
+ payload: dict[str, Any],
77
+ decision: str,
78
+ requested_by: str,
79
+ matched_rule: str,
80
+ pending_action_id: int | None = None,
81
+ decided_by: str | None = None,
82
+ ) -> None:
83
+ self._request(
84
+ "POST",
85
+ "/api/audit",
86
+ json={
87
+ "tool_name": tool_name,
88
+ "payload": payload,
89
+ "decision": decision,
90
+ "requested_by": requested_by,
91
+ "matched_rule": matched_rule,
92
+ "pending_action_id": pending_action_id,
93
+ "decided_by": decided_by,
94
+ },
95
+ )
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Settings:
14
+ firewall_url: str = "http://127.0.0.1:8000"
15
+ db_path: str = "./firewall.db"
16
+ policy_path: str = "./firewall.yaml"
17
+ auth_mode: str = "local" # local | passthrough
18
+ session_secret: str = "dev-secret-change-me"
19
+ passthrough_header: str = "X-Forwarded-User"
20
+ poll_interval_seconds: float = 1.5
21
+
22
+
23
+ def get_settings(
24
+ db_path: str | None = None,
25
+ policy_path: str | None = None,
26
+ firewall_url: str | None = None,
27
+ ) -> Settings:
28
+ return Settings(
29
+ firewall_url=(firewall_url or os.getenv("FIREWALL_URL", "http://127.0.0.1:8000")).rstrip(
30
+ "/"
31
+ ),
32
+ db_path=db_path or os.getenv("FIREWALL_DB_PATH", "./firewall.db"),
33
+ policy_path=policy_path or os.getenv("FIREWALL_POLICY_PATH", "./firewall.yaml"),
34
+ auth_mode=os.getenv("FIREWALL_AUTH_MODE", "local").lower(),
35
+ session_secret=os.getenv("FIREWALL_SESSION_SECRET", "dev-secret-change-me"),
36
+ passthrough_header=os.getenv("FIREWALL_PASSTHROUGH_HEADER", "X-Forwarded-User"),
37
+ poll_interval_seconds=float(os.getenv("FIREWALL_POLL_INTERVAL", "1.5")),
38
+ )
39
+
40
+
41
+ def resolve_path(path: str) -> Path:
42
+ return Path(path).expanduser().resolve()
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from contextvars import ContextVar
4
+
5
+ current_user: ContextVar[str] = ContextVar("current_user", default="unknown")
6
+
7
+
8
+ def set_current_user(user_id: str) -> None:
9
+ """Set the requesting user identity for subsequent guarded tool calls."""
10
+ current_user.set(user_id)
11
+
12
+
13
+ def get_current_user() -> str:
14
+ return current_user.get()
@@ -0,0 +1,64 @@
1
+ # Copy to firewall.yaml and adjust for your project.
2
+ # Demo users (local auth). Change passwords before any real deployment.
3
+
4
+ users:
5
+ - username: admin
6
+ password: admin123
7
+ display_name: Admin Approver
8
+ - username: alice
9
+ password: alice123
10
+ display_name: Alice
11
+
12
+ rules:
13
+ - name: high-value-payments
14
+ match:
15
+ tool_name: send_payment
16
+ payload_contains:
17
+ field: amount
18
+ operator: ">"
19
+ value: 1000
20
+ action: require_approval
21
+ risk_level: high
22
+ timeout_seconds: 120
23
+
24
+ - name: low-value-payments
25
+ match:
26
+ tool_name: send_payment
27
+ payload_contains:
28
+ field: amount
29
+ operator: "<="
30
+ value: 1000
31
+ action: allow
32
+ risk_level: low
33
+
34
+ - name: any-deletion
35
+ match:
36
+ tool_name: "delete_*"
37
+ action: require_approval
38
+ risk_level: high
39
+ timeout_seconds: 90
40
+
41
+ - name: block-transfer-all
42
+ match:
43
+ tool_name: transfer_all_funds
44
+ action: block
45
+ risk_level: high
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Guard pipeline — injection detection and PII scanning.
49
+ # These run BEFORE policy rules and can escalate the action (never downgrade).
50
+ # Groundedness is a standalone post-answer API (check_groundedness /
51
+ # format_grounded_answer), not part of this pre-tool pipeline.
52
+ # ---------------------------------------------------------------------------
53
+ guards:
54
+ injection_detection:
55
+ enabled: true
56
+ block_threshold: 0.8 # score >= 0.8 → auto-block
57
+ approval_threshold: 0.5 # score >= 0.5 → require approval
58
+ scan_fields: null # null = scan all string fields
59
+
60
+ pii_policy:
61
+ enabled: true
62
+ action: require_approval # allow | require_approval | block
63
+ scan_fields: null # null = scan all string fields
64
+ types: [EMAIL, PHONE, CREDIT_CARD, SSN, IPV4]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from agent_firewall.db.models import make_session_factory
4
+
5
+ __all__ = ["make_session_factory"]
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, create_engine
6
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
7
+
8
+
9
+ def utcnow() -> datetime:
10
+ return datetime.now(timezone.utc)
11
+
12
+
13
+ class Base(DeclarativeBase):
14
+ pass
15
+
16
+
17
+ class User(Base):
18
+ __tablename__ = "users"
19
+
20
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
21
+ username: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
22
+ password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
23
+ display_name: Mapped[str] = mapped_column(String(255), nullable=False)
24
+
25
+
26
+ class PendingAction(Base):
27
+ __tablename__ = "pending_actions"
28
+
29
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
30
+ tool_name: Mapped[str] = mapped_column(String(255), nullable=False)
31
+ payload_json: Mapped[str] = mapped_column(Text, nullable=False)
32
+ risk_level: Mapped[str] = mapped_column(String(32), nullable=False, default="medium")
33
+ matched_rule: Mapped[str] = mapped_column(String(255), nullable=False, default="")
34
+ status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending")
35
+ requested_by: Mapped[str] = mapped_column(String(255), nullable=False, default="unknown")
36
+ requested_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
37
+ decided_by: Mapped[str | None] = mapped_column(String(255), nullable=True)
38
+ decided_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
39
+ reason: Mapped[str | None] = mapped_column(Text, nullable=True)
40
+ timeout_seconds: Mapped[int] = mapped_column(Integer, nullable=False, default=120)
41
+ executed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
42
+ error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
43
+
44
+
45
+ class AuditLog(Base):
46
+ __tablename__ = "audit_log"
47
+
48
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
49
+ pending_action_id: Mapped[int | None] = mapped_column(
50
+ Integer, ForeignKey("pending_actions.id"), nullable=True
51
+ )
52
+ tool_name: Mapped[str] = mapped_column(String(255), nullable=False)
53
+ payload_json: Mapped[str] = mapped_column(Text, nullable=False)
54
+ decision: Mapped[str] = mapped_column(String(32), nullable=False)
55
+ requested_by: Mapped[str] = mapped_column(String(255), nullable=False, default="unknown")
56
+ decided_by: Mapped[str | None] = mapped_column(String(255), nullable=True)
57
+ timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
58
+ matched_rule: Mapped[str] = mapped_column(String(255), nullable=False, default="")
59
+ reason: Mapped[str | None] = mapped_column(Text, nullable=True)
60
+
61
+
62
+ def make_engine(db_path: str):
63
+ # SQLite needs absolute-ish URL; keep simple file path.
64
+ url = f"sqlite:///{db_path}"
65
+ return create_engine(url, connect_args={"check_same_thread": False})
66
+
67
+
68
+ def make_session_factory(db_path: str):
69
+ engine = make_engine(db_path)
70
+ return engine, sessionmaker(bind=engine, autoflush=False, autocommit=False)
71
+
72
+
73
+ def _ensure_sqlite_columns(engine) -> None:
74
+ """Add columns introduced after first install (SQLite has no migrations)."""
75
+ if engine.dialect.name != "sqlite":
76
+ return
77
+ with engine.begin() as conn:
78
+ rows = conn.exec_driver_sql("PRAGMA table_info(audit_log)").fetchall()
79
+ columns = {row[1] for row in rows}
80
+ if rows and "reason" not in columns:
81
+ conn.exec_driver_sql("ALTER TABLE audit_log ADD COLUMN reason TEXT")
82
+
83
+
84
+ def init_db(engine) -> None:
85
+ Base.metadata.create_all(bind=engine)
86
+ _ensure_sqlite_columns(engine)
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from collections.abc import Callable
6
+ from functools import wraps
7
+ from typing import Any, TypeVar
8
+
9
+ from agent_firewall.client import FirewallClient
10
+ from agent_firewall.config import get_settings
11
+ from agent_firewall.context import get_current_user
12
+ from agent_firewall.models import PolicyAction
13
+ from agent_firewall.policy_engine import PolicyEngine
14
+ from agent_firewall.waiter import start_approval_waiter
15
+
16
+ F = TypeVar("F", bound=Callable[..., Any])
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _policy_engine: PolicyEngine | None = None
21
+ _guard_pipeline: Any = None # lazy import to avoid circular imports
22
+
23
+
24
+ def _get_policy_engine() -> PolicyEngine:
25
+ global _policy_engine
26
+ if _policy_engine is None:
27
+ settings = get_settings()
28
+ _policy_engine = PolicyEngine(settings.policy_path)
29
+ return _policy_engine
30
+
31
+
32
+ def _get_guard_pipeline():
33
+ """Build or return the cached ``GuardPipeline`` from the policy YAML ``guards:`` section."""
34
+ global _guard_pipeline
35
+ if _guard_pipeline is None:
36
+ from agent_firewall.guards.pipeline import GuardPipeline
37
+
38
+ engine = _get_policy_engine()
39
+ guards_config = engine.guards_config
40
+ if guards_config:
41
+ _guard_pipeline = GuardPipeline.from_policy_config(guards_config)
42
+ else:
43
+ # No guards configured — return a no-op pipeline.
44
+ _guard_pipeline = GuardPipeline()
45
+ return _guard_pipeline
46
+
47
+
48
+ def reload_policy() -> None:
49
+ global _guard_pipeline
50
+ _get_policy_engine().reload()
51
+ _guard_pipeline = None # Force rebuild on next call.
52
+
53
+
54
+ def guarded_tool(
55
+ require_approval: bool | None = None,
56
+ risk_level_hint: str | None = None,
57
+ scan_injection: bool | None = None,
58
+ scan_pii: bool | None = None,
59
+ ) -> Callable[[F], F]:
60
+ """
61
+ Intercept tool calls and apply YAML policy (+ optional decorator override).
62
+
63
+ Non-blocking on require_approval: returns a dashboard message immediately and
64
+ executes the real tool in a background waiter after approve.
65
+
66
+ Parameters
67
+ ----------
68
+ require_approval:
69
+ Force HITL regardless of YAML policy.
70
+ risk_level_hint:
71
+ Override the risk level for this tool.
72
+ scan_injection:
73
+ Override the YAML ``guards.injection_detection.enabled`` setting for
74
+ this specific tool. ``True`` to force scanning, ``False`` to skip.
75
+ scan_pii:
76
+ Override the YAML ``guards.pii_policy.enabled`` setting for this
77
+ specific tool. ``True`` to force scanning, ``False`` to skip.
78
+ """
79
+
80
+ def decorator(fn: F) -> F:
81
+ @wraps(fn)
82
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
83
+ engine = _get_policy_engine()
84
+ # Prefer kwargs as payload (typical tool calling). Merge positional by name if needed.
85
+ payload = dict(kwargs)
86
+ decision = engine.evaluate(fn.__name__, payload)
87
+
88
+ if require_approval is True:
89
+ from agent_firewall.models import PolicyDecision
90
+
91
+ decision = PolicyDecision(
92
+ action=PolicyAction.REQUIRE_APPROVAL,
93
+ risk_level=risk_level_hint or decision.risk_level or "high",
94
+ matched_rule=f"decorator-override:{decision.matched_rule}",
95
+ timeout_seconds=decision.timeout_seconds or 120,
96
+ )
97
+
98
+ # --- Run guard pipeline (injection + PII) ---
99
+ guard_details: dict[str, Any] = {}
100
+ try:
101
+ pipeline = _get_guard_pipeline()
102
+
103
+ # Apply per-decorator overrides.
104
+ if scan_injection is not None:
105
+ pipeline.injection_enabled = scan_injection
106
+ if scan_pii is not None:
107
+ pipeline.pii_enabled = scan_pii
108
+
109
+ guard_result = pipeline.evaluate(fn.__name__, payload)
110
+ guard_details = guard_result.guard_details
111
+
112
+ # Guards can escalate the action but never downgrade it.
113
+ if guard_result.action == PolicyAction.BLOCK and decision.action != PolicyAction.BLOCK:
114
+ from agent_firewall.models import PolicyDecision
115
+
116
+ decision = PolicyDecision(
117
+ action=PolicyAction.BLOCK,
118
+ risk_level="critical",
119
+ matched_rule=f"guard:{guard_result.reason}",
120
+ timeout_seconds=decision.timeout_seconds,
121
+ )
122
+ elif (
123
+ guard_result.action == PolicyAction.REQUIRE_APPROVAL
124
+ and decision.action == PolicyAction.ALLOW
125
+ ):
126
+ from agent_firewall.models import PolicyDecision
127
+
128
+ decision = PolicyDecision(
129
+ action=PolicyAction.REQUIRE_APPROVAL,
130
+ risk_level=risk_level_hint or "high",
131
+ matched_rule=f"guard:{guard_result.reason}",
132
+ timeout_seconds=decision.timeout_seconds or 120,
133
+ )
134
+ except Exception: # noqa: BLE001
135
+ logger.warning("Guard pipeline error for %s — proceeding with policy only", fn.__name__, exc_info=True)
136
+
137
+ client = FirewallClient()
138
+ requested_by = get_current_user()
139
+
140
+ # Merge guard scan details into the audit payload so the dashboard can show them.
141
+ audit_extra: dict[str, Any] = {}
142
+ if guard_details:
143
+ audit_extra["guard_scan"] = guard_details
144
+
145
+ if decision.action == PolicyAction.BLOCK:
146
+ client.log_audit(
147
+ tool_name=fn.__name__,
148
+ payload={**payload, **audit_extra},
149
+ decision="auto_block",
150
+ requested_by=requested_by,
151
+ matched_rule=decision.matched_rule,
152
+ )
153
+ return (
154
+ f"Blocked by Agent Firewall policy rule '{decision.matched_rule}'. "
155
+ f"Tool '{fn.__name__}' was not executed."
156
+ )
157
+
158
+ if decision.action == PolicyAction.ALLOW:
159
+ client.log_audit(
160
+ tool_name=fn.__name__,
161
+ payload={**payload, **audit_extra},
162
+ decision="auto_allow",
163
+ requested_by=requested_by,
164
+ matched_rule=decision.matched_rule,
165
+ )
166
+ return fn(*args, **kwargs)
167
+
168
+ # require_approval
169
+ pending_id = client.create_pending(
170
+ tool_name=fn.__name__,
171
+ payload={**payload, **audit_extra},
172
+ risk_level=risk_level_hint or decision.risk_level,
173
+ matched_rule=decision.matched_rule,
174
+ requested_by=requested_by,
175
+ timeout_seconds=decision.timeout_seconds or 120,
176
+ )
177
+ start_approval_waiter(
178
+ pending_id=pending_id,
179
+ timeout_seconds=decision.timeout_seconds or 120,
180
+ fn=fn,
181
+ args=args,
182
+ kwargs=kwargs,
183
+ client=client,
184
+ )
185
+ settings = get_settings()
186
+ return (
187
+ f"This tool requires human approval. Request #{pending_id} was added to the "
188
+ f"Agent Firewall dashboard ({settings.firewall_url}). "
189
+ f"Sign in and approve or reject it there. "
190
+ f"Payload: {json.dumps(payload, default=str)}"
191
+ )
192
+
193
+ return wrapper # type: ignore[return-value]
194
+
195
+ return decorator
@@ -0,0 +1,33 @@
1
+ """Agent Firewall — pluggable guardrails (injection, PII, groundedness)."""
2
+
3
+ from agent_firewall.guards.injection_detector import InjectionScore, score_chunk
4
+ from agent_firewall.guards.pii_masker import (
5
+ PIIMasker,
6
+ get_pii_mapping,
7
+ mask_pii,
8
+ reset_pii_masker,
9
+ unmask_pii,
10
+ )
11
+ from agent_firewall.guards.groundedness_checker import (
12
+ ClaimResult,
13
+ GroundednessReport,
14
+ check_groundedness,
15
+ format_grounded_answer,
16
+ )
17
+ from agent_firewall.guards.pipeline import GuardPipeline, GuardResult
18
+
19
+ __all__ = [
20
+ "InjectionScore",
21
+ "score_chunk",
22
+ "PIIMasker",
23
+ "mask_pii",
24
+ "unmask_pii",
25
+ "reset_pii_masker",
26
+ "get_pii_mapping",
27
+ "ClaimResult",
28
+ "GroundednessReport",
29
+ "check_groundedness",
30
+ "format_grounded_answer",
31
+ "GuardPipeline",
32
+ "GuardResult",
33
+ ]