agentguardproxy 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.
- agentguard/__init__.py +197 -0
- agentguard/adapters/__init__.py +8 -0
- agentguard/adapters/browseruse.py +128 -0
- agentguard/adapters/crewai.py +134 -0
- agentguard/adapters/langchain.py +167 -0
- agentguard/adapters/mcp.py +299 -0
- agentguardproxy-0.2.0.dist-info/METADATA +142 -0
- agentguardproxy-0.2.0.dist-info/RECORD +10 -0
- agentguardproxy-0.2.0.dist-info/WHEEL +5 -0
- agentguardproxy-0.2.0.dist-info/top_level.txt +1 -0
agentguard/__init__.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentGuard Python SDK
|
|
3
|
+
|
|
4
|
+
Lightweight client for checking actions against AgentGuard policies.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from agentguard import Guard
|
|
8
|
+
|
|
9
|
+
guard = Guard("http://localhost:8080")
|
|
10
|
+
result = guard.check("shell", command="rm -rf ./data")
|
|
11
|
+
|
|
12
|
+
if result.allowed:
|
|
13
|
+
execute(command)
|
|
14
|
+
elif result.needs_approval:
|
|
15
|
+
print(f"Approve at: {result.approval_url}")
|
|
16
|
+
else:
|
|
17
|
+
print(f"Blocked: {result.reason}")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Optional
|
|
24
|
+
from urllib import request, error
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CheckResult:
|
|
29
|
+
"""Result of a policy check."""
|
|
30
|
+
decision: str
|
|
31
|
+
reason: str
|
|
32
|
+
matched_rule: str = ""
|
|
33
|
+
approval_id: str = ""
|
|
34
|
+
approval_url: str = ""
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def allowed(self) -> bool:
|
|
38
|
+
return self.decision == "ALLOW"
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def denied(self) -> bool:
|
|
42
|
+
return self.decision == "DENY"
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def needs_approval(self) -> bool:
|
|
46
|
+
return self.decision == "REQUIRE_APPROVAL"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Guard:
|
|
50
|
+
"""Client for the AgentGuard proxy."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, base_url: str = "http://localhost:8080", agent_id: str = ""):
|
|
53
|
+
self.base_url = base_url.rstrip("/")
|
|
54
|
+
self.agent_id = agent_id
|
|
55
|
+
|
|
56
|
+
def check(
|
|
57
|
+
self,
|
|
58
|
+
scope: str,
|
|
59
|
+
*,
|
|
60
|
+
action: str = "",
|
|
61
|
+
command: str = "",
|
|
62
|
+
path: str = "",
|
|
63
|
+
domain: str = "",
|
|
64
|
+
url: str = "",
|
|
65
|
+
meta: Optional[dict] = None,
|
|
66
|
+
) -> CheckResult:
|
|
67
|
+
"""Check an action against the policy.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
scope: The rule scope (filesystem, shell, network, browser, cost, data)
|
|
71
|
+
action: Action type (read, write, delete) — used with filesystem scope
|
|
72
|
+
command: Shell command string — used with shell scope
|
|
73
|
+
path: File path — used with filesystem scope
|
|
74
|
+
domain: Target domain — used with network/browser scope
|
|
75
|
+
url: Full URL — used with network scope
|
|
76
|
+
meta: Additional metadata
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
CheckResult with the policy decision
|
|
80
|
+
"""
|
|
81
|
+
payload = {
|
|
82
|
+
"scope": scope,
|
|
83
|
+
"agent_id": self.agent_id,
|
|
84
|
+
}
|
|
85
|
+
if action:
|
|
86
|
+
payload["action"] = action
|
|
87
|
+
if command:
|
|
88
|
+
payload["command"] = command
|
|
89
|
+
if path:
|
|
90
|
+
payload["path"] = path
|
|
91
|
+
if domain:
|
|
92
|
+
payload["domain"] = domain
|
|
93
|
+
if url:
|
|
94
|
+
payload["url"] = url
|
|
95
|
+
if meta:
|
|
96
|
+
payload["meta"] = meta
|
|
97
|
+
|
|
98
|
+
data = json.dumps(payload).encode("utf-8")
|
|
99
|
+
req = request.Request(
|
|
100
|
+
f"{self.base_url}/v1/check",
|
|
101
|
+
data=data,
|
|
102
|
+
headers={"Content-Type": "application/json"},
|
|
103
|
+
method="POST",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
with request.urlopen(req, timeout=5) as resp:
|
|
108
|
+
body = json.loads(resp.read())
|
|
109
|
+
return CheckResult(
|
|
110
|
+
decision=body.get("decision", "DENY"),
|
|
111
|
+
reason=body.get("reason", ""),
|
|
112
|
+
matched_rule=body.get("matched_rule", ""),
|
|
113
|
+
approval_id=body.get("approval_id", ""),
|
|
114
|
+
approval_url=body.get("approval_url", ""),
|
|
115
|
+
)
|
|
116
|
+
except error.URLError as e:
|
|
117
|
+
# If AgentGuard is unreachable, default to deny (fail closed)
|
|
118
|
+
return CheckResult(
|
|
119
|
+
decision="DENY",
|
|
120
|
+
reason=f"AgentGuard unreachable: {e}",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def approve(self, approval_id: str) -> bool:
|
|
124
|
+
"""Approve a pending action."""
|
|
125
|
+
req = request.Request(
|
|
126
|
+
f"{self.base_url}/v1/approve/{approval_id}",
|
|
127
|
+
method="POST",
|
|
128
|
+
)
|
|
129
|
+
try:
|
|
130
|
+
with request.urlopen(req, timeout=5):
|
|
131
|
+
return True
|
|
132
|
+
except error.URLError:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
def deny(self, approval_id: str) -> bool:
|
|
136
|
+
"""Deny a pending action."""
|
|
137
|
+
req = request.Request(
|
|
138
|
+
f"{self.base_url}/v1/deny/{approval_id}",
|
|
139
|
+
method="POST",
|
|
140
|
+
)
|
|
141
|
+
try:
|
|
142
|
+
with request.urlopen(req, timeout=5):
|
|
143
|
+
return True
|
|
144
|
+
except error.URLError:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
def wait_for_approval(self, approval_id: str, timeout: int = 300, poll_interval: int = 2) -> CheckResult:
|
|
148
|
+
"""Block until a pending action is approved or denied (or timeout)."""
|
|
149
|
+
deadline = time.time() + timeout
|
|
150
|
+
while time.time() < deadline:
|
|
151
|
+
# Poll the status endpoint for resolution
|
|
152
|
+
req = request.Request(
|
|
153
|
+
f"{self.base_url}/v1/status/{approval_id}",
|
|
154
|
+
method="GET",
|
|
155
|
+
)
|
|
156
|
+
try:
|
|
157
|
+
with request.urlopen(req, timeout=5) as resp:
|
|
158
|
+
body = json.loads(resp.read())
|
|
159
|
+
if body.get("status") == "resolved" and body.get("decision") in ("ALLOW", "DENY"):
|
|
160
|
+
return CheckResult(
|
|
161
|
+
decision=body["decision"],
|
|
162
|
+
reason=body.get("reason", "resolved"),
|
|
163
|
+
)
|
|
164
|
+
except error.URLError:
|
|
165
|
+
pass
|
|
166
|
+
time.sleep(poll_interval)
|
|
167
|
+
|
|
168
|
+
return CheckResult(decision="DENY", reason="Approval timed out")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Convenience decorator for guarding functions
|
|
172
|
+
def guarded(scope: str, guard: Optional[Guard] = None, **check_kwargs):
|
|
173
|
+
"""Decorator that checks policy before executing a function.
|
|
174
|
+
|
|
175
|
+
Usage:
|
|
176
|
+
guard = Guard("http://localhost:8080")
|
|
177
|
+
|
|
178
|
+
@guarded("shell", guard=guard)
|
|
179
|
+
def run_command(cmd: str):
|
|
180
|
+
os.system(cmd)
|
|
181
|
+
"""
|
|
182
|
+
def decorator(func):
|
|
183
|
+
def wrapper(*args, **kwargs):
|
|
184
|
+
g = guard or Guard()
|
|
185
|
+
# Try to extract meaningful info from args
|
|
186
|
+
cmd = args[0] if args else kwargs.get("command", kwargs.get("cmd", ""))
|
|
187
|
+
result = g.check(scope, command=str(cmd), **check_kwargs)
|
|
188
|
+
if result.allowed:
|
|
189
|
+
return func(*args, **kwargs)
|
|
190
|
+
elif result.needs_approval:
|
|
191
|
+
raise PermissionError(
|
|
192
|
+
f"Action requires approval. Approve at: {result.approval_url}"
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
raise PermissionError(f"Action denied by AgentGuard: {result.reason}")
|
|
196
|
+
return wrapper
|
|
197
|
+
return decorator
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""AgentGuard framework adapters.
|
|
2
|
+
|
|
3
|
+
Available adapters:
|
|
4
|
+
- langchain: Wraps LangChain tools with policy enforcement
|
|
5
|
+
- crewai: Wraps CrewAI tools with policy enforcement
|
|
6
|
+
- browseruse: Wraps browser-use actions with policy enforcement
|
|
7
|
+
- mcp: MCP-compatible server that enforces policies on tool calls
|
|
8
|
+
"""
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentGuard browser-use Adapter
|
|
3
|
+
|
|
4
|
+
Wraps browser-use actions so navigation, clicks, and form inputs pass through
|
|
5
|
+
AgentGuard policy checks before execution.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from agentguard.adapters.browseruse import GuardedBrowser
|
|
9
|
+
|
|
10
|
+
browser = GuardedBrowser(
|
|
11
|
+
guard_url="http://localhost:8080",
|
|
12
|
+
agent_id="my-browser-agent",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Check before navigating
|
|
16
|
+
result = browser.check_navigation("https://example.com")
|
|
17
|
+
if result.allowed:
|
|
18
|
+
await page.goto("https://example.com")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
from urllib.parse import urlparse
|
|
23
|
+
from agentguard import Guard, CheckResult
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GuardedBrowser:
|
|
27
|
+
"""Policy-enforced wrapper for browser-use automation.
|
|
28
|
+
|
|
29
|
+
browser-use exposes a Browser/BrowserContext that agents drive. This class
|
|
30
|
+
provides guard methods that should be called before performing browser actions.
|
|
31
|
+
It can also wrap a browser-use Browser instance to intercept calls automatically.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
guard: Optional[Guard] = None,
|
|
37
|
+
guard_url: str = "http://localhost:8080",
|
|
38
|
+
agent_id: str = "",
|
|
39
|
+
browser: Any = None,
|
|
40
|
+
):
|
|
41
|
+
self._guard = guard or Guard(guard_url, agent_id=agent_id)
|
|
42
|
+
self._browser = browser
|
|
43
|
+
|
|
44
|
+
def check_navigation(self, url: str) -> CheckResult:
|
|
45
|
+
"""Check if navigation to a URL is allowed by policy."""
|
|
46
|
+
domain = ""
|
|
47
|
+
try:
|
|
48
|
+
parsed = urlparse(url)
|
|
49
|
+
domain = parsed.hostname or ""
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
return self._guard.check("browser", url=url, domain=domain)
|
|
54
|
+
|
|
55
|
+
def check_action(self, action: str, target: str = "", meta: Optional[dict] = None) -> CheckResult:
|
|
56
|
+
"""Check a browser action (click, type, etc.) against policy.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
action: The action type (e.g., "click", "type", "screenshot")
|
|
60
|
+
target: The target selector or URL
|
|
61
|
+
meta: Additional context
|
|
62
|
+
"""
|
|
63
|
+
return self._guard.check(
|
|
64
|
+
"browser",
|
|
65
|
+
command=f"{action} {target}".strip(),
|
|
66
|
+
meta=meta,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def check_form_input(self, url: str, field_name: str, value: str) -> CheckResult:
|
|
70
|
+
"""Check if typing into a form field is allowed.
|
|
71
|
+
|
|
72
|
+
This is useful for preventing PII or credential leakage into web forms.
|
|
73
|
+
"""
|
|
74
|
+
domain = ""
|
|
75
|
+
try:
|
|
76
|
+
parsed = urlparse(url)
|
|
77
|
+
domain = parsed.hostname or ""
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
return self._guard.check(
|
|
82
|
+
"data",
|
|
83
|
+
domain=domain,
|
|
84
|
+
command=f"input:{field_name}",
|
|
85
|
+
meta={"field": field_name, "url": url},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def wrap_page(self, page: Any) -> "GuardedPage":
|
|
89
|
+
"""Wrap a browser-use Page object with policy enforcement.
|
|
90
|
+
|
|
91
|
+
Returns a GuardedPage that intercepts goto() and other navigation methods.
|
|
92
|
+
"""
|
|
93
|
+
return GuardedPage(page, self._guard)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GuardedPage:
|
|
97
|
+
"""Wraps a browser-use Page to enforce policies on navigation."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, page: Any, guard: Guard):
|
|
100
|
+
self._page = page
|
|
101
|
+
self._guard = guard
|
|
102
|
+
|
|
103
|
+
async def goto(self, url: str, **kwargs) -> Any:
|
|
104
|
+
"""Navigate to a URL after policy check."""
|
|
105
|
+
domain = ""
|
|
106
|
+
try:
|
|
107
|
+
parsed = urlparse(url)
|
|
108
|
+
domain = parsed.hostname or ""
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
result = self._guard.check("browser", url=url, domain=domain)
|
|
113
|
+
|
|
114
|
+
if result.allowed:
|
|
115
|
+
return await self._page.goto(url, **kwargs)
|
|
116
|
+
elif result.needs_approval:
|
|
117
|
+
raise PermissionError(
|
|
118
|
+
f"[AgentGuard] Navigation requires approval. "
|
|
119
|
+
f"Approve at: {result.approval_url}"
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
raise PermissionError(
|
|
123
|
+
f"[AgentGuard] Navigation denied: {result.reason}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def __getattr__(self, name: str) -> Any:
|
|
127
|
+
"""Proxy all other attributes to the wrapped page."""
|
|
128
|
+
return getattr(self._page, name)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentGuard CrewAI Adapter
|
|
3
|
+
|
|
4
|
+
Wraps CrewAI tools so every invocation passes through AgentGuard policy checks.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from agentguard.adapters.crewai import GuardedCrewTool, guard_crew_tools
|
|
8
|
+
|
|
9
|
+
# Wrap a single tool
|
|
10
|
+
guarded = GuardedCrewTool(my_tool, guard_url="http://localhost:8080")
|
|
11
|
+
|
|
12
|
+
# Wrap all tools for a crew
|
|
13
|
+
tools = guard_crew_tools(
|
|
14
|
+
tools=[tool_a, tool_b],
|
|
15
|
+
guard_url="http://localhost:8080",
|
|
16
|
+
agent_id="my-crew-agent",
|
|
17
|
+
)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any, List, Optional
|
|
21
|
+
from agentguard import Guard
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GuardedCrewTool:
|
|
25
|
+
"""Wraps a CrewAI BaseTool with AgentGuard policy enforcement.
|
|
26
|
+
|
|
27
|
+
CrewAI tools implement a `_run` method and expose `name` and `description`.
|
|
28
|
+
This wrapper intercepts `_run` and `run` to check the policy first.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
tool: Any,
|
|
34
|
+
guard: Optional[Guard] = None,
|
|
35
|
+
guard_url: str = "http://localhost:8080",
|
|
36
|
+
agent_id: str = "",
|
|
37
|
+
scope: str = "shell",
|
|
38
|
+
):
|
|
39
|
+
self._tool = tool
|
|
40
|
+
self._guard = guard or Guard(guard_url, agent_id=agent_id)
|
|
41
|
+
self._scope = scope
|
|
42
|
+
|
|
43
|
+
# Preserve original tool metadata
|
|
44
|
+
self.name = getattr(tool, "name", type(tool).__name__)
|
|
45
|
+
self.description = getattr(tool, "description", "")
|
|
46
|
+
if hasattr(tool, "args_schema"):
|
|
47
|
+
self.args_schema = tool.args_schema
|
|
48
|
+
|
|
49
|
+
def _infer_scope(self, tool_input: Any) -> str:
|
|
50
|
+
"""Infer scope from the tool input."""
|
|
51
|
+
combined = f"{self.name} {self.description}".lower()
|
|
52
|
+
if any(kw in combined for kw in ["http", "api", "fetch", "request", "url", "web"]):
|
|
53
|
+
return "network"
|
|
54
|
+
if any(kw in combined for kw in ["file", "read", "write", "directory", "path"]):
|
|
55
|
+
return "filesystem"
|
|
56
|
+
if any(kw in combined for kw in ["browser", "navigate", "click", "page"]):
|
|
57
|
+
return "browser"
|
|
58
|
+
return self._scope
|
|
59
|
+
|
|
60
|
+
def _extract_check_params(self, tool_input: Any) -> dict:
|
|
61
|
+
"""Extract parameters for the policy check from tool input."""
|
|
62
|
+
params = {}
|
|
63
|
+
if isinstance(tool_input, str):
|
|
64
|
+
params["command"] = tool_input
|
|
65
|
+
elif isinstance(tool_input, dict):
|
|
66
|
+
if "command" in tool_input or "cmd" in tool_input:
|
|
67
|
+
params["command"] = tool_input.get("command", tool_input.get("cmd", ""))
|
|
68
|
+
if "url" in tool_input:
|
|
69
|
+
params["url"] = tool_input["url"]
|
|
70
|
+
try:
|
|
71
|
+
from urllib.parse import urlparse
|
|
72
|
+
parsed = urlparse(tool_input["url"])
|
|
73
|
+
if parsed.hostname:
|
|
74
|
+
params["domain"] = parsed.hostname
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
if "path" in tool_input or "file_path" in tool_input:
|
|
78
|
+
params["path"] = tool_input.get("path", tool_input.get("file_path", ""))
|
|
79
|
+
return params
|
|
80
|
+
|
|
81
|
+
def run(self, tool_input: Any = "", **kwargs) -> Any:
|
|
82
|
+
"""Run the tool after checking with AgentGuard."""
|
|
83
|
+
params = self._extract_check_params(tool_input)
|
|
84
|
+
scope = self._infer_scope(tool_input)
|
|
85
|
+
result = self._guard.check(scope, **params)
|
|
86
|
+
|
|
87
|
+
if result.allowed:
|
|
88
|
+
# CrewAI tools use _run internally
|
|
89
|
+
if hasattr(self._tool, "_run"):
|
|
90
|
+
return self._tool._run(tool_input, **kwargs)
|
|
91
|
+
return self._tool.run(tool_input, **kwargs)
|
|
92
|
+
elif result.needs_approval:
|
|
93
|
+
return (
|
|
94
|
+
f"[AgentGuard] Action requires approval. "
|
|
95
|
+
f"Approve at: {result.approval_url}\n"
|
|
96
|
+
f"Reason: {result.reason}"
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
return (
|
|
100
|
+
f"[AgentGuard] Action denied.\n"
|
|
101
|
+
f"Reason: {result.reason}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _run(self, *args, **kwargs) -> Any:
|
|
105
|
+
"""CrewAI calls _run internally."""
|
|
106
|
+
return self.run(*args, **kwargs)
|
|
107
|
+
|
|
108
|
+
def __getattr__(self, name: str) -> Any:
|
|
109
|
+
"""Proxy all other attributes to the wrapped tool."""
|
|
110
|
+
return getattr(self._tool, name)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def guard_crew_tools(
|
|
114
|
+
tools: List[Any],
|
|
115
|
+
guard_url: str = "http://localhost:8080",
|
|
116
|
+
agent_id: str = "",
|
|
117
|
+
default_scope: str = "shell",
|
|
118
|
+
) -> List[GuardedCrewTool]:
|
|
119
|
+
"""Wrap a list of CrewAI tools with AgentGuard enforcement.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
tools: List of CrewAI tools to guard
|
|
123
|
+
guard_url: URL of the AgentGuard proxy
|
|
124
|
+
agent_id: Identifier for this agent in audit logs
|
|
125
|
+
default_scope: Default policy scope
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of GuardedCrewTool instances
|
|
129
|
+
"""
|
|
130
|
+
guard = Guard(guard_url, agent_id=agent_id)
|
|
131
|
+
return [
|
|
132
|
+
GuardedCrewTool(tool, guard=guard, scope=default_scope)
|
|
133
|
+
for tool in tools
|
|
134
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentGuard LangChain Adapter
|
|
3
|
+
|
|
4
|
+
Wraps LangChain tools so every invocation passes through AgentGuard policy checks.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from agentguard.adapters.langchain import GuardedToolkit
|
|
8
|
+
|
|
9
|
+
toolkit = GuardedToolkit(
|
|
10
|
+
tools=my_tools,
|
|
11
|
+
guard_url="http://localhost:8080",
|
|
12
|
+
agent_id="my-langchain-agent",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
agent = create_react_agent(llm, toolkit.tools, prompt)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Any, List, Optional
|
|
19
|
+
from agentguard import Guard, CheckResult
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GuardedTool:
|
|
23
|
+
"""Wraps a LangChain tool with AgentGuard policy enforcement."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, tool: Any, guard: Guard, scope: str = "shell"):
|
|
26
|
+
self._tool = tool
|
|
27
|
+
self._guard = guard
|
|
28
|
+
self._scope = scope
|
|
29
|
+
|
|
30
|
+
# Preserve the original tool's metadata
|
|
31
|
+
self.name = tool.name
|
|
32
|
+
self.description = tool.description
|
|
33
|
+
if hasattr(tool, "args_schema"):
|
|
34
|
+
self.args_schema = tool.args_schema
|
|
35
|
+
|
|
36
|
+
def _infer_check_params(self, tool_input: Any) -> dict:
|
|
37
|
+
"""Extract meaningful parameters from tool input for policy checking."""
|
|
38
|
+
params = {}
|
|
39
|
+
|
|
40
|
+
if isinstance(tool_input, str):
|
|
41
|
+
params["command"] = tool_input
|
|
42
|
+
elif isinstance(tool_input, dict):
|
|
43
|
+
if "command" in tool_input or "cmd" in tool_input:
|
|
44
|
+
params["command"] = tool_input.get("command", tool_input.get("cmd", ""))
|
|
45
|
+
if "url" in tool_input:
|
|
46
|
+
params["url"] = tool_input["url"]
|
|
47
|
+
# Extract domain from URL
|
|
48
|
+
try:
|
|
49
|
+
from urllib.parse import urlparse
|
|
50
|
+
parsed = urlparse(tool_input["url"])
|
|
51
|
+
if parsed.hostname:
|
|
52
|
+
params["domain"] = parsed.hostname
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
if "path" in tool_input or "file_path" in tool_input:
|
|
56
|
+
params["path"] = tool_input.get("path", tool_input.get("file_path", ""))
|
|
57
|
+
# Infer action from tool name
|
|
58
|
+
name_lower = self.name.lower()
|
|
59
|
+
if "read" in name_lower or "get" in name_lower:
|
|
60
|
+
params["action"] = "read"
|
|
61
|
+
elif "write" in name_lower or "save" in name_lower or "create" in name_lower:
|
|
62
|
+
params["action"] = "write"
|
|
63
|
+
elif "delete" in name_lower or "remove" in name_lower:
|
|
64
|
+
params["action"] = "delete"
|
|
65
|
+
|
|
66
|
+
return params
|
|
67
|
+
|
|
68
|
+
def _infer_scope(self, params: dict) -> str:
|
|
69
|
+
"""Infer the appropriate policy scope from the parameters."""
|
|
70
|
+
if params.get("domain") or params.get("url"):
|
|
71
|
+
return "network"
|
|
72
|
+
if params.get("path"):
|
|
73
|
+
return "filesystem"
|
|
74
|
+
return self._scope
|
|
75
|
+
|
|
76
|
+
def run(self, tool_input: Any, **kwargs) -> Any:
|
|
77
|
+
"""Run the tool after checking with AgentGuard."""
|
|
78
|
+
params = self._infer_check_params(tool_input)
|
|
79
|
+
scope = self._infer_scope(params)
|
|
80
|
+
|
|
81
|
+
result = self._guard.check(scope, **params)
|
|
82
|
+
|
|
83
|
+
if result.allowed:
|
|
84
|
+
return self._tool.run(tool_input, **kwargs)
|
|
85
|
+
elif result.needs_approval:
|
|
86
|
+
return (
|
|
87
|
+
f"[AgentGuard] Action requires approval. "
|
|
88
|
+
f"Approve at: {result.approval_url}\n"
|
|
89
|
+
f"Reason: {result.reason}"
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
return (
|
|
93
|
+
f"[AgentGuard] Action denied.\n"
|
|
94
|
+
f"Reason: {result.reason}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
async def arun(self, tool_input: Any, **kwargs) -> Any:
|
|
98
|
+
"""Async version — policy check is synchronous, tool execution is async."""
|
|
99
|
+
params = self._infer_check_params(tool_input)
|
|
100
|
+
scope = self._infer_scope(params)
|
|
101
|
+
|
|
102
|
+
result = self._guard.check(scope, **params)
|
|
103
|
+
|
|
104
|
+
if result.allowed:
|
|
105
|
+
return await self._tool.arun(tool_input, **kwargs)
|
|
106
|
+
elif result.needs_approval:
|
|
107
|
+
return (
|
|
108
|
+
f"[AgentGuard] Action requires approval. "
|
|
109
|
+
f"Approve at: {result.approval_url}\n"
|
|
110
|
+
f"Reason: {result.reason}"
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
return (
|
|
114
|
+
f"[AgentGuard] Action denied.\n"
|
|
115
|
+
f"Reason: {result.reason}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def __getattr__(self, name: str) -> Any:
|
|
119
|
+
"""Proxy all other attributes to the wrapped tool."""
|
|
120
|
+
return getattr(self._tool, name)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class GuardedToolkit:
|
|
124
|
+
"""Wraps a list of LangChain tools with AgentGuard enforcement.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
tools: List of LangChain tools to guard
|
|
128
|
+
guard_url: URL of the AgentGuard proxy
|
|
129
|
+
agent_id: Identifier for this agent in audit logs
|
|
130
|
+
default_scope: Default policy scope for tools that can't be auto-detected
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
tools: List[Any],
|
|
136
|
+
guard_url: str = "http://localhost:8080",
|
|
137
|
+
agent_id: str = "",
|
|
138
|
+
default_scope: str = "shell",
|
|
139
|
+
):
|
|
140
|
+
self._guard = Guard(guard_url, agent_id=agent_id)
|
|
141
|
+
self._default_scope = default_scope
|
|
142
|
+
self._tools = [
|
|
143
|
+
GuardedTool(tool, self._guard, scope=self._infer_scope(tool))
|
|
144
|
+
for tool in tools
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
def _infer_scope(self, tool: Any) -> str:
|
|
148
|
+
"""Try to infer the policy scope from the tool's name/description."""
|
|
149
|
+
name = getattr(tool, "name", "").lower()
|
|
150
|
+
desc = getattr(tool, "description", "").lower()
|
|
151
|
+
combined = f"{name} {desc}"
|
|
152
|
+
|
|
153
|
+
if any(kw in combined for kw in ["http", "api", "fetch", "request", "url", "web"]):
|
|
154
|
+
return "network"
|
|
155
|
+
if any(kw in combined for kw in ["file", "read", "write", "directory", "path"]):
|
|
156
|
+
return "filesystem"
|
|
157
|
+
if any(kw in combined for kw in ["browser", "navigate", "click", "page"]):
|
|
158
|
+
return "browser"
|
|
159
|
+
if any(kw in combined for kw in ["shell", "command", "exec", "terminal", "bash"]):
|
|
160
|
+
return "shell"
|
|
161
|
+
|
|
162
|
+
return self._default_scope
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def tools(self) -> List[GuardedTool]:
|
|
166
|
+
"""The guarded tool list — drop-in replacement for unguarded tools."""
|
|
167
|
+
return self._tools
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentGuard MCP (Model Context Protocol) Adapter
|
|
3
|
+
|
|
4
|
+
Provides an MCP-compatible tool server that wraps existing tools with AgentGuard
|
|
5
|
+
policy enforcement. This allows any MCP-compatible client (Claude Desktop,
|
|
6
|
+
Cursor, etc.) to have its tool calls guarded by policy.
|
|
7
|
+
|
|
8
|
+
Usage as an MCP server (stdio transport):
|
|
9
|
+
|
|
10
|
+
python -m agentguard.adapters.mcp --policy configs/default.yaml
|
|
11
|
+
|
|
12
|
+
Or programmatically:
|
|
13
|
+
|
|
14
|
+
from agentguard.adapters.mcp import GuardedMCPServer
|
|
15
|
+
|
|
16
|
+
server = GuardedMCPServer(
|
|
17
|
+
guard_url="http://localhost:8080",
|
|
18
|
+
agent_id="mcp-agent",
|
|
19
|
+
)
|
|
20
|
+
server.add_tool(my_tool_definition, my_tool_handler)
|
|
21
|
+
server.run()
|
|
22
|
+
|
|
23
|
+
MCP config (claude_desktop_config.json / .cursor/mcp.json):
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"agentguard": {
|
|
28
|
+
"command": "python",
|
|
29
|
+
"args": ["-m", "agentguard.adapters.mcp", "--guard-url", "http://localhost:8080"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
import sys
|
|
37
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
38
|
+
from agentguard import Guard, CheckResult
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ToolDefinition:
|
|
42
|
+
"""Defines an MCP tool that can be guarded."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
name: str,
|
|
47
|
+
description: str,
|
|
48
|
+
input_schema: Optional[dict] = None,
|
|
49
|
+
scope: str = "shell",
|
|
50
|
+
):
|
|
51
|
+
self.name = name
|
|
52
|
+
self.description = description
|
|
53
|
+
self.input_schema = input_schema or {"type": "object", "properties": {}}
|
|
54
|
+
self.scope = scope
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GuardedMCPServer:
|
|
58
|
+
"""MCP server that enforces AgentGuard policies on tool calls.
|
|
59
|
+
|
|
60
|
+
This implements the MCP stdio transport protocol. Tool calls are checked
|
|
61
|
+
against the AgentGuard proxy before execution.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
guard: Optional[Guard] = None,
|
|
67
|
+
guard_url: str = "http://localhost:8080",
|
|
68
|
+
agent_id: str = "mcp-agent",
|
|
69
|
+
server_name: str = "agentguard",
|
|
70
|
+
server_version: str = "0.2.0",
|
|
71
|
+
):
|
|
72
|
+
self._guard = guard or Guard(guard_url, agent_id=agent_id)
|
|
73
|
+
self._tools: Dict[str, ToolDefinition] = {}
|
|
74
|
+
self._handlers: Dict[str, Callable] = {}
|
|
75
|
+
self._server_name = server_name
|
|
76
|
+
self._server_version = server_version
|
|
77
|
+
|
|
78
|
+
def add_tool(
|
|
79
|
+
self,
|
|
80
|
+
name: str,
|
|
81
|
+
description: str,
|
|
82
|
+
handler: Callable,
|
|
83
|
+
input_schema: Optional[dict] = None,
|
|
84
|
+
scope: str = "shell",
|
|
85
|
+
):
|
|
86
|
+
"""Register a tool with the MCP server.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
name: Tool name
|
|
90
|
+
description: Human-readable description
|
|
91
|
+
handler: Function to call when the tool is invoked
|
|
92
|
+
input_schema: JSON Schema for the tool's input
|
|
93
|
+
scope: AgentGuard policy scope for this tool
|
|
94
|
+
"""
|
|
95
|
+
self._tools[name] = ToolDefinition(name, description, input_schema, scope)
|
|
96
|
+
self._handlers[name] = handler
|
|
97
|
+
|
|
98
|
+
def _infer_check_params(self, tool: ToolDefinition, arguments: dict) -> dict:
|
|
99
|
+
"""Extract policy-relevant parameters from tool arguments."""
|
|
100
|
+
params = {}
|
|
101
|
+
|
|
102
|
+
if "command" in arguments or "cmd" in arguments:
|
|
103
|
+
params["command"] = arguments.get("command", arguments.get("cmd", ""))
|
|
104
|
+
elif tool.scope == "shell":
|
|
105
|
+
# Use the full arguments as the command representation
|
|
106
|
+
params["command"] = f"{tool.name} {json.dumps(arguments)}"
|
|
107
|
+
|
|
108
|
+
if "url" in arguments:
|
|
109
|
+
params["url"] = arguments["url"]
|
|
110
|
+
try:
|
|
111
|
+
from urllib.parse import urlparse
|
|
112
|
+
parsed = urlparse(arguments["url"])
|
|
113
|
+
if parsed.hostname:
|
|
114
|
+
params["domain"] = parsed.hostname
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
if "path" in arguments or "file_path" in arguments:
|
|
119
|
+
params["path"] = arguments.get("path", arguments.get("file_path", ""))
|
|
120
|
+
name_lower = tool.name.lower()
|
|
121
|
+
if "read" in name_lower or "get" in name_lower:
|
|
122
|
+
params["action"] = "read"
|
|
123
|
+
elif "write" in name_lower or "save" in name_lower:
|
|
124
|
+
params["action"] = "write"
|
|
125
|
+
elif "delete" in name_lower or "remove" in name_lower:
|
|
126
|
+
params["action"] = "delete"
|
|
127
|
+
|
|
128
|
+
if "domain" in arguments:
|
|
129
|
+
params["domain"] = arguments["domain"]
|
|
130
|
+
|
|
131
|
+
return params
|
|
132
|
+
|
|
133
|
+
def _handle_request(self, request: dict) -> dict:
|
|
134
|
+
"""Handle a single JSON-RPC request."""
|
|
135
|
+
method = request.get("method", "")
|
|
136
|
+
req_id = request.get("id")
|
|
137
|
+
params = request.get("params", {})
|
|
138
|
+
|
|
139
|
+
if method == "initialize":
|
|
140
|
+
return {
|
|
141
|
+
"jsonrpc": "2.0",
|
|
142
|
+
"id": req_id,
|
|
143
|
+
"result": {
|
|
144
|
+
"protocolVersion": "2024-11-05",
|
|
145
|
+
"serverInfo": {
|
|
146
|
+
"name": self._server_name,
|
|
147
|
+
"version": self._server_version,
|
|
148
|
+
},
|
|
149
|
+
"capabilities": {
|
|
150
|
+
"tools": {"listChanged": False},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if method == "tools/list":
|
|
156
|
+
tools_list = []
|
|
157
|
+
for tool in self._tools.values():
|
|
158
|
+
tools_list.append({
|
|
159
|
+
"name": tool.name,
|
|
160
|
+
"description": tool.description,
|
|
161
|
+
"inputSchema": tool.input_schema,
|
|
162
|
+
})
|
|
163
|
+
return {
|
|
164
|
+
"jsonrpc": "2.0",
|
|
165
|
+
"id": req_id,
|
|
166
|
+
"result": {"tools": tools_list},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if method == "tools/call":
|
|
170
|
+
tool_name = params.get("name", "")
|
|
171
|
+
arguments = params.get("arguments", {})
|
|
172
|
+
return self._call_tool(req_id, tool_name, arguments)
|
|
173
|
+
|
|
174
|
+
if method == "notifications/initialized":
|
|
175
|
+
# Notification, no response needed
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
# Unknown method
|
|
179
|
+
return {
|
|
180
|
+
"jsonrpc": "2.0",
|
|
181
|
+
"id": req_id,
|
|
182
|
+
"error": {"code": -32601, "message": f"Unknown method: {method}"},
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
def _call_tool(self, req_id: Any, tool_name: str, arguments: dict) -> dict:
|
|
186
|
+
"""Execute a tool call with policy enforcement."""
|
|
187
|
+
if tool_name not in self._tools:
|
|
188
|
+
return {
|
|
189
|
+
"jsonrpc": "2.0",
|
|
190
|
+
"id": req_id,
|
|
191
|
+
"error": {"code": -32602, "message": f"Unknown tool: {tool_name}"},
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
tool = self._tools[tool_name]
|
|
195
|
+
handler = self._handlers[tool_name]
|
|
196
|
+
|
|
197
|
+
# Policy check
|
|
198
|
+
check_params = self._infer_check_params(tool, arguments)
|
|
199
|
+
scope = tool.scope
|
|
200
|
+
if check_params.get("domain") or check_params.get("url"):
|
|
201
|
+
scope = "network"
|
|
202
|
+
if check_params.get("path"):
|
|
203
|
+
scope = "filesystem"
|
|
204
|
+
|
|
205
|
+
result = self._guard.check(scope, **check_params)
|
|
206
|
+
|
|
207
|
+
if result.denied:
|
|
208
|
+
return {
|
|
209
|
+
"jsonrpc": "2.0",
|
|
210
|
+
"id": req_id,
|
|
211
|
+
"result": {
|
|
212
|
+
"content": [
|
|
213
|
+
{
|
|
214
|
+
"type": "text",
|
|
215
|
+
"text": f"[AgentGuard] Action denied: {result.reason}",
|
|
216
|
+
}
|
|
217
|
+
],
|
|
218
|
+
"isError": True,
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if result.needs_approval:
|
|
223
|
+
return {
|
|
224
|
+
"jsonrpc": "2.0",
|
|
225
|
+
"id": req_id,
|
|
226
|
+
"result": {
|
|
227
|
+
"content": [
|
|
228
|
+
{
|
|
229
|
+
"type": "text",
|
|
230
|
+
"text": (
|
|
231
|
+
f"[AgentGuard] Action requires approval.\n"
|
|
232
|
+
f"Reason: {result.reason}\n"
|
|
233
|
+
f"Approve at: {result.approval_url}"
|
|
234
|
+
),
|
|
235
|
+
}
|
|
236
|
+
],
|
|
237
|
+
"isError": True,
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Action allowed — execute the handler
|
|
242
|
+
try:
|
|
243
|
+
output = handler(**arguments) if isinstance(arguments, dict) else handler(arguments)
|
|
244
|
+
if not isinstance(output, str):
|
|
245
|
+
output = json.dumps(output, default=str)
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
"jsonrpc": "2.0",
|
|
249
|
+
"id": req_id,
|
|
250
|
+
"result": {
|
|
251
|
+
"content": [{"type": "text", "text": output}],
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
except Exception as e:
|
|
255
|
+
return {
|
|
256
|
+
"jsonrpc": "2.0",
|
|
257
|
+
"id": req_id,
|
|
258
|
+
"result": {
|
|
259
|
+
"content": [{"type": "text", "text": f"Error: {e}"}],
|
|
260
|
+
"isError": True,
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
def run(self):
|
|
265
|
+
"""Run the MCP server on stdio (blocking)."""
|
|
266
|
+
for line in sys.stdin:
|
|
267
|
+
line = line.strip()
|
|
268
|
+
if not line:
|
|
269
|
+
continue
|
|
270
|
+
try:
|
|
271
|
+
request = json.loads(line)
|
|
272
|
+
except json.JSONDecodeError:
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
response = self._handle_request(request)
|
|
276
|
+
if response is not None:
|
|
277
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
278
|
+
sys.stdout.flush()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def main():
|
|
282
|
+
"""Entry point for running as `python -m agentguard.adapters.mcp`."""
|
|
283
|
+
import argparse
|
|
284
|
+
|
|
285
|
+
parser = argparse.ArgumentParser(description="AgentGuard MCP Server")
|
|
286
|
+
parser.add_argument("--guard-url", default="http://localhost:8080", help="AgentGuard proxy URL")
|
|
287
|
+
parser.add_argument("--agent-id", default="mcp-agent", help="Agent identifier")
|
|
288
|
+
args = parser.parse_args()
|
|
289
|
+
|
|
290
|
+
server = GuardedMCPServer(guard_url=args.guard_url, agent_id=args.agent_id)
|
|
291
|
+
|
|
292
|
+
# The MCP server starts with no tools — downstream MCP proxies or
|
|
293
|
+
# configurations add tools dynamically via the protocol. For standalone
|
|
294
|
+
# usage, users import GuardedMCPServer and call add_tool().
|
|
295
|
+
server.run()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
main()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentguardproxy
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for AgentGuard — the firewall for AI agents
|
|
5
|
+
Author: AgentGuard Contributors
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/Caua-ferraz/AgentGuard
|
|
8
|
+
Project-URL: Repository, https://github.com/Caua-ferraz/AgentGuard
|
|
9
|
+
Project-URL: Documentation, https://github.com/Caua-ferraz/AgentGuard/blob/master/docs/SETUP.md
|
|
10
|
+
Project-URL: Issues, https://github.com/Caua-ferraz/AgentGuard/issues
|
|
11
|
+
Keywords: ai,agents,firewall,policy,guardrails,safety
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Provides-Extra: langchain
|
|
26
|
+
Requires-Dist: langchain>=0.1.0; extra == "langchain"
|
|
27
|
+
Provides-Extra: crewai
|
|
28
|
+
Requires-Dist: crewai>=0.1.0; extra == "crewai"
|
|
29
|
+
Provides-Extra: browser-use
|
|
30
|
+
Requires-Dist: browser-use>=0.1.0; extra == "browser-use"
|
|
31
|
+
Provides-Extra: mcp
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: langchain>=0.1.0; extra == "all"
|
|
34
|
+
Requires-Dist: crewai>=0.1.0; extra == "all"
|
|
35
|
+
Requires-Dist: browser-use>=0.1.0; extra == "all"
|
|
36
|
+
|
|
37
|
+
# AgentGuard Python SDK
|
|
38
|
+
|
|
39
|
+
Lightweight Python client for [AgentGuard](https://github.com/Caua-ferraz/AgentGuard) — the firewall for AI agents.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install agentguard
|
|
45
|
+
|
|
46
|
+
# With framework adapters
|
|
47
|
+
pip install agentguard[langchain]
|
|
48
|
+
pip install agentguard[crewai]
|
|
49
|
+
pip install agentguard[browser-use]
|
|
50
|
+
pip install agentguard[all]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from agentguard import Guard
|
|
57
|
+
|
|
58
|
+
guard = Guard("http://localhost:8080", agent_id="my-agent")
|
|
59
|
+
|
|
60
|
+
# Check before executing
|
|
61
|
+
result = guard.check("shell", command="rm -rf ./old_data")
|
|
62
|
+
|
|
63
|
+
if result.allowed:
|
|
64
|
+
execute(command)
|
|
65
|
+
elif result.needs_approval:
|
|
66
|
+
print(f"Approve at: {result.approval_url}")
|
|
67
|
+
else:
|
|
68
|
+
print(f"Blocked: {result.reason}")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Framework Adapters
|
|
72
|
+
|
|
73
|
+
### LangChain
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from agentguard.adapters.langchain import GuardedToolkit
|
|
77
|
+
|
|
78
|
+
toolkit = GuardedToolkit(
|
|
79
|
+
tools=my_tools,
|
|
80
|
+
guard_url="http://localhost:8080",
|
|
81
|
+
agent_id="langchain-agent",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
agent = create_react_agent(llm, toolkit.tools, prompt)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### CrewAI
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from agentguard.adapters.crewai import guard_crew_tools
|
|
91
|
+
|
|
92
|
+
guarded_tools = guard_crew_tools(
|
|
93
|
+
tools=my_crew_tools,
|
|
94
|
+
guard_url="http://localhost:8080",
|
|
95
|
+
agent_id="crew-agent",
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### browser-use
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from agentguard.adapters.browseruse import GuardedBrowser
|
|
103
|
+
|
|
104
|
+
browser = GuardedBrowser(guard_url="http://localhost:8080")
|
|
105
|
+
|
|
106
|
+
result = browser.check_navigation("https://example.com")
|
|
107
|
+
if result.allowed:
|
|
108
|
+
await page.goto("https://example.com")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### MCP
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from agentguard.adapters.mcp import GuardedMCPServer
|
|
115
|
+
|
|
116
|
+
server = GuardedMCPServer(guard_url="http://localhost:8080")
|
|
117
|
+
server.add_tool("my_tool", "Description", handler=my_handler)
|
|
118
|
+
server.run() # Starts stdio MCP server
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## API Reference
|
|
122
|
+
|
|
123
|
+
### `Guard(base_url, agent_id="")`
|
|
124
|
+
- `check(scope, *, action, command, path, domain, url, meta)` — Check an action against policy
|
|
125
|
+
- `approve(approval_id)` — Approve a pending action
|
|
126
|
+
- `deny(approval_id)` — Deny a pending action
|
|
127
|
+
- `wait_for_approval(approval_id, timeout=300)` — Block until resolved
|
|
128
|
+
|
|
129
|
+
### `CheckResult`
|
|
130
|
+
- `.allowed` — True if action is permitted
|
|
131
|
+
- `.denied` — True if action is blocked
|
|
132
|
+
- `.needs_approval` — True if human approval required
|
|
133
|
+
- `.decision` — Raw decision string
|
|
134
|
+
- `.reason` — Explanation
|
|
135
|
+
- `.approval_url` — URL to approve (when applicable)
|
|
136
|
+
|
|
137
|
+
### `@guarded(scope, guard=None)` decorator
|
|
138
|
+
Wraps a function so it's checked before execution.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
Apache 2.0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
agentguard/__init__.py,sha256=if1mgAiWmE9conpqQymOVRYSWk8EDCeZBfRhJAgrBpg,6299
|
|
2
|
+
agentguard/adapters/__init__.py,sha256=skMoDik_MG99VgzU_rprUlqMWPY6wbD6xENTE47PNaY,317
|
|
3
|
+
agentguard/adapters/browseruse.py,sha256=7BLdvVWzyNDUTHU8viV3Z9uh2UtnxCB9SJ3xToOJIds,4022
|
|
4
|
+
agentguard/adapters/crewai.py,sha256=5uxp1mP39JxVhbDj1td6dPgUKFdJU100L1xgpti70QQ,4733
|
|
5
|
+
agentguard/adapters/langchain.py,sha256=YFSWeQtdevBx4Nmed1l1W7vL7AytbbylIst34yvBX5g,6052
|
|
6
|
+
agentguard/adapters/mcp.py,sha256=j0ZGFXioMQ3pou5vijaBxkI-ZuWYtwzdAeD4rEXXqSQ,9856
|
|
7
|
+
agentguardproxy-0.2.0.dist-info/METADATA,sha256=ba2uyeMjHKZTV-6FZl7_8Okb8yNI3_kRuIP6QaCKow0,4144
|
|
8
|
+
agentguardproxy-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
agentguardproxy-0.2.0.dist-info/top_level.txt,sha256=rEW5sAnjxXRs63ja2psmsMRMJJfi_3EAxf7crl1nHyw,11
|
|
10
|
+
agentguardproxy-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agentguard
|