flux7-mesh 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # Build / OS
2
+ *.exe
3
+ *.dll
4
+ *.so
5
+ *.dylib
6
+ /agent-mesh
7
+ /mesh
8
+ .DS_Store
9
+
10
+ # IDE
11
+ .claude
12
+ CLAUDE.md
13
+ .idea/
14
+ .vscode/
15
+ *.swp
16
+ *.swo
17
+
18
+ # Go
19
+ vendor/
20
+
21
+ # Python
22
+ .venv/
23
+ __pycache__/
24
+
25
+ # Environment
26
+ .env
27
+ .env.*
28
+
29
+ # Debug / Test artifacts
30
+ *.debug
31
+ *.test
32
+
33
+ # Project
34
+ docs/internal/
35
+ docs/positioning.md
36
+ docs/agent-landscape.md
37
+ traces.jsonl
38
+ traces-*.jsonl
39
+ state.db
40
+ *.db-wal
41
+ *.db-shm
42
+ *.local.yaml
43
+ policies/*.local.yaml
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: flux7-mesh
3
+ Version: 0.1.0
4
+ Summary: Python SDK for agent-mesh — governance mesh for AI agent tool calls
5
+ Project-URL: Homepage, https://github.com/KTCrisis/agent-mesh
6
+ Project-URL: Repository, https://github.com/KTCrisis/agent-mesh
7
+ License-Expression: MIT
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: requests>=2.28
10
+ Provides-Extra: anthropic
11
+ Requires-Dist: anthropic>=0.40; extra == 'anthropic'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # agent-mesh Python SDK
15
+
16
+ Governance mesh for AI agent tool calls. Wraps any Python function with policy enforcement, human approval, and tracing via [agent-mesh](https://github.com/KTCrisis/agent-mesh).
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install agent-mesh
22
+ pip install agent-mesh[anthropic] # with Claude API support
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ ### Direct client
28
+
29
+ ```python
30
+ from agent_mesh import AgentMesh
31
+
32
+ mesh = AgentMesh("http://localhost:9090", agent="my-agent")
33
+
34
+ # Check what's available
35
+ tools = mesh.tools()
36
+ health = mesh.health()
37
+
38
+ # Policy check only (POST /decide) — no execution
39
+ decision = mesh.decide("filesystem.write_file", {"path": "/tmp/x"})
40
+ print(decision.action) # allow | deny | human_approval
41
+
42
+ # Full proxy (POST /tool/{name}) — policy + execute + trace
43
+ decision = mesh.call_tool("filesystem.read_file", {"path": "/tmp/data.txt"})
44
+ print(decision.result)
45
+
46
+ # Manage grants
47
+ mesh.create_grant("filesystem.*", "30m")
48
+ mesh.revoke_grant("grant-id")
49
+ ```
50
+
51
+ ### GovernedToolkit (Claude API)
52
+
53
+ ```python
54
+ from agent_mesh import GovernedToolkit
55
+
56
+ toolkit = GovernedToolkit(agent="my-agent")
57
+
58
+ @toolkit.tool
59
+ def get_weather(city: str) -> str:
60
+ """Get current weather for a city."""
61
+ return fetch_weather(city)
62
+
63
+ @toolkit.tool
64
+ def send_email(to: str, subject: str, body: str) -> str:
65
+ """Send an email."""
66
+ return smtp_send(to, subject, body)
67
+
68
+ # Generate tools[] for Claude API
69
+ schemas = toolkit.schemas()
70
+
71
+ # Process tool_use blocks with governance
72
+ response = client.messages.create(model="claude-sonnet-4-6", tools=schemas, ...)
73
+ results = toolkit.process_response([b.model_dump() for b in response.content])
74
+ ```
75
+
76
+ The toolkit:
77
+ 1. **`schemas()`** — generates the `tools[]` array from Python function signatures
78
+ 2. **`process_response()`** — intercepts `tool_use` blocks, checks policy via agent-mesh, executes locally if allowed
79
+ 3. **`execute()`** — single tool call with governance
80
+
81
+ ## Prerequisites
82
+
83
+ agent-mesh daemon running:
84
+
85
+ ```bash
86
+ agent-mesh serve --config config.yaml
87
+ ```
88
+
89
+ ## API
90
+
91
+ ### `AgentMesh(url, agent, timeout)`
92
+
93
+ | Method | Description |
94
+ |---|---|
95
+ | `decide(name, args)` | Evaluate policy without executing |
96
+ | `call_tool(name, args)` | Execute tool through governance (proxy) |
97
+ | `tools()` | List available tools |
98
+ | `approvals()` | List pending approvals |
99
+ | `approve(id)` / `deny(id)` | Resolve approval |
100
+ | `grants()` | List active grants |
101
+ | `create_grant(tools, duration)` | Create temporal grant |
102
+ | `health()` / `is_healthy()` | Check mesh status |
103
+
104
+ ### `GovernedToolkit(agent, url)`
105
+
106
+ | Method | Description |
107
+ |---|---|
108
+ | `@toolkit.tool` | Decorator to register a function |
109
+ | `schemas()` | Generate Claude API tools array |
110
+ | `execute(name, input)` | Governed execution |
111
+ | `process_response(content)` | Process Claude response blocks |
@@ -0,0 +1,98 @@
1
+ # agent-mesh Python SDK
2
+
3
+ Governance mesh for AI agent tool calls. Wraps any Python function with policy enforcement, human approval, and tracing via [agent-mesh](https://github.com/KTCrisis/agent-mesh).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install agent-mesh
9
+ pip install agent-mesh[anthropic] # with Claude API support
10
+ ```
11
+
12
+ ## Quick start
13
+
14
+ ### Direct client
15
+
16
+ ```python
17
+ from agent_mesh import AgentMesh
18
+
19
+ mesh = AgentMesh("http://localhost:9090", agent="my-agent")
20
+
21
+ # Check what's available
22
+ tools = mesh.tools()
23
+ health = mesh.health()
24
+
25
+ # Policy check only (POST /decide) — no execution
26
+ decision = mesh.decide("filesystem.write_file", {"path": "/tmp/x"})
27
+ print(decision.action) # allow | deny | human_approval
28
+
29
+ # Full proxy (POST /tool/{name}) — policy + execute + trace
30
+ decision = mesh.call_tool("filesystem.read_file", {"path": "/tmp/data.txt"})
31
+ print(decision.result)
32
+
33
+ # Manage grants
34
+ mesh.create_grant("filesystem.*", "30m")
35
+ mesh.revoke_grant("grant-id")
36
+ ```
37
+
38
+ ### GovernedToolkit (Claude API)
39
+
40
+ ```python
41
+ from agent_mesh import GovernedToolkit
42
+
43
+ toolkit = GovernedToolkit(agent="my-agent")
44
+
45
+ @toolkit.tool
46
+ def get_weather(city: str) -> str:
47
+ """Get current weather for a city."""
48
+ return fetch_weather(city)
49
+
50
+ @toolkit.tool
51
+ def send_email(to: str, subject: str, body: str) -> str:
52
+ """Send an email."""
53
+ return smtp_send(to, subject, body)
54
+
55
+ # Generate tools[] for Claude API
56
+ schemas = toolkit.schemas()
57
+
58
+ # Process tool_use blocks with governance
59
+ response = client.messages.create(model="claude-sonnet-4-6", tools=schemas, ...)
60
+ results = toolkit.process_response([b.model_dump() for b in response.content])
61
+ ```
62
+
63
+ The toolkit:
64
+ 1. **`schemas()`** — generates the `tools[]` array from Python function signatures
65
+ 2. **`process_response()`** — intercepts `tool_use` blocks, checks policy via agent-mesh, executes locally if allowed
66
+ 3. **`execute()`** — single tool call with governance
67
+
68
+ ## Prerequisites
69
+
70
+ agent-mesh daemon running:
71
+
72
+ ```bash
73
+ agent-mesh serve --config config.yaml
74
+ ```
75
+
76
+ ## API
77
+
78
+ ### `AgentMesh(url, agent, timeout)`
79
+
80
+ | Method | Description |
81
+ |---|---|
82
+ | `decide(name, args)` | Evaluate policy without executing |
83
+ | `call_tool(name, args)` | Execute tool through governance (proxy) |
84
+ | `tools()` | List available tools |
85
+ | `approvals()` | List pending approvals |
86
+ | `approve(id)` / `deny(id)` | Resolve approval |
87
+ | `grants()` | List active grants |
88
+ | `create_grant(tools, duration)` | Create temporal grant |
89
+ | `health()` / `is_healthy()` | Check mesh status |
90
+
91
+ ### `GovernedToolkit(agent, url)`
92
+
93
+ | Method | Description |
94
+ |---|---|
95
+ | `@toolkit.tool` | Decorator to register a function |
96
+ | `schemas()` | Generate Claude API tools array |
97
+ | `execute(name, input)` | Governed execution |
98
+ | `process_response(content)` | Process Claude response blocks |
@@ -0,0 +1,53 @@
1
+ """Example: governed tool use with the Claude API.
2
+
3
+ Requires: pip install agent-mesh[anthropic]
4
+ Requires: agent-mesh daemon running on localhost:9090
5
+ """
6
+ from agent_mesh import GovernedToolkit
7
+
8
+ toolkit = GovernedToolkit(agent="my-agent")
9
+
10
+
11
+ @toolkit.tool
12
+ def get_weather(city: str) -> str:
13
+ """Get current weather for a city."""
14
+ return f"22C and sunny in {city}"
15
+
16
+
17
+ @toolkit.tool
18
+ def send_email(to: str, subject: str, body: str) -> str:
19
+ """Send an email to a recipient."""
20
+ print(f" -> sending email to {to}")
21
+ return f"email sent to {to}"
22
+
23
+
24
+ def main():
25
+ import anthropic
26
+
27
+ client = anthropic.Anthropic()
28
+ messages = [{"role": "user", "content": "What's the weather in Paris? Then email the result to alice@example.com"}]
29
+
30
+ while True:
31
+ response = client.messages.create(
32
+ model="claude-sonnet-4-6",
33
+ max_tokens=1024,
34
+ tools=toolkit.schemas(),
35
+ messages=messages,
36
+ )
37
+
38
+ if response.stop_reason == "end_turn":
39
+ for block in response.content:
40
+ if hasattr(block, "text"):
41
+ print(block.text)
42
+ break
43
+
44
+ if response.stop_reason == "tool_use":
45
+ tool_results = toolkit.process_response(
46
+ [b.model_dump() for b in response.content]
47
+ )
48
+ messages.append({"role": "assistant", "content": [b.model_dump() for b in response.content]})
49
+ messages.append({"role": "user", "content": tool_results})
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "flux7-mesh"
7
+ version = "0.1.0"
8
+ description = "Python SDK for agent-mesh — governance mesh for AI agent tool calls"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = ["requests>=2.28"]
13
+
14
+ [project.optional-dependencies]
15
+ anthropic = ["anthropic>=0.40"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/KTCrisis/agent-mesh"
19
+ Repository = "https://github.com/KTCrisis/agent-mesh"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/agent_mesh"]
@@ -0,0 +1,5 @@
1
+ from agent_mesh.client import AgentMesh, Decision
2
+ from agent_mesh.toolkit import GovernedToolkit, tool
3
+
4
+ __all__ = ["AgentMesh", "Decision", "GovernedToolkit", "tool"]
5
+ __version__ = "0.1.0"
@@ -0,0 +1,191 @@
1
+ """agent-mesh Python SDK — governance mesh for AI agent tool calls.
2
+
3
+ Usage::
4
+
5
+ from agent_mesh import AgentMesh
6
+
7
+ mesh = AgentMesh("http://localhost:9090", agent="my-agent")
8
+ decision = mesh.decide("filesystem.write_file", {"path": "/tmp/x"})
9
+ print(decision.action) # "allow" | "deny" | "human_approval"
10
+
11
+ tools = mesh.tools()
12
+ health = mesh.health()
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+ from enum import Enum
18
+ from typing import Any
19
+
20
+ import requests
21
+
22
+
23
+ class Action(str, Enum):
24
+ ALLOW = "allow"
25
+ DENY = "deny"
26
+ HUMAN_APPROVAL = "human_approval"
27
+ ERROR = "error"
28
+
29
+
30
+ @dataclass
31
+ class Decision:
32
+ action: Action
33
+ tool: str
34
+ result: str = ""
35
+ error: str = ""
36
+ approval_id: str = ""
37
+
38
+
39
+ @dataclass
40
+ class Tool:
41
+ name: str
42
+ description: str = ""
43
+ source: str = ""
44
+
45
+
46
+ class AgentMeshError(Exception):
47
+ pass
48
+
49
+
50
+ class AgentMesh:
51
+ def __init__(
52
+ self,
53
+ url: str = "http://localhost:9090",
54
+ agent: str = "default",
55
+ timeout: int = 300,
56
+ ) -> None:
57
+ self._url = url.rstrip("/")
58
+ self._agent = agent
59
+ self._timeout = timeout
60
+ self._session = requests.Session()
61
+ self._session.headers["Authorization"] = f"Bearer agent:{agent}"
62
+ self._session.headers["Content-Type"] = "application/json"
63
+
64
+ def decide(self, name: str, arguments: dict[str, Any] | None = None) -> Decision:
65
+ """Evaluate policy without executing. Returns allow/deny/human_approval."""
66
+ resp = self._session.post(
67
+ f"{self._url}/decide",
68
+ json={"agent": self._agent, "tool": name, "arguments": arguments or {}},
69
+ timeout=self._timeout,
70
+ )
71
+ body = resp.json() if resp.content else {}
72
+ action = body.get("action", "error")
73
+ try:
74
+ act = Action(action)
75
+ except ValueError:
76
+ act = Action.ERROR
77
+ return Decision(
78
+ action=act,
79
+ tool=name,
80
+ error=body.get("reason", "") if act == Action.DENY else "",
81
+ )
82
+
83
+ def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> Decision:
84
+ """Execute a tool call through agent-mesh (policy + execute + trace)."""
85
+ resp = self._session.post(
86
+ f"{self._url}/tool/{name}",
87
+ json=arguments or {},
88
+ timeout=self._timeout,
89
+ )
90
+ if resp.status_code == 403:
91
+ return Decision(action=Action.DENY, tool=name, error="denied by policy")
92
+ if resp.status_code == 202:
93
+ body = resp.json() if resp.content else {}
94
+ return Decision(
95
+ action=Action.HUMAN_APPROVAL,
96
+ tool=name,
97
+ approval_id=body.get("approval_id", ""),
98
+ )
99
+ if resp.status_code >= 400:
100
+ return Decision(
101
+ action=Action.ERROR,
102
+ tool=name,
103
+ error=f"HTTP {resp.status_code}: {resp.text}",
104
+ )
105
+ return Decision(
106
+ action=Action.ALLOW,
107
+ tool=name,
108
+ result=resp.text,
109
+ )
110
+
111
+ def tools(self) -> list[Tool]:
112
+ """List all available tools."""
113
+ resp = self._session.get(f"{self._url}/tools", timeout=self._timeout)
114
+ resp.raise_for_status()
115
+ items = resp.json()
116
+ return [
117
+ Tool(
118
+ name=t.get("name", ""),
119
+ description=t.get("description", ""),
120
+ source=t.get("source", ""),
121
+ )
122
+ for t in items
123
+ ]
124
+
125
+ def approvals(self) -> list[dict[str, Any]]:
126
+ """List pending approvals."""
127
+ resp = self._session.get(f"{self._url}/approvals", timeout=self._timeout)
128
+ resp.raise_for_status()
129
+ return resp.json()
130
+
131
+ def approve(self, approval_id: str) -> bool:
132
+ """Approve a pending request."""
133
+ resp = self._session.post(
134
+ f"{self._url}/approvals/{approval_id}/approve",
135
+ timeout=self._timeout,
136
+ )
137
+ return resp.status_code == 200
138
+
139
+ def deny(self, approval_id: str) -> bool:
140
+ """Deny a pending request."""
141
+ resp = self._session.post(
142
+ f"{self._url}/approvals/{approval_id}/deny",
143
+ timeout=self._timeout,
144
+ )
145
+ return resp.status_code == 200
146
+
147
+ def grants(self) -> list[dict[str, Any]]:
148
+ """List active grants."""
149
+ resp = self._session.get(f"{self._url}/grants", timeout=self._timeout)
150
+ resp.raise_for_status()
151
+ return resp.json()
152
+
153
+ def create_grant(self, tools: str, duration: str) -> dict[str, Any]:
154
+ """Create a temporal grant."""
155
+ resp = self._session.post(
156
+ f"{self._url}/grants",
157
+ json={"agent": self._agent, "tools": tools, "duration": duration},
158
+ timeout=self._timeout,
159
+ )
160
+ resp.raise_for_status()
161
+ return resp.json()
162
+
163
+ def revoke_grant(self, grant_id: str) -> bool:
164
+ """Revoke an active grant."""
165
+ resp = self._session.delete(
166
+ f"{self._url}/grants/{grant_id}",
167
+ timeout=self._timeout,
168
+ )
169
+ return resp.status_code == 200
170
+
171
+ def traces(self, limit: int = 100) -> list[dict[str, Any]]:
172
+ """Get recent traces."""
173
+ resp = self._session.get(
174
+ f"{self._url}/traces",
175
+ params={"limit": limit},
176
+ timeout=self._timeout,
177
+ )
178
+ resp.raise_for_status()
179
+ return resp.json()
180
+
181
+ def health(self) -> dict[str, Any]:
182
+ """Get mesh health status."""
183
+ try:
184
+ resp = self._session.get(f"{self._url}/health", timeout=5)
185
+ resp.raise_for_status()
186
+ return resp.json()
187
+ except requests.RequestException:
188
+ return {"status": "unreachable"}
189
+
190
+ def is_healthy(self) -> bool:
191
+ return self.health().get("status") == "ok"
@@ -0,0 +1,167 @@
1
+ """GovernedToolkit — wrap Python functions as governed tools for the Claude API.
2
+
3
+ Usage::
4
+
5
+ from agent_mesh import GovernedToolkit
6
+
7
+ toolkit = GovernedToolkit(agent="my-agent")
8
+
9
+ @toolkit.tool
10
+ def get_weather(city: str) -> str:
11
+ \"\"\"Get current weather for a city.\"\"\"
12
+ return fetch_weather(city)
13
+
14
+ # Generate tools[] for Claude API
15
+ schemas = toolkit.schemas()
16
+
17
+ # Execute tool_use blocks with governance
18
+ result = toolkit.execute(tool_name, tool_input)
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import inspect
23
+ import json
24
+ from typing import Any, Callable, get_type_hints
25
+
26
+ from agent_mesh.client import Action, AgentMesh, AgentMeshError, Decision
27
+
28
+
29
+ def _python_type_to_json(t: Any) -> str:
30
+ mapping = {str: "string", int: "integer", float: "number", bool: "boolean"}
31
+ return mapping.get(t, "string")
32
+
33
+
34
+ def _build_schema(func: Callable) -> dict[str, Any]:
35
+ """Build a JSON Schema input_schema from a function's signature and type hints."""
36
+ hints = get_type_hints(func)
37
+ sig = inspect.signature(func)
38
+ properties: dict[str, Any] = {}
39
+ required: list[str] = []
40
+
41
+ for name, param in sig.parameters.items():
42
+ prop: dict[str, Any] = {"type": _python_type_to_json(hints.get(name, str))}
43
+ desc = ""
44
+ if func.__doc__:
45
+ for line in func.__doc__.splitlines():
46
+ stripped = line.strip()
47
+ if stripped.startswith(f":param {name}:") or stripped.startswith(f"{name}:"):
48
+ desc = stripped.split(":", 2)[-1].strip()
49
+ break
50
+ if desc:
51
+ prop["description"] = desc
52
+ properties[name] = prop
53
+ if param.default is inspect.Parameter.empty:
54
+ required.append(name)
55
+
56
+ schema: dict[str, Any] = {"type": "object", "properties": properties}
57
+ if required:
58
+ schema["required"] = required
59
+ return schema
60
+
61
+
62
+ def tool(func: Callable) -> Callable:
63
+ """Standalone decorator — marks a function as a governed tool."""
64
+ func._is_governed_tool = True # type: ignore[attr-defined]
65
+ return func
66
+
67
+
68
+ class GovernedToolkit:
69
+ def __init__(
70
+ self,
71
+ agent: str = "default",
72
+ url: str = "http://localhost:9090",
73
+ mesh: AgentMesh | None = None,
74
+ ) -> None:
75
+ self._mesh = mesh or AgentMesh(url=url, agent=agent)
76
+ self._tools: dict[str, Callable] = {}
77
+
78
+ def tool(self, func: Callable) -> Callable:
79
+ """Register a function as a governed tool."""
80
+ self._tools[func.__name__] = func
81
+ func._is_governed_tool = True # type: ignore[attr-defined]
82
+ return func
83
+
84
+ def register(self, func: Callable, name: str | None = None) -> None:
85
+ """Register a function programmatically."""
86
+ key = name or func.__name__
87
+ self._tools[key] = func
88
+
89
+ def schemas(self) -> list[dict[str, Any]]:
90
+ """Generate the tools[] array for the Claude API messages endpoint."""
91
+ result = []
92
+ for name, func in self._tools.items():
93
+ result.append({
94
+ "name": name,
95
+ "description": (func.__doc__ or "").strip().split("\n")[0],
96
+ "input_schema": _build_schema(func),
97
+ })
98
+ return result
99
+
100
+ def execute(self, tool_name: str, tool_input: dict[str, Any]) -> Decision:
101
+ """Execute a tool call with governance: decide via mesh, then run locally."""
102
+ if tool_name not in self._tools:
103
+ return Decision(
104
+ action=Action.ERROR,
105
+ tool=tool_name,
106
+ error=f"unknown tool: {tool_name}",
107
+ )
108
+ decision = self._mesh.decide(tool_name, tool_input)
109
+ if decision.action != Action.ALLOW:
110
+ return decision
111
+ try:
112
+ result = self._tools[tool_name](**tool_input)
113
+ decision.result = str(result) if result is not None else ""
114
+ except Exception as e:
115
+ decision.action = Action.ERROR
116
+ decision.error = str(e)
117
+ return decision
118
+
119
+ def execute_local(self, tool_name: str, tool_input: dict[str, Any]) -> Decision:
120
+ """Execute locally without going through mesh (for tools not proxied)."""
121
+ if tool_name not in self._tools:
122
+ return Decision(
123
+ action=Action.ERROR,
124
+ tool=tool_name,
125
+ error=f"unknown tool: {tool_name}",
126
+ )
127
+ try:
128
+ result = self._tools[tool_name](**tool_input)
129
+ return Decision(
130
+ action=Action.ALLOW,
131
+ tool=tool_name,
132
+ result=str(result) if result is not None else "",
133
+ )
134
+ except Exception as e:
135
+ return Decision(action=Action.ERROR, tool=tool_name, error=str(e))
136
+
137
+ def process_response(self, content: list[dict[str, Any]]) -> list[dict[str, Any]]:
138
+ """Process tool_use blocks from a Claude API response.
139
+
140
+ Returns tool_result blocks ready to send back.
141
+ """
142
+ results = []
143
+ for block in content:
144
+ if block.get("type") != "tool_use":
145
+ continue
146
+ decision = self.execute(block["name"], block.get("input", {}))
147
+ if decision.action in (Action.DENY, Action.ERROR):
148
+ results.append({
149
+ "type": "tool_result",
150
+ "tool_use_id": block["id"],
151
+ "is_error": True,
152
+ "content": decision.error or f"denied: {decision.tool}",
153
+ })
154
+ elif decision.action == Action.HUMAN_APPROVAL:
155
+ results.append({
156
+ "type": "tool_result",
157
+ "tool_use_id": block["id"],
158
+ "is_error": True,
159
+ "content": f"awaiting human approval (id: {decision.approval_id})",
160
+ })
161
+ else:
162
+ results.append({
163
+ "type": "tool_result",
164
+ "tool_use_id": block["id"],
165
+ "content": decision.result,
166
+ })
167
+ return results
File without changes
@@ -0,0 +1,170 @@
1
+ """Tests for the agent-mesh Python SDK client."""
2
+ from __future__ import annotations
3
+
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import pytest
7
+
8
+ from agent_mesh import AgentMesh, Decision
9
+ from agent_mesh.client import Action, AgentMeshError, Tool
10
+
11
+
12
+ @pytest.fixture
13
+ def mesh():
14
+ return AgentMesh("http://localhost:9090", agent="test-agent")
15
+
16
+
17
+ class TestInit:
18
+ def test_url_trailing_slash(self):
19
+ m = AgentMesh("http://localhost:9090/")
20
+ assert m._url == "http://localhost:9090"
21
+
22
+ def test_auth_header(self):
23
+ m = AgentMesh(agent="bot")
24
+ assert m._session.headers["Authorization"] == "Bearer agent:bot"
25
+
26
+ def test_default_url(self):
27
+ m = AgentMesh()
28
+ assert m._url == "http://localhost:9090"
29
+
30
+
31
+ class TestDecide:
32
+ def test_allow(self, mesh):
33
+ mock_resp = MagicMock()
34
+ mock_resp.status_code = 200
35
+ mock_resp.content = b'{"action":"allow","rule":"allow-all","agent":"test-agent","tool":"fs.read"}'
36
+ mock_resp.json.return_value = {"action": "allow", "rule": "allow-all", "agent": "test-agent", "tool": "fs.read"}
37
+ with patch.object(mesh._session, "post", return_value=mock_resp) as mock_post:
38
+ d = mesh.decide("fs.read", {"path": "/tmp"})
39
+ assert d.action == Action.ALLOW
40
+ assert d.tool == "fs.read"
41
+ body = mock_post.call_args[1]["json"]
42
+ assert body["agent"] == "test-agent"
43
+ assert body["tool"] == "fs.read"
44
+ assert mock_post.call_args[0][0] == "http://localhost:9090/decide"
45
+
46
+ def test_deny(self, mesh):
47
+ mock_resp = MagicMock()
48
+ mock_resp.status_code = 403
49
+ mock_resp.content = b'{"action":"deny","reason":"blocked by policy"}'
50
+ mock_resp.json.return_value = {"action": "deny", "reason": "blocked by policy"}
51
+ with patch.object(mesh._session, "post", return_value=mock_resp):
52
+ d = mesh.decide("dangerous.tool", {})
53
+ assert d.action == Action.DENY
54
+ assert "blocked" in d.error
55
+
56
+ def test_human_approval(self, mesh):
57
+ mock_resp = MagicMock()
58
+ mock_resp.status_code = 200
59
+ mock_resp.content = b'{"action":"human_approval","rule":"needs-approval"}'
60
+ mock_resp.json.return_value = {"action": "human_approval", "rule": "needs-approval"}
61
+ with patch.object(mesh._session, "post", return_value=mock_resp):
62
+ d = mesh.decide("fs.write", {})
63
+ assert d.action == Action.HUMAN_APPROVAL
64
+
65
+
66
+ class TestCallTool:
67
+ def test_allowed(self, mesh):
68
+ mock_resp = MagicMock()
69
+ mock_resp.status_code = 200
70
+ mock_resp.text = '{"result": "ok"}'
71
+ with patch.object(mesh._session, "post", return_value=mock_resp) as mock_post:
72
+ d = mesh.call_tool("mesh.catalog", {})
73
+ assert d.action == Action.ALLOW
74
+ assert d.tool == "mesh.catalog"
75
+ assert d.result == '{"result": "ok"}'
76
+ assert mock_post.call_args[0][0] == "http://localhost:9090/tool/mesh.catalog"
77
+
78
+ def test_denied(self, mesh):
79
+ mock_resp = MagicMock()
80
+ mock_resp.status_code = 403
81
+ with patch.object(mesh._session, "post", return_value=mock_resp):
82
+ d = mesh.call_tool("dangerous.tool", {})
83
+ assert d.action == Action.DENY
84
+
85
+ def test_human_approval(self, mesh):
86
+ mock_resp = MagicMock()
87
+ mock_resp.status_code = 202
88
+ mock_resp.content = b'{"approval_id": "abc123"}'
89
+ mock_resp.json.return_value = {"approval_id": "abc123"}
90
+ with patch.object(mesh._session, "post", return_value=mock_resp):
91
+ d = mesh.call_tool("fs.write", {"path": "/etc/passwd"})
92
+ assert d.action == Action.HUMAN_APPROVAL
93
+ assert d.approval_id == "abc123"
94
+
95
+ def test_server_error(self, mesh):
96
+ mock_resp = MagicMock()
97
+ mock_resp.status_code = 500
98
+ mock_resp.text = "internal error"
99
+ with patch.object(mesh._session, "post", return_value=mock_resp):
100
+ d = mesh.call_tool("broken", {})
101
+ assert d.action == Action.ERROR
102
+ assert "500" in d.error
103
+
104
+
105
+ class TestTools:
106
+ def test_list_tools(self, mesh):
107
+ mock_resp = MagicMock()
108
+ mock_resp.json.return_value = [
109
+ {"name": "fs.read", "description": "Read a file", "source": "filesystem"},
110
+ {"name": "git.status", "description": "Git status", "source": "git"},
111
+ ]
112
+ mock_resp.raise_for_status = MagicMock()
113
+ with patch.object(mesh._session, "get", return_value=mock_resp):
114
+ tools = mesh.tools()
115
+ assert len(tools) == 2
116
+ assert isinstance(tools[0], Tool)
117
+ assert tools[0].name == "fs.read"
118
+ assert tools[1].source == "git"
119
+
120
+
121
+ class TestApprovals:
122
+ def test_approve(self, mesh):
123
+ mock_resp = MagicMock()
124
+ mock_resp.status_code = 200
125
+ with patch.object(mesh._session, "post", return_value=mock_resp) as mock_post:
126
+ assert mesh.approve("abc123") is True
127
+ assert "/approvals/abc123/approve" in mock_post.call_args[0][0]
128
+
129
+ def test_deny(self, mesh):
130
+ mock_resp = MagicMock()
131
+ mock_resp.status_code = 200
132
+ with patch.object(mesh._session, "post", return_value=mock_resp):
133
+ assert mesh.deny("abc123") is True
134
+
135
+
136
+ class TestGrants:
137
+ def test_create_grant(self, mesh):
138
+ mock_resp = MagicMock()
139
+ mock_resp.json.return_value = {"id": "g1", "tools": "fs.*", "duration": "30m"}
140
+ mock_resp.raise_for_status = MagicMock()
141
+ with patch.object(mesh._session, "post", return_value=mock_resp) as mock_post:
142
+ g = mesh.create_grant("fs.*", "30m")
143
+ assert g["id"] == "g1"
144
+ body = mock_post.call_args[1]["json"]
145
+ assert body["agent"] == "test-agent"
146
+ assert body["tools"] == "fs.*"
147
+
148
+ def test_revoke_grant(self, mesh):
149
+ mock_resp = MagicMock()
150
+ mock_resp.status_code = 200
151
+ with patch.object(mesh._session, "delete", return_value=mock_resp):
152
+ assert mesh.revoke_grant("g1") is True
153
+
154
+
155
+ class TestHealth:
156
+ def test_healthy(self, mesh):
157
+ mock_resp = MagicMock()
158
+ mock_resp.json.return_value = {"status": "ok", "tools": 42}
159
+ mock_resp.raise_for_status = MagicMock()
160
+ with patch.object(mesh._session, "get", return_value=mock_resp):
161
+ assert mesh.is_healthy() is True
162
+
163
+ def test_unreachable(self, mesh):
164
+ with patch.object(mesh._session, "get", side_effect=requests.ConnectionError):
165
+ h = mesh.health()
166
+ assert h["status"] == "unreachable"
167
+ assert mesh.is_healthy() is False
168
+
169
+
170
+ import requests
@@ -0,0 +1,134 @@
1
+ """Tests for the GovernedToolkit."""
2
+ from __future__ import annotations
3
+
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import pytest
7
+
8
+ from agent_mesh import GovernedToolkit
9
+ from agent_mesh.client import Action, AgentMesh, Decision
10
+
11
+
12
+ @pytest.fixture
13
+ def toolkit():
14
+ tk = GovernedToolkit(agent="test", url="http://localhost:9090")
15
+
16
+ @tk.tool
17
+ def get_weather(city: str, units: str = "celsius") -> str:
18
+ """Get current weather for a city."""
19
+ return f"sunny in {city}"
20
+
21
+ @tk.tool
22
+ def add_numbers(a: int, b: int) -> int:
23
+ """Add two numbers."""
24
+ return a + b
25
+
26
+ return tk
27
+
28
+
29
+ class TestSchemas:
30
+ def test_generates_schemas(self, toolkit):
31
+ schemas = toolkit.schemas()
32
+ assert len(schemas) == 2
33
+ weather = next(s for s in schemas if s["name"] == "get_weather")
34
+ assert weather["description"] == "Get current weather for a city."
35
+ assert weather["input_schema"]["properties"]["city"]["type"] == "string"
36
+ assert weather["input_schema"]["properties"]["units"]["type"] == "string"
37
+ assert "city" in weather["input_schema"]["required"]
38
+ assert "units" not in weather["input_schema"]["required"]
39
+
40
+ def test_integer_types(self, toolkit):
41
+ schemas = toolkit.schemas()
42
+ add = next(s for s in schemas if s["name"] == "add_numbers")
43
+ assert add["input_schema"]["properties"]["a"]["type"] == "integer"
44
+ assert add["input_schema"]["required"] == ["a", "b"]
45
+
46
+
47
+ class TestExecute:
48
+ def test_allowed_runs_locally(self, toolkit):
49
+ mock_decision = Decision(action=Action.ALLOW, tool="get_weather", result="")
50
+ with patch.object(toolkit._mesh, "decide", return_value=mock_decision):
51
+ d = toolkit.execute("get_weather", {"city": "Paris"})
52
+ assert d.action == Action.ALLOW
53
+ assert d.result == "sunny in Paris"
54
+
55
+ def test_denied_does_not_run(self, toolkit):
56
+ mock_decision = Decision(action=Action.DENY, tool="get_weather", error="denied")
57
+ with patch.object(toolkit._mesh, "decide", return_value=mock_decision):
58
+ d = toolkit.execute("get_weather", {"city": "Paris"})
59
+ assert d.action == Action.DENY
60
+ assert d.result == ""
61
+
62
+ def test_unknown_tool(self, toolkit):
63
+ d = toolkit.execute("nonexistent", {})
64
+ assert d.action == Action.ERROR
65
+ assert "unknown tool" in d.error
66
+
67
+ def test_function_exception(self, toolkit):
68
+ def failing_tool():
69
+ raise ValueError("boom")
70
+
71
+ toolkit.register(failing_tool, "failing")
72
+ mock_decision = Decision(action=Action.ALLOW, tool="failing", result="")
73
+ with patch.object(toolkit._mesh, "decide", return_value=mock_decision):
74
+ d = toolkit.execute("failing", {})
75
+ assert d.action == Action.ERROR
76
+ assert "boom" in d.error
77
+
78
+
79
+ class TestProcessResponse:
80
+ def test_processes_tool_use_blocks(self, toolkit):
81
+ mock_decision = Decision(action=Action.ALLOW, tool="get_weather", result="")
82
+ with patch.object(toolkit._mesh, "decide", return_value=mock_decision):
83
+ results = toolkit.process_response([
84
+ {"type": "text", "text": "Let me check the weather."},
85
+ {"type": "tool_use", "id": "tu_1", "name": "get_weather", "input": {"city": "Lyon"}},
86
+ ])
87
+ assert len(results) == 1
88
+ assert results[0]["type"] == "tool_result"
89
+ assert results[0]["tool_use_id"] == "tu_1"
90
+ assert results[0]["content"] == "sunny in Lyon"
91
+
92
+ def test_denied_tool_returns_error(self, toolkit):
93
+ mock_decision = Decision(action=Action.DENY, tool="get_weather", error="denied by policy")
94
+ with patch.object(toolkit._mesh, "decide", return_value=mock_decision):
95
+ results = toolkit.process_response([
96
+ {"type": "tool_use", "id": "tu_2", "name": "get_weather", "input": {"city": "Lyon"}},
97
+ ])
98
+ assert results[0]["is_error"] is True
99
+ assert "denied" in results[0]["content"]
100
+
101
+ def test_approval_pending(self, toolkit):
102
+ mock_decision = Decision(
103
+ action=Action.HUMAN_APPROVAL, tool="get_weather", approval_id="ap_123"
104
+ )
105
+ with patch.object(toolkit._mesh, "decide", return_value=mock_decision):
106
+ results = toolkit.process_response([
107
+ {"type": "tool_use", "id": "tu_3", "name": "get_weather", "input": {"city": "Lyon"}},
108
+ ])
109
+ assert results[0]["is_error"] is True
110
+ assert "ap_123" in results[0]["content"]
111
+
112
+
113
+ class TestRegister:
114
+ def test_register_programmatic(self):
115
+ tk = GovernedToolkit(agent="test")
116
+
117
+ def my_func(x: str) -> str:
118
+ """Do something."""
119
+ return x.upper()
120
+
121
+ tk.register(my_func, "custom_name")
122
+ schemas = tk.schemas()
123
+ assert schemas[0]["name"] == "custom_name"
124
+
125
+
126
+ class TestExecuteLocal:
127
+ def test_runs_without_mesh(self, toolkit):
128
+ d = toolkit.execute_local("add_numbers", {"a": 3, "b": 4})
129
+ assert d.action == Action.ALLOW
130
+ assert d.result == "7"
131
+
132
+ def test_unknown_tool(self, toolkit):
133
+ d = toolkit.execute_local("nope", {})
134
+ assert d.action == Action.ERROR