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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ agentguard