opentrust-sdk 1.0.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.
- opentrust_sdk-1.0.0/PKG-INFO +60 -0
- opentrust_sdk-1.0.0/README.md +44 -0
- opentrust_sdk-1.0.0/pyproject.toml +28 -0
- opentrust_sdk-1.0.0/setup.cfg +4 -0
- opentrust_sdk-1.0.0/src/opentrust/__init__.py +85 -0
- opentrust_sdk-1.0.0/src/opentrust/_client.py +31 -0
- opentrust_sdk-1.0.0/src/opentrust/_recommend.py +60 -0
- opentrust_sdk-1.0.0/src/opentrust/_types.py +21 -0
- opentrust_sdk-1.0.0/src/opentrust/mcp.py +69 -0
- opentrust_sdk-1.0.0/src/opentrust_sdk.egg-info/PKG-INFO +60 -0
- opentrust_sdk-1.0.0/src/opentrust_sdk.egg-info/SOURCES.txt +17 -0
- opentrust_sdk-1.0.0/src/opentrust_sdk.egg-info/dependency_links.txt +1 -0
- opentrust_sdk-1.0.0/src/opentrust_sdk.egg-info/entry_points.txt +2 -0
- opentrust_sdk-1.0.0/src/opentrust_sdk.egg-info/requires.txt +4 -0
- opentrust_sdk-1.0.0/src/opentrust_sdk.egg-info/top_level.txt +1 -0
- opentrust_sdk-1.0.0/tests/test_client.py +52 -0
- opentrust_sdk-1.0.0/tests/test_mcp.py +85 -0
- opentrust_sdk-1.0.0/tests/test_recommend.py +76 -0
- opentrust_sdk-1.0.0/tests/test_sdk.py +90 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opentrust-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK and MCP bridge for the OpenTrust tool trust registry
|
|
5
|
+
Author-email: Novel Hut Studios <founder@novelhut.net>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Costder/opentrust
|
|
8
|
+
Project-URL: Repository, https://github.com/Costder/opentrust
|
|
9
|
+
Project-URL: Issues, https://github.com/Costder/opentrust/issues
|
|
10
|
+
Keywords: opentrust,ai-agents,mcp,trust-registry,tool-passports
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: httpx>=0.28
|
|
14
|
+
Provides-Extra: mcp
|
|
15
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
16
|
+
|
|
17
|
+
# opentrust-sdk
|
|
18
|
+
|
|
19
|
+
Python SDK for OpenTrust, the trust registry and passport layer for AI-agent tools.
|
|
20
|
+
|
|
21
|
+
Use it to verify tools, fetch passports, search the registry, and expose an MCP bridge for agent runtimes.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install opentrust-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For MCP support:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install 'opentrust-sdk[mcp]'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Basic usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from opentrust import verify
|
|
40
|
+
|
|
41
|
+
async def main():
|
|
42
|
+
result = await verify('github/file-search-mcp')
|
|
43
|
+
print(result)
|
|
44
|
+
|
|
45
|
+
asyncio.run(main())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## MCP bridge
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
opentrust-mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Repository
|
|
55
|
+
|
|
56
|
+
https://github.com/Costder/opentrust
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# opentrust-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for OpenTrust, the trust registry and passport layer for AI-agent tools.
|
|
4
|
+
|
|
5
|
+
Use it to verify tools, fetch passports, search the registry, and expose an MCP bridge for agent runtimes.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install opentrust-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For MCP support:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install 'opentrust-sdk[mcp]'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Basic usage
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import asyncio
|
|
23
|
+
from opentrust import verify
|
|
24
|
+
|
|
25
|
+
async def main():
|
|
26
|
+
result = await verify('github/file-search-mcp')
|
|
27
|
+
print(result)
|
|
28
|
+
|
|
29
|
+
asyncio.run(main())
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## MCP bridge
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
opentrust-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Repository
|
|
39
|
+
|
|
40
|
+
https://github.com/Costder/opentrust
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=82", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "opentrust-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python SDK and MCP bridge for the OpenTrust tool trust registry"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Novel Hut Studios", email = "founder@novelhut.net" }]
|
|
13
|
+
keywords = ["opentrust", "ai-agents", "mcp", "trust-registry", "tool-passports"]
|
|
14
|
+
dependencies = ["httpx>=0.28"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/Costder/opentrust"
|
|
18
|
+
Repository = "https://github.com/Costder/opentrust"
|
|
19
|
+
Issues = "https://github.com/Costder/opentrust/issues"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
mcp = ["mcp>=1.0"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
opentrust-mcp = "opentrust.mcp:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""opentrust — Python SDK for the OpenTrust tool trust registry."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from ._client import _Client
|
|
7
|
+
from ._recommend import TRUST_LEVELS, recommend, risk_level
|
|
8
|
+
from ._types import ToolsPage, VerifyResult
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__all__ = [
|
|
12
|
+
"verify", "get", "search", "list",
|
|
13
|
+
"verify_sync", "get_sync",
|
|
14
|
+
"VerifyResult", "ToolsPage",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_result(passport: dict) -> VerifyResult:
|
|
19
|
+
status = passport.get("trust_status", "auto_generated_draft")
|
|
20
|
+
level = TRUST_LEVELS.get(status, 1)
|
|
21
|
+
perms = passport.get("permission_manifest") or {}
|
|
22
|
+
return VerifyResult(
|
|
23
|
+
slug=passport["slug"],
|
|
24
|
+
trust_status=status,
|
|
25
|
+
trust_level=level,
|
|
26
|
+
is_disputed=(status == "disputed"),
|
|
27
|
+
recommendation=recommend(status, perms),
|
|
28
|
+
risk=risk_level(status, perms),
|
|
29
|
+
passport=passport,
|
|
30
|
+
permissions=perms,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def verify(slug: str, *, api_url: str | None = None) -> VerifyResult:
|
|
35
|
+
"""Fetch a passport and return a VerifyResult with recommendation and risk level."""
|
|
36
|
+
passport = await get(slug, api_url=api_url)
|
|
37
|
+
return _build_result(passport)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def get(slug: str, *, api_url: str | None = None) -> dict:
|
|
41
|
+
"""Fetch the raw passport dict for a slug."""
|
|
42
|
+
client = _Client(base_url=api_url)
|
|
43
|
+
return await client.get(f"/tools/{slug}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def search(
|
|
47
|
+
query: str,
|
|
48
|
+
*,
|
|
49
|
+
trust_status: str | None = None,
|
|
50
|
+
api_url: str | None = None,
|
|
51
|
+
) -> list[dict]:
|
|
52
|
+
"""Search tools by query. Returns list of raw passport dicts."""
|
|
53
|
+
client = _Client(base_url=api_url)
|
|
54
|
+
page = await client.get("/tools", q=query, trust_status=trust_status)
|
|
55
|
+
return page.get("items", [])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def list( # noqa: A001
|
|
59
|
+
*,
|
|
60
|
+
page: int = 1,
|
|
61
|
+
limit: int = 20,
|
|
62
|
+
trust_status: str | None = None,
|
|
63
|
+
api_url: str | None = None,
|
|
64
|
+
) -> ToolsPage:
|
|
65
|
+
"""List tools with optional filters. Returns a ToolsPage."""
|
|
66
|
+
client = _Client(base_url=api_url)
|
|
67
|
+
data = await client.get(
|
|
68
|
+
"/tools", page=page, limit=limit, trust_status=trust_status
|
|
69
|
+
)
|
|
70
|
+
return ToolsPage(
|
|
71
|
+
items=data.get("items", []),
|
|
72
|
+
total=data.get("total", 0),
|
|
73
|
+
page=data.get("page", page),
|
|
74
|
+
limit=data.get("limit", limit),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def verify_sync(slug: str, *, api_url: str | None = None) -> VerifyResult:
|
|
79
|
+
"""Synchronous wrapper for verify(). Do not call from an async context."""
|
|
80
|
+
return asyncio.run(verify(slug, api_url=api_url))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_sync(slug: str, *, api_url: str | None = None) -> dict:
|
|
84
|
+
"""Synchronous wrapper for get(). Do not call from an async context."""
|
|
85
|
+
return asyncio.run(get(slug, api_url=api_url))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Internal async HTTP client wrapping httpx."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
_DEFAULT_URL = "https://api-kappa-pied-59.vercel.app"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _Client:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
base_url: str | None = None,
|
|
16
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
self._base = (
|
|
19
|
+
base_url or os.getenv("OPENTRUST_API_URL") or _DEFAULT_URL
|
|
20
|
+
).rstrip("/")
|
|
21
|
+
self._transport = transport
|
|
22
|
+
|
|
23
|
+
async def get(self, path: str, **params: Any) -> Any:
|
|
24
|
+
"""GET /api/v1{path} with optional query params (None values are dropped)."""
|
|
25
|
+
filtered = {k: v for k, v in params.items() if v is not None}
|
|
26
|
+
async with httpx.AsyncClient(
|
|
27
|
+
base_url=self._base, transport=self._transport
|
|
28
|
+
) as client:
|
|
29
|
+
resp = await client.get(f"/api/v1{path}", params=filtered)
|
|
30
|
+
resp.raise_for_status()
|
|
31
|
+
return resp.json()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Recommendation table and risk logic for OpenTrust trust statuses."""
|
|
2
|
+
|
|
3
|
+
TRUST_LEVELS: dict[str, int] = {
|
|
4
|
+
"auto_generated_draft": 1,
|
|
5
|
+
"creator_claimed": 2,
|
|
6
|
+
"seller_confirmed": 3,
|
|
7
|
+
"community_reviewed": 4,
|
|
8
|
+
"reviewer_signed": 5,
|
|
9
|
+
"security_checked": 6,
|
|
10
|
+
"continuously_monitored": 7,
|
|
11
|
+
"disputed": 0,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_RECOMMENDATIONS: dict[int, str] = {
|
|
15
|
+
0: "⛔ Under active dispute. Do not use until resolved.",
|
|
16
|
+
1: "Auto-generated draft. Do not use in any agent workflow.",
|
|
17
|
+
2: "Creator claimed. Verify source independently before use.",
|
|
18
|
+
3: "Seller confirmed. Suitable for sandboxed/test environments only.",
|
|
19
|
+
4: "Community reviewed. Safe for low-risk tasks. Require level 6+ for production.",
|
|
20
|
+
5: "Reviewer signed. Suitable for most production tasks without sensitive permissions.",
|
|
21
|
+
6: "Security checked. Safe for production including sensitive permissions.",
|
|
22
|
+
7: "Continuously monitored. Highest trust level available.",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _perm_active(val: object) -> bool:
|
|
27
|
+
"""Return True if a permission value represents an active/granted permission."""
|
|
28
|
+
if val is True:
|
|
29
|
+
return True
|
|
30
|
+
if val and isinstance(val, dict):
|
|
31
|
+
return any(
|
|
32
|
+
v is True or (isinstance(v, list) and len(v) > 0)
|
|
33
|
+
for v in val.values()
|
|
34
|
+
)
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def recommend(trust_status: str, permission_manifest: dict) -> str:
|
|
39
|
+
"""Return a plain-English recommendation for a trust status + permission manifest."""
|
|
40
|
+
level = TRUST_LEVELS.get(trust_status, 1)
|
|
41
|
+
text = _RECOMMENDATIONS.get(level, _RECOMMENDATIONS[1])
|
|
42
|
+
if _perm_active(permission_manifest.get("wallet")):
|
|
43
|
+
text += " ⚠ Wallet access active — verify payment amounts before use."
|
|
44
|
+
if _perm_active(permission_manifest.get("terminal")):
|
|
45
|
+
text += " ⚠ Terminal access active — review allowed commands carefully."
|
|
46
|
+
return text
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def risk_level(trust_status: str, permission_manifest: dict) -> str:
|
|
50
|
+
"""Return 'low', 'medium', or 'high' risk for a trust status + permission manifest."""
|
|
51
|
+
if trust_status == "disputed":
|
|
52
|
+
return "high"
|
|
53
|
+
level = TRUST_LEVELS.get(trust_status, 1)
|
|
54
|
+
dangerous = {"wallet", "terminal", "private_data", "browser"}
|
|
55
|
+
n = sum(1 for k in dangerous if _perm_active(permission_manifest.get(k)))
|
|
56
|
+
if level <= 2 or n >= 2:
|
|
57
|
+
return "high"
|
|
58
|
+
if n == 1 or level <= 4:
|
|
59
|
+
return "medium"
|
|
60
|
+
return "low"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class VerifyResult:
|
|
6
|
+
slug: str
|
|
7
|
+
trust_status: str # e.g. "community_reviewed"
|
|
8
|
+
trust_level: int # 1–7; 0 if trust_status == "disputed"
|
|
9
|
+
is_disputed: bool # True when trust_status == "disputed"
|
|
10
|
+
recommendation: str # plain-English guidance
|
|
11
|
+
risk: str # "low" | "medium" | "high"
|
|
12
|
+
passport: dict # full raw passport
|
|
13
|
+
permissions: dict # permission_manifest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ToolsPage:
|
|
18
|
+
items: list[dict]
|
|
19
|
+
total: int
|
|
20
|
+
page: int
|
|
21
|
+
limit: int
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""OpenTrust MCP server — exposes trust registry as MCP tools via stdio."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
except ImportError as exc:
|
|
7
|
+
raise ImportError(
|
|
8
|
+
"MCP server requires the mcp extra: pip install opentrust-sdk[mcp]"
|
|
9
|
+
) from exc
|
|
10
|
+
|
|
11
|
+
import opentrust
|
|
12
|
+
|
|
13
|
+
mcp_server = FastMCP(
|
|
14
|
+
"OpenTrust",
|
|
15
|
+
instructions=(
|
|
16
|
+
"Query the OpenTrust tool trust registry. "
|
|
17
|
+
"Use verify_tool before calling any external tool to check its "
|
|
18
|
+
"trust status and permissions."
|
|
19
|
+
),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@mcp_server.tool()
|
|
24
|
+
async def verify_tool(slug: str) -> dict:
|
|
25
|
+
"""Look up a tool's trust passport and get a plain-English safety recommendation."""
|
|
26
|
+
result = await opentrust.verify(slug)
|
|
27
|
+
return {
|
|
28
|
+
"passport": result.passport,
|
|
29
|
+
"trust_status": result.trust_status,
|
|
30
|
+
"trust_level": result.trust_level,
|
|
31
|
+
"is_disputed": result.is_disputed,
|
|
32
|
+
"recommendation": result.recommendation,
|
|
33
|
+
"risk": result.risk,
|
|
34
|
+
"permissions": result.permissions,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@mcp_server.tool()
|
|
39
|
+
async def search_tools(query: str, trust_status: str = "") -> list:
|
|
40
|
+
"""Search the OpenTrust registry for tools matching a query."""
|
|
41
|
+
tools = await opentrust.search(query, trust_status=trust_status or None)
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
"slug": t.get("slug"),
|
|
45
|
+
"name": t.get("name"),
|
|
46
|
+
"trust_status": t.get("trust_status"),
|
|
47
|
+
"description": t.get("description", ""),
|
|
48
|
+
}
|
|
49
|
+
for t in tools
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@mcp_server.tool()
|
|
54
|
+
async def list_tools(
|
|
55
|
+
page: int = 1, limit: int = 20, trust_status: str = ""
|
|
56
|
+
) -> dict:
|
|
57
|
+
"""List registered tools, optionally filtered by trust level."""
|
|
58
|
+
page_result = await opentrust.list(
|
|
59
|
+
page=page, limit=limit, trust_status=trust_status or None
|
|
60
|
+
)
|
|
61
|
+
return {"items": page_result.items, "total": page_result.total}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> None:
|
|
65
|
+
mcp_server.run()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opentrust-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK and MCP bridge for the OpenTrust tool trust registry
|
|
5
|
+
Author-email: Novel Hut Studios <founder@novelhut.net>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Costder/opentrust
|
|
8
|
+
Project-URL: Repository, https://github.com/Costder/opentrust
|
|
9
|
+
Project-URL: Issues, https://github.com/Costder/opentrust/issues
|
|
10
|
+
Keywords: opentrust,ai-agents,mcp,trust-registry,tool-passports
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: httpx>=0.28
|
|
14
|
+
Provides-Extra: mcp
|
|
15
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
16
|
+
|
|
17
|
+
# opentrust-sdk
|
|
18
|
+
|
|
19
|
+
Python SDK for OpenTrust, the trust registry and passport layer for AI-agent tools.
|
|
20
|
+
|
|
21
|
+
Use it to verify tools, fetch passports, search the registry, and expose an MCP bridge for agent runtimes.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install opentrust-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For MCP support:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install 'opentrust-sdk[mcp]'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Basic usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from opentrust import verify
|
|
40
|
+
|
|
41
|
+
async def main():
|
|
42
|
+
result = await verify('github/file-search-mcp')
|
|
43
|
+
print(result)
|
|
44
|
+
|
|
45
|
+
asyncio.run(main())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## MCP bridge
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
opentrust-mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Repository
|
|
55
|
+
|
|
56
|
+
https://github.com/Costder/opentrust
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/opentrust/__init__.py
|
|
4
|
+
src/opentrust/_client.py
|
|
5
|
+
src/opentrust/_recommend.py
|
|
6
|
+
src/opentrust/_types.py
|
|
7
|
+
src/opentrust/mcp.py
|
|
8
|
+
src/opentrust_sdk.egg-info/PKG-INFO
|
|
9
|
+
src/opentrust_sdk.egg-info/SOURCES.txt
|
|
10
|
+
src/opentrust_sdk.egg-info/dependency_links.txt
|
|
11
|
+
src/opentrust_sdk.egg-info/entry_points.txt
|
|
12
|
+
src/opentrust_sdk.egg-info/requires.txt
|
|
13
|
+
src/opentrust_sdk.egg-info/top_level.txt
|
|
14
|
+
tests/test_client.py
|
|
15
|
+
tests/test_mcp.py
|
|
16
|
+
tests/test_recommend.py
|
|
17
|
+
tests/test_sdk.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
opentrust
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import httpx
|
|
3
|
+
from opentrust._client import _Client
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _transport(data: dict, status: int = 200) -> httpx.MockTransport:
|
|
7
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
8
|
+
return httpx.Response(status, json=data)
|
|
9
|
+
return httpx.MockTransport(handler)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.asyncio
|
|
13
|
+
async def test_get_returns_json():
|
|
14
|
+
client = _Client(base_url="http://test", transport=_transport({"slug": "t"}))
|
|
15
|
+
result = await client.get("/tools/t")
|
|
16
|
+
assert result["slug"] == "t"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
async def test_get_raises_on_404():
|
|
21
|
+
client = _Client(base_url="http://test", transport=_transport({"detail": "not found"}, 404))
|
|
22
|
+
with pytest.raises(httpx.HTTPStatusError):
|
|
23
|
+
await client.get("/tools/missing")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.asyncio
|
|
27
|
+
async def test_get_strips_none_params():
|
|
28
|
+
seen: list[str] = []
|
|
29
|
+
|
|
30
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
31
|
+
seen.append(str(request.url))
|
|
32
|
+
return httpx.Response(200, json={"items": [], "total": 0, "page": 1, "limit": 20})
|
|
33
|
+
|
|
34
|
+
client = _Client(base_url="http://test", transport=httpx.MockTransport(handler))
|
|
35
|
+
await client.get("/tools", q=None, trust_status=None, page=1)
|
|
36
|
+
assert "q=" not in seen[0]
|
|
37
|
+
assert "trust_status=" not in seen[0]
|
|
38
|
+
assert "page=1" in seen[0]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
async def test_get_includes_non_none_params():
|
|
43
|
+
seen: list[str] = []
|
|
44
|
+
|
|
45
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
46
|
+
seen.append(str(request.url))
|
|
47
|
+
return httpx.Response(200, json={"items": [], "total": 0, "page": 1, "limit": 20})
|
|
48
|
+
|
|
49
|
+
client = _Client(base_url="http://test", transport=httpx.MockTransport(handler))
|
|
50
|
+
await client.get("/tools", q="github", trust_status="community_reviewed")
|
|
51
|
+
assert "q=github" in seen[0]
|
|
52
|
+
assert "trust_status=community_reviewed" in seen[0]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Tests for the MCP server tools. Calls tool functions directly — no MCP transport."""
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import AsyncMock, patch
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from opentrust.mcp import verify_tool, search_tools, list_tools
|
|
7
|
+
MCP_AVAILABLE = True
|
|
8
|
+
except ImportError:
|
|
9
|
+
MCP_AVAILABLE = False
|
|
10
|
+
|
|
11
|
+
from opentrust._types import ToolsPage, VerifyResult
|
|
12
|
+
|
|
13
|
+
pytestmark = pytest.mark.skipif(not MCP_AVAILABLE, reason="mcp extra not installed")
|
|
14
|
+
|
|
15
|
+
FAKE_RESULT = VerifyResult(
|
|
16
|
+
slug="test-tool",
|
|
17
|
+
trust_status="community_reviewed",
|
|
18
|
+
trust_level=4,
|
|
19
|
+
is_disputed=False,
|
|
20
|
+
recommendation="Community reviewed. Safe for low-risk tasks.",
|
|
21
|
+
risk="medium",
|
|
22
|
+
passport={"slug": "test-tool", "name": "Test"},
|
|
23
|
+
permissions={"network": True},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.asyncio
|
|
28
|
+
async def test_verify_tool_returns_all_expected_keys():
|
|
29
|
+
with patch("opentrust.mcp.opentrust.verify", new_callable=AsyncMock) as m:
|
|
30
|
+
m.return_value = FAKE_RESULT
|
|
31
|
+
result = await verify_tool("test-tool")
|
|
32
|
+
assert result["trust_status"] == "community_reviewed"
|
|
33
|
+
assert result["trust_level"] == 4
|
|
34
|
+
assert result["is_disputed"] is False
|
|
35
|
+
assert "recommendation" in result
|
|
36
|
+
assert "permissions" in result
|
|
37
|
+
assert "passport" in result
|
|
38
|
+
assert result["risk"] == "medium"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
async def test_search_tools_returns_list_of_dicts():
|
|
43
|
+
with patch("opentrust.mcp.opentrust.search", new_callable=AsyncMock) as m:
|
|
44
|
+
m.return_value = [
|
|
45
|
+
{"slug": "t", "name": "T", "trust_status": "community_reviewed", "description": "x"}
|
|
46
|
+
]
|
|
47
|
+
result = await search_tools("test")
|
|
48
|
+
assert isinstance(result, list)
|
|
49
|
+
assert result[0]["slug"] == "t"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.asyncio
|
|
53
|
+
async def test_search_tools_converts_empty_trust_status_to_none():
|
|
54
|
+
with patch("opentrust.mcp.opentrust.search", new_callable=AsyncMock) as m:
|
|
55
|
+
m.return_value = []
|
|
56
|
+
await search_tools("test", trust_status="")
|
|
57
|
+
m.assert_called_once_with("test", trust_status=None)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_search_tools_passes_trust_status_when_given():
|
|
62
|
+
with patch("opentrust.mcp.opentrust.search", new_callable=AsyncMock) as m:
|
|
63
|
+
m.return_value = []
|
|
64
|
+
await search_tools("test", trust_status="security_checked")
|
|
65
|
+
m.assert_called_once_with("test", trust_status="security_checked")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.asyncio
|
|
69
|
+
async def test_list_tools_returns_dict_with_items_and_total():
|
|
70
|
+
with patch("opentrust.mcp.opentrust.list", new_callable=AsyncMock) as m:
|
|
71
|
+
m.return_value = ToolsPage(
|
|
72
|
+
items=[{"slug": "a", "name": "A"}], total=1, page=1, limit=20
|
|
73
|
+
)
|
|
74
|
+
result = await list_tools(page=1, limit=20)
|
|
75
|
+
assert result["total"] == 1
|
|
76
|
+
assert isinstance(result["items"], list)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_list_tools_converts_empty_trust_status_to_none():
|
|
81
|
+
with patch("opentrust.mcp.opentrust.list", new_callable=AsyncMock) as m:
|
|
82
|
+
m.return_value = ToolsPage(items=[], total=0, page=1, limit=20)
|
|
83
|
+
await list_tools(trust_status="")
|
|
84
|
+
_, kwargs = m.call_args
|
|
85
|
+
assert kwargs["trust_status"] is None
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from opentrust._recommend import recommend, risk_level, TRUST_LEVELS
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_trust_levels_maps_all_statuses():
|
|
6
|
+
assert TRUST_LEVELS["auto_generated_draft"] == 1
|
|
7
|
+
assert TRUST_LEVELS["creator_claimed"] == 2
|
|
8
|
+
assert TRUST_LEVELS["seller_confirmed"] == 3
|
|
9
|
+
assert TRUST_LEVELS["community_reviewed"] == 4
|
|
10
|
+
assert TRUST_LEVELS["reviewer_signed"] == 5
|
|
11
|
+
assert TRUST_LEVELS["security_checked"] == 6
|
|
12
|
+
assert TRUST_LEVELS["continuously_monitored"] == 7
|
|
13
|
+
assert TRUST_LEVELS["disputed"] == 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_recommend_draft_says_do_not_use():
|
|
17
|
+
r = recommend("auto_generated_draft", {})
|
|
18
|
+
assert "Do not use" in r
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_recommend_disputed_mentions_dispute():
|
|
22
|
+
r = recommend("disputed", {})
|
|
23
|
+
assert "dispute" in r.lower()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_recommend_wallet_true_appends_warning():
|
|
27
|
+
r = recommend("security_checked", {"wallet": True})
|
|
28
|
+
assert "Wallet access active" in r
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_recommend_terminal_true_appends_warning():
|
|
32
|
+
r = recommend("continuously_monitored", {"terminal": True})
|
|
33
|
+
assert "Terminal access active" in r
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_recommend_no_warning_when_perms_false():
|
|
37
|
+
r = recommend("security_checked", {"wallet": False, "terminal": False})
|
|
38
|
+
assert "⚠" not in r
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_recommend_granular_wallet_object_appends_warning():
|
|
42
|
+
r = recommend("security_checked", {"wallet": {"send": True}})
|
|
43
|
+
assert "Wallet access active" in r
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_recommend_granular_empty_list_no_warning():
|
|
47
|
+
r = recommend("security_checked", {"wallet": {"read": []}})
|
|
48
|
+
assert "⚠" not in r
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_risk_disputed_is_high():
|
|
52
|
+
assert risk_level("disputed", {}) == "high"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_risk_draft_is_high():
|
|
56
|
+
assert risk_level("auto_generated_draft", {}) == "high"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_risk_creator_claimed_is_high():
|
|
60
|
+
assert risk_level("creator_claimed", {}) == "high"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_risk_monitored_no_perms_is_low():
|
|
64
|
+
assert risk_level("continuously_monitored", {}) == "low"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_risk_security_checked_with_wallet_is_medium():
|
|
68
|
+
assert risk_level("security_checked", {"wallet": True}) == "medium"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_risk_two_dangerous_perms_is_high():
|
|
72
|
+
assert risk_level("security_checked", {"wallet": True, "terminal": True}) == "high"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_risk_community_reviewed_no_perms_is_medium():
|
|
76
|
+
assert risk_level("community_reviewed", {}) == "medium"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import AsyncMock, patch
|
|
3
|
+
from opentrust import verify, get, search, list as list_tools, verify_sync, VerifyResult, ToolsPage
|
|
4
|
+
|
|
5
|
+
FAKE_PASSPORT = {
|
|
6
|
+
"id": "abc123",
|
|
7
|
+
"slug": "github-file-search",
|
|
8
|
+
"name": "GitHub File Search",
|
|
9
|
+
"description": "Search repos",
|
|
10
|
+
"trust_status": "community_reviewed",
|
|
11
|
+
"tool_identity": {"slug": "github-file-search", "name": "GitHub File Search"},
|
|
12
|
+
"version_hash": {"version": "1.0.0"},
|
|
13
|
+
"capabilities": ["search"],
|
|
14
|
+
"permission_manifest": {"network": True, "file": False, "terminal": False, "wallet": False},
|
|
15
|
+
"commercial_status": {"status": "free"},
|
|
16
|
+
"agent_access": {"allowed": True},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
FAKE_PAGE = {"items": [FAKE_PASSPORT], "total": 1, "page": 1, "limit": 20}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_verify_returns_verify_result():
|
|
24
|
+
with patch("opentrust._client._Client.get", new_callable=AsyncMock) as m:
|
|
25
|
+
m.return_value = FAKE_PASSPORT
|
|
26
|
+
result = await verify("github-file-search")
|
|
27
|
+
assert isinstance(result, VerifyResult)
|
|
28
|
+
assert result.slug == "github-file-search"
|
|
29
|
+
assert result.trust_status == "community_reviewed"
|
|
30
|
+
assert result.trust_level == 4
|
|
31
|
+
assert result.is_disputed is False
|
|
32
|
+
assert "Community reviewed" in result.recommendation
|
|
33
|
+
assert result.risk == "medium"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.asyncio
|
|
37
|
+
async def test_verify_disputed_sets_is_disputed_and_level_zero():
|
|
38
|
+
disputed = {**FAKE_PASSPORT, "trust_status": "disputed"}
|
|
39
|
+
with patch("opentrust._client._Client.get", new_callable=AsyncMock) as m:
|
|
40
|
+
m.return_value = disputed
|
|
41
|
+
result = await verify("github-file-search")
|
|
42
|
+
assert result.is_disputed is True
|
|
43
|
+
assert result.trust_level == 0
|
|
44
|
+
assert result.risk == "high"
|
|
45
|
+
assert "dispute" in result.recommendation.lower()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.asyncio
|
|
49
|
+
async def test_get_returns_raw_passport_dict():
|
|
50
|
+
with patch("opentrust._client._Client.get", new_callable=AsyncMock) as m:
|
|
51
|
+
m.return_value = FAKE_PASSPORT
|
|
52
|
+
result = await get("github-file-search")
|
|
53
|
+
assert result["slug"] == "github-file-search"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
async def test_search_returns_list_of_dicts():
|
|
58
|
+
with patch("opentrust._client._Client.get", new_callable=AsyncMock) as m:
|
|
59
|
+
m.return_value = FAKE_PAGE
|
|
60
|
+
result = await search("github")
|
|
61
|
+
assert isinstance(result, list)
|
|
62
|
+
assert result[0]["slug"] == "github-file-search"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.asyncio
|
|
66
|
+
async def test_list_returns_tools_page():
|
|
67
|
+
with patch("opentrust._client._Client.get", new_callable=AsyncMock) as m:
|
|
68
|
+
m.return_value = FAKE_PAGE
|
|
69
|
+
result = await list_tools(trust_status="community_reviewed")
|
|
70
|
+
assert isinstance(result, ToolsPage)
|
|
71
|
+
assert result.total == 1
|
|
72
|
+
assert result.page == 1
|
|
73
|
+
assert result.limit == 20
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.asyncio
|
|
77
|
+
async def test_list_passes_none_trust_status_when_not_given():
|
|
78
|
+
with patch("opentrust._client._Client.get", new_callable=AsyncMock) as m:
|
|
79
|
+
m.return_value = FAKE_PAGE
|
|
80
|
+
await list_tools()
|
|
81
|
+
_, kwargs = m.call_args
|
|
82
|
+
assert kwargs.get("trust_status") is None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_verify_sync_returns_verify_result():
|
|
86
|
+
with patch("opentrust._client._Client.get", new_callable=AsyncMock) as m:
|
|
87
|
+
m.return_value = FAKE_PASSPORT
|
|
88
|
+
result = verify_sync("github-file-search")
|
|
89
|
+
assert isinstance(result, VerifyResult)
|
|
90
|
+
assert result.trust_level == 4
|