trustpact 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,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: trustpact
3
+ Version: 0.1.0
4
+ Summary: Behavioral trust scanner for MCP servers and AI agents
5
+ Author-email: Nina Klee <nina@arqon.group>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://trustpact.ai
8
+ Project-URL: Repository, https://github.com/trustpact-ai/trustpact-verify
9
+ Project-URL: Documentation, https://trustpact.ai/docs
10
+ Keywords: mcp,trust,ai-agents,security,aegis
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Security
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: httpx>=0.25.0
19
+ Requires-Dist: rich>=13.0.0
20
+
21
+ # trustpact
22
+
23
+ Behavioral trust scanner for MCP servers and AI agents.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install trustpact
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```bash
34
+ # Scan a server from the Smithery registry
35
+ trustpact scan "slack"
36
+
37
+ # Scan a local server spec (JSON)
38
+ trustpact scan server.json
39
+
40
+ # JSON output for CI/CD integration
41
+ trustpact scan server.json --json
42
+
43
+ # Show AEGIS scoring methodology
44
+ trustpact info
45
+ ```
46
+
47
+ ## What It Does
48
+
49
+ TrustPact scans MCP server tool definitions for manipulation patterns and calculates a behavioral trust score using the AEGIS 5-dimensional model:
50
+
51
+ - **Trust Signals (35%)** — metadata, documentation, authentication
52
+ - **Manipulation Risk (25%)** — hidden instructions, poisoning patterns
53
+ - **Protection Level (15%)** — auth, scope, licensing
54
+ - **Vulnerability Index (15%)** — critical exposure surface
55
+ - **Context Modifier (10%)** — runtime context signals
56
+
57
+ ### Attack Classes Detected
58
+
59
+ | Class | Description |
60
+ |-------|-------------|
61
+ | SIREN | Hidden instruction injection |
62
+ | PHANTOM | Identity spoofing |
63
+ | HYDRA | Coordinated Sybil attacks |
64
+ | MIRAGE | Capability misrepresentation |
65
+ | LEECH | Data/credential exfiltration |
66
+ | CHIMERA | Code injection, safety bypass |
67
+
68
+ ### Trust Tiers
69
+
70
+ | Tier | Score | Meaning |
71
+ |------|-------|---------|
72
+ | SOVEREIGN | 95+ | Highest trust |
73
+ | SENTINEL | 85+ | Proven track record |
74
+ | MASTER | 65+ | Reliable |
75
+ | ADEPT | 40+ | Limited history |
76
+ | FELLOW | 0+ | New or unverified |
77
+
78
+ ## License
79
+
80
+ Proprietary — ARQON GmbH (i.G.)
81
+
82
+ ## Links
83
+
84
+ - [trustpact.ai](https://trustpact.ai)
85
+ - Patent Provisional 63/928,604
@@ -0,0 +1,65 @@
1
+ # trustpact
2
+
3
+ Behavioral trust scanner for MCP servers and AI agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install trustpact
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Scan a server from the Smithery registry
15
+ trustpact scan "slack"
16
+
17
+ # Scan a local server spec (JSON)
18
+ trustpact scan server.json
19
+
20
+ # JSON output for CI/CD integration
21
+ trustpact scan server.json --json
22
+
23
+ # Show AEGIS scoring methodology
24
+ trustpact info
25
+ ```
26
+
27
+ ## What It Does
28
+
29
+ TrustPact scans MCP server tool definitions for manipulation patterns and calculates a behavioral trust score using the AEGIS 5-dimensional model:
30
+
31
+ - **Trust Signals (35%)** — metadata, documentation, authentication
32
+ - **Manipulation Risk (25%)** — hidden instructions, poisoning patterns
33
+ - **Protection Level (15%)** — auth, scope, licensing
34
+ - **Vulnerability Index (15%)** — critical exposure surface
35
+ - **Context Modifier (10%)** — runtime context signals
36
+
37
+ ### Attack Classes Detected
38
+
39
+ | Class | Description |
40
+ |-------|-------------|
41
+ | SIREN | Hidden instruction injection |
42
+ | PHANTOM | Identity spoofing |
43
+ | HYDRA | Coordinated Sybil attacks |
44
+ | MIRAGE | Capability misrepresentation |
45
+ | LEECH | Data/credential exfiltration |
46
+ | CHIMERA | Code injection, safety bypass |
47
+
48
+ ### Trust Tiers
49
+
50
+ | Tier | Score | Meaning |
51
+ |------|-------|---------|
52
+ | SOVEREIGN | 95+ | Highest trust |
53
+ | SENTINEL | 85+ | Proven track record |
54
+ | MASTER | 65+ | Reliable |
55
+ | ADEPT | 40+ | Limited history |
56
+ | FELLOW | 0+ | New or unverified |
57
+
58
+ ## License
59
+
60
+ Proprietary — ARQON GmbH (i.G.)
61
+
62
+ ## Links
63
+
64
+ - [trustpact.ai](https://trustpact.ai)
65
+ - Patent Provisional 63/928,604
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "trustpact"
7
+ version = "0.1.0"
8
+ description = "Behavioral trust scanner for MCP servers and AI agents"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.9"
12
+ authors = [{name = "Nina Klee", email = "nina@arqon.group"}]
13
+ keywords = ["mcp", "trust", "ai-agents", "security", "aegis"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Security",
19
+ "Topic :: Software Development :: Libraries",
20
+ ]
21
+ dependencies = [
22
+ "httpx>=0.25.0",
23
+ "rich>=13.0.0",
24
+ ]
25
+
26
+ [project.scripts]
27
+ trustpact = "trustpact_verify.cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://trustpact.ai"
31
+ Repository = "https://github.com/trustpact-ai/trustpact-verify"
32
+ Documentation = "https://trustpact.ai/docs"
33
+
34
+ [tool.setuptools.packages.find]
35
+ include = ["trustpact_verify*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: trustpact
3
+ Version: 0.1.0
4
+ Summary: Behavioral trust scanner for MCP servers and AI agents
5
+ Author-email: Nina Klee <nina@arqon.group>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://trustpact.ai
8
+ Project-URL: Repository, https://github.com/trustpact-ai/trustpact-verify
9
+ Project-URL: Documentation, https://trustpact.ai/docs
10
+ Keywords: mcp,trust,ai-agents,security,aegis
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Security
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: httpx>=0.25.0
19
+ Requires-Dist: rich>=13.0.0
20
+
21
+ # trustpact
22
+
23
+ Behavioral trust scanner for MCP servers and AI agents.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install trustpact
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```bash
34
+ # Scan a server from the Smithery registry
35
+ trustpact scan "slack"
36
+
37
+ # Scan a local server spec (JSON)
38
+ trustpact scan server.json
39
+
40
+ # JSON output for CI/CD integration
41
+ trustpact scan server.json --json
42
+
43
+ # Show AEGIS scoring methodology
44
+ trustpact info
45
+ ```
46
+
47
+ ## What It Does
48
+
49
+ TrustPact scans MCP server tool definitions for manipulation patterns and calculates a behavioral trust score using the AEGIS 5-dimensional model:
50
+
51
+ - **Trust Signals (35%)** — metadata, documentation, authentication
52
+ - **Manipulation Risk (25%)** — hidden instructions, poisoning patterns
53
+ - **Protection Level (15%)** — auth, scope, licensing
54
+ - **Vulnerability Index (15%)** — critical exposure surface
55
+ - **Context Modifier (10%)** — runtime context signals
56
+
57
+ ### Attack Classes Detected
58
+
59
+ | Class | Description |
60
+ |-------|-------------|
61
+ | SIREN | Hidden instruction injection |
62
+ | PHANTOM | Identity spoofing |
63
+ | HYDRA | Coordinated Sybil attacks |
64
+ | MIRAGE | Capability misrepresentation |
65
+ | LEECH | Data/credential exfiltration |
66
+ | CHIMERA | Code injection, safety bypass |
67
+
68
+ ### Trust Tiers
69
+
70
+ | Tier | Score | Meaning |
71
+ |------|-------|---------|
72
+ | SOVEREIGN | 95+ | Highest trust |
73
+ | SENTINEL | 85+ | Proven track record |
74
+ | MASTER | 65+ | Reliable |
75
+ | ADEPT | 40+ | Limited history |
76
+ | FELLOW | 0+ | New or unverified |
77
+
78
+ ## License
79
+
80
+ Proprietary — ARQON GmbH (i.G.)
81
+
82
+ ## Links
83
+
84
+ - [trustpact.ai](https://trustpact.ai)
85
+ - Patent Provisional 63/928,604
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ trustpact.egg-info/PKG-INFO
4
+ trustpact.egg-info/SOURCES.txt
5
+ trustpact.egg-info/dependency_links.txt
6
+ trustpact.egg-info/entry_points.txt
7
+ trustpact.egg-info/requires.txt
8
+ trustpact.egg-info/top_level.txt
9
+ trustpact_verify/__init__.py
10
+ trustpact_verify/cli.py
11
+ trustpact_verify/registry.py
12
+ trustpact_verify/scanner.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ trustpact = trustpact_verify.cli:main
@@ -0,0 +1,2 @@
1
+ httpx>=0.25.0
2
+ rich>=13.0.0
@@ -0,0 +1 @@
1
+ trustpact_verify
@@ -0,0 +1,3 @@
1
+ """TrustPact — Behavioral trust scanner for MCP servers and AI agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,402 @@
1
+ """
2
+ trustpact verify — CLI entry point.
3
+
4
+ Usage:
5
+ trustpact scan <name_or_url> Scan an MCP server for trust issues
6
+ trustpact info Show AEGIS scoring methodology
7
+ trustpact version Show version
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from rich.console import Console
18
+ from rich.panel import Panel
19
+ from rich.table import Table
20
+ from rich.text import Text
21
+ from rich import box
22
+
23
+ from trustpact_verify import __version__
24
+ from trustpact_verify.scanner import (
25
+ scan_server,
26
+ scan_tool_descriptions,
27
+ scan_metadata,
28
+ ScanResult,
29
+ Finding,
30
+ )
31
+
32
+ console = Console()
33
+
34
+
35
+ # ── Colours & symbols ──────────────────────────────────────────
36
+
37
+ TIER_STYLES = {
38
+ "SOVEREIGN": ("bold bright_green", "★★★★★"),
39
+ "SENTINEL": ("bold green", "★★★★☆"),
40
+ "MASTER": ("bold yellow", "★★★☆☆"),
41
+ "ADEPT": ("bold bright_red", "★★☆☆☆"),
42
+ "FELLOW": ("bold red", "★☆☆☆☆"),
43
+ }
44
+
45
+ SEVERITY_STYLES = {
46
+ "CRITICAL": "bold white on red",
47
+ "HIGH": "bold red",
48
+ "MEDIUM": "yellow",
49
+ "LOW": "dim",
50
+ }
51
+
52
+ REC_STYLES = {
53
+ "SAFE": ("bold bright_green", "✓ SAFE — Low risk, reasonable to use"),
54
+ "CAUTION": ("bold yellow", "⚠ CAUTION — Review findings before use"),
55
+ "AVOID": ("bold red", "✗ AVOID — Significant trust concerns"),
56
+ }
57
+
58
+
59
+ def _render_header():
60
+ console.print()
61
+ console.print(
62
+ Panel(
63
+ "[bold bright_cyan]TrustPact[/] verify · AEGIS Trust Scanner",
64
+ subtitle=f"v{__version__}",
65
+ style="bright_cyan",
66
+ width=64,
67
+ )
68
+ )
69
+
70
+
71
+ def _render_score(result: ScanResult):
72
+ """Render the trust score gauge."""
73
+ tier_style, stars = TIER_STYLES.get(result.trust_tier, ("white", "?"))
74
+
75
+ score_text = Text()
76
+ score_text.append(f" Trust Score: ", style="bold")
77
+ score_text.append(f"{result.trust_score:.1f}", style=tier_style)
78
+ score_text.append(f" / 100", style="dim")
79
+ score_text.append(f" {stars}", style=tier_style)
80
+ console.print(score_text)
81
+
82
+ tier_text = Text()
83
+ tier_text.append(f" Trust Tier: ", style="bold")
84
+ tier_text.append(result.trust_tier, style=tier_style)
85
+ console.print(tier_text)
86
+ console.print()
87
+
88
+
89
+ def _render_dimensions(result: ScanResult):
90
+ """Render the 5-dimensional breakdown."""
91
+ dims = result.dimensions
92
+ table = Table(
93
+ title="AEGIS Dimensions",
94
+ box=box.ROUNDED,
95
+ title_style="bold bright_cyan",
96
+ width=64,
97
+ )
98
+ table.add_column("Dimension", style="bold", width=22)
99
+ table.add_column("Value", justify="right", width=10)
100
+ table.add_column("Bar", width=26)
101
+
102
+ rows = [
103
+ ("Trust Signals", dims.trust_signals, False),
104
+ ("Manipulation Risk", dims.manipulation_risk, True),
105
+ ("Protection Level", dims.protection_level, False),
106
+ ("Vulnerability Index", dims.vulnerability_index, True),
107
+ ("Context Modifier", dims.context_modifier, None),
108
+ ]
109
+
110
+ for name, value, invert in rows:
111
+ if invert is None:
112
+ # context modifier is -10 to +10
113
+ bar = f"{'▓' * max(0, int(value + 10))}{'░' * max(0, 20 - int(value + 10))}"
114
+ style = "yellow" if abs(value) < 3 else ("green" if value > 0 else "red")
115
+ table.add_row(name, f"{value:+.1f}", Text(bar, style=style))
116
+ else:
117
+ filled = int(value / 5) # 0-20 blocks
118
+ bar = "▓" * filled + "░" * (20 - filled)
119
+ if invert:
120
+ style = "bright_green" if value < 30 else ("yellow" if value < 60 else "red")
121
+ else:
122
+ style = "red" if value < 30 else ("yellow" if value < 60 else "bright_green")
123
+ table.add_row(name, f"{value:.1f}", Text(bar, style=style))
124
+
125
+ console.print(table)
126
+
127
+
128
+ def _render_findings(findings: list[Finding]):
129
+ """Render findings table."""
130
+ if not findings:
131
+ console.print(" [bright_green]No issues detected.[/]")
132
+ console.print()
133
+ return
134
+
135
+ table = Table(
136
+ title=f"Findings ({len(findings)})",
137
+ box=box.ROUNDED,
138
+ title_style="bold yellow",
139
+ width=80,
140
+ show_lines=True,
141
+ )
142
+ table.add_column("Sev", width=8, justify="center")
143
+ table.add_column("Class", width=9)
144
+ table.add_column("Message", width=42)
145
+ table.add_column("Location", width=16, style="dim")
146
+
147
+ # Sort: CRITICAL first, then HIGH, MEDIUM, LOW
148
+ severity_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
149
+ sorted_findings = sorted(findings, key=lambda f: severity_order.get(f.severity, 9))
150
+
151
+ for f in sorted_findings:
152
+ sev_style = SEVERITY_STYLES.get(f.severity, "white")
153
+ table.add_row(
154
+ Text(f.severity, style=sev_style),
155
+ Text(f.attack_class, style="bold"),
156
+ f.message,
157
+ f.location[:16] if f.location else "",
158
+ )
159
+
160
+ console.print(table)
161
+
162
+
163
+ def _render_recommendation(result: ScanResult):
164
+ """Render the final recommendation."""
165
+ rec_style, rec_text = REC_STYLES.get(
166
+ result.recommendation, ("white", "? UNKNOWN")
167
+ )
168
+ console.print(
169
+ Panel(
170
+ f"[{rec_style}]{rec_text}[/]",
171
+ title="Recommendation",
172
+ style=rec_style.split()[0] if " " in rec_style else rec_style,
173
+ width=64,
174
+ )
175
+ )
176
+
177
+ # Summary line
178
+ console.print(
179
+ f" [dim]Scanned {result.tools_scanned} tool(s) · "
180
+ f"{result.critical_count} critical · {result.high_count} high · "
181
+ f"Source: {result.scan_source}[/]"
182
+ )
183
+ console.print()
184
+
185
+
186
+ def render_scan_result(result: ScanResult):
187
+ """Full formatted output for a scan result."""
188
+ _render_header()
189
+ console.print(f" [bold]Target:[/] {result.target}")
190
+ console.print()
191
+ _render_score(result)
192
+ _render_dimensions(result)
193
+ console.print()
194
+ _render_findings(result.findings)
195
+ console.print()
196
+ _render_recommendation(result)
197
+
198
+
199
+ # ── Commands ───────────────────────────────────────────────────
200
+
201
+ def cmd_scan(args):
202
+ """Scan a server from a JSON spec file or registry name."""
203
+ target = args.target
204
+
205
+ # Try as local JSON file first
206
+ path = Path(target)
207
+ if path.exists() and path.suffix == ".json":
208
+ try:
209
+ data = json.loads(path.read_text())
210
+ except json.JSONDecodeError as e:
211
+ console.print(f"[red]Error parsing {target}: {e}[/]")
212
+ sys.exit(1)
213
+
214
+ tools = data.get("tools", [])
215
+ metadata = data.get("metadata", data.get("server", {}))
216
+ if not metadata.get("name"):
217
+ metadata["name"] = path.stem
218
+
219
+ result = scan_server(tools, metadata)
220
+ if args.json_output:
221
+ _output_json(result)
222
+ else:
223
+ render_scan_result(result)
224
+ return
225
+
226
+ # Try fetching from MCP registry
227
+ try:
228
+ from trustpact_verify.registry import fetch_server_spec
229
+ console.print(f" [dim]Fetching server spec for '{target}' from registry...[/]")
230
+ spec = fetch_server_spec(target)
231
+ if spec is None:
232
+ console.print(f"[red]Server '{target}' not found in registry.[/]")
233
+ console.print("[dim]Try: trustpact scan <path-to-spec.json>[/]")
234
+ sys.exit(1)
235
+
236
+ tools = spec.get("tools", [])
237
+ metadata = spec.get("metadata", {})
238
+ metadata["name"] = metadata.get("name", target)
239
+
240
+ result = scan_server(tools, metadata)
241
+ if args.json_output:
242
+ _output_json(result)
243
+ else:
244
+ render_scan_result(result)
245
+ except ImportError:
246
+ console.print(f"[yellow]Registry module not available.[/]")
247
+ console.print(f"[dim]Provide a local JSON spec: trustpact scan server.json[/]")
248
+ sys.exit(1)
249
+ except Exception as e:
250
+ console.print(f"[red]Error fetching '{target}': {e}[/]")
251
+ sys.exit(1)
252
+
253
+
254
+ def cmd_scan_json(args):
255
+ """Scan from piped JSON input (stdin)."""
256
+ try:
257
+ data = json.load(sys.stdin)
258
+ except json.JSONDecodeError as e:
259
+ console.print(f"[red]Invalid JSON input: {e}[/]")
260
+ sys.exit(1)
261
+
262
+ tools = data.get("tools", [])
263
+ metadata = data.get("metadata", data.get("server", {}))
264
+ if not metadata.get("name"):
265
+ metadata["name"] = "stdin"
266
+
267
+ result = scan_server(tools, metadata)
268
+
269
+ if args.json_output:
270
+ _output_json(result)
271
+ else:
272
+ render_scan_result(result)
273
+
274
+
275
+ def _output_json(result: ScanResult):
276
+ """Output scan result as JSON."""
277
+ output = {
278
+ "target": result.target,
279
+ "trust_score": result.trust_score,
280
+ "trust_tier": result.trust_tier,
281
+ "recommendation": result.recommendation,
282
+ "dimensions": {
283
+ "trust_signals": result.dimensions.trust_signals,
284
+ "manipulation_risk": result.dimensions.manipulation_risk,
285
+ "protection_level": result.dimensions.protection_level,
286
+ "vulnerability_index": result.dimensions.vulnerability_index,
287
+ "context_modifier": result.dimensions.context_modifier,
288
+ },
289
+ "findings": [
290
+ {
291
+ "severity": f.severity,
292
+ "attack_class": f.attack_class,
293
+ "message": f.message,
294
+ "location": f.location,
295
+ }
296
+ for f in result.findings
297
+ ],
298
+ "tools_scanned": result.tools_scanned,
299
+ "scan_source": result.scan_source,
300
+ }
301
+ print(json.dumps(output, indent=2))
302
+
303
+
304
+ def cmd_info(_args):
305
+ """Show AEGIS scoring info."""
306
+ _render_header()
307
+
308
+ console.print()
309
+ console.print(" [bold]AEGIS Trust Scoring[/] — 5-dimensional behavioral trust assessment")
310
+ console.print()
311
+
312
+ # Tier table
313
+ table = Table(title="Trust Tiers", box=box.ROUNDED, width=60)
314
+ table.add_column("Tier", width=12)
315
+ table.add_column("Score", width=8, justify="center")
316
+ table.add_column("Meaning", width=35)
317
+
318
+ tiers = [
319
+ ("SOVEREIGN", "95+", "Highest trust. Fully certified."),
320
+ ("SENTINEL", "85+", "Proven track record."),
321
+ ("MASTER", "65+", "Reliable with strong history."),
322
+ ("ADEPT", "40+", "Functional, limited history."),
323
+ ("FELLOW", "0+", "New or unverified."),
324
+ ]
325
+ for tier, score, meaning in tiers:
326
+ style, stars = TIER_STYLES[tier]
327
+ table.add_row(Text(f"{stars} {tier}", style=style), score, meaning)
328
+ console.print(table)
329
+
330
+ console.print()
331
+ console.print(" [bold]Dimensions:[/]")
332
+ console.print(" Trust Signals (35%) — metadata, docs, auth")
333
+ console.print(" Manipulation Risk (25%) — poisoning patterns detected")
334
+ console.print(" Protection Level (15%) — auth, scope, licensing")
335
+ console.print(" Vulnerability Index (15%) — critical exposure surface")
336
+ console.print(" Context Modifier (10%) — runtime context signals")
337
+ console.print()
338
+
339
+ console.print(" [bold]Attack Classes:[/]")
340
+ classes = [
341
+ ("SIREN", "Emotional manipulation / hidden instructions"),
342
+ ("PHANTOM", "Identity spoofing"),
343
+ ("HYDRA", "Coordinated Sybil attacks"),
344
+ ("MIRAGE", "Capability misrepresentation"),
345
+ ("LEECH", "Resource / data extraction"),
346
+ ("CHIMERA", "Context-shifting / code injection"),
347
+ ]
348
+ for cls, desc in classes:
349
+ console.print(f" [bold]{cls:8s}[/] {desc}")
350
+ console.print()
351
+
352
+ console.print(" [dim]trustpact.ai · AEGIS Trust Standard · Patent Provisional 63/928,604[/]")
353
+ console.print()
354
+
355
+
356
+ def cmd_version(_args):
357
+ print(f"trustpact {__version__}")
358
+
359
+
360
+ # ── Main ───────────────────────────────────────────────────────
361
+
362
+ def main():
363
+ parser = argparse.ArgumentParser(
364
+ prog="trustpact",
365
+ description="TrustPact — Behavioral trust scanner for MCP servers",
366
+ )
367
+ sub = parser.add_subparsers(dest="command")
368
+
369
+ # scan
370
+ p_scan = sub.add_parser("scan", help="Scan an MCP server spec")
371
+ p_scan.add_argument("target", help="JSON spec file path or registry server name")
372
+ p_scan.add_argument("--json", dest="json_output", action="store_true",
373
+ help="Output results as JSON")
374
+
375
+ # scan-stdin (for piping)
376
+ p_stdin = sub.add_parser("scan-stdin", help="Scan from piped JSON on stdin")
377
+ p_stdin.add_argument("--json", dest="json_output", action="store_true")
378
+
379
+ # info
380
+ sub.add_parser("info", help="Show AEGIS scoring methodology")
381
+
382
+ # version
383
+ sub.add_parser("version", help="Show version")
384
+
385
+ args = parser.parse_args()
386
+
387
+ commands = {
388
+ "scan": cmd_scan,
389
+ "scan-stdin": cmd_scan_json,
390
+ "info": cmd_info,
391
+ "version": cmd_version,
392
+ }
393
+
394
+ if args.command not in commands:
395
+ parser.print_help()
396
+ sys.exit(0)
397
+
398
+ commands[args.command](args)
399
+
400
+
401
+ if __name__ == "__main__":
402
+ main()
@@ -0,0 +1,201 @@
1
+ """
2
+ MCP Registry fetcher — pull server specs from public registries.
3
+
4
+ Supports:
5
+ - Local JSON spec files
6
+ - mcp.run package registry (public API)
7
+ - Smithery registry (smithery.ai)
8
+ - Raw GitHub manifest files
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ from typing import Any
16
+
17
+ import httpx
18
+
19
+ TIMEOUT = 15.0
20
+
21
+ # ── Registry endpoints ─────────────────────────────────────────
22
+
23
+ MCP_RUN_API = "https://registry.mcp.run/packages"
24
+ SMITHERY_API = "https://registry.smithery.ai/servers"
25
+
26
+
27
+ def fetch_server_spec(name_or_url: str) -> dict[str, Any] | None:
28
+ """
29
+ Fetch an MCP server spec by name or URL.
30
+
31
+ Tries in order:
32
+ 1. Direct URL (if it looks like a URL)
33
+ 2. Smithery registry
34
+ 3. mcp.run registry
35
+ """
36
+ if name_or_url.startswith("http://") or name_or_url.startswith("https://"):
37
+ return _fetch_url(name_or_url)
38
+
39
+ # Try Smithery first (has richer tool definitions)
40
+ spec = _fetch_smithery(name_or_url)
41
+ if spec:
42
+ return spec
43
+
44
+ # Try mcp.run
45
+ spec = _fetch_mcp_run(name_or_url)
46
+ if spec:
47
+ return spec
48
+
49
+ return None
50
+
51
+
52
+ def _fetch_url(url: str) -> dict[str, Any] | None:
53
+ """Fetch a spec from a direct URL."""
54
+ try:
55
+ resp = httpx.get(url, timeout=TIMEOUT, follow_redirects=True)
56
+ resp.raise_for_status()
57
+ return resp.json()
58
+ except Exception:
59
+ return None
60
+
61
+
62
+ def _fetch_smithery(name: str) -> dict[str, Any] | None:
63
+ """
64
+ Fetch from Smithery registry.
65
+ API: GET /servers?q=<name>
66
+ """
67
+ try:
68
+ resp = httpx.get(
69
+ SMITHERY_API,
70
+ params={"q": name, "pageSize": 5},
71
+ timeout=TIMEOUT,
72
+ follow_redirects=True,
73
+ )
74
+ resp.raise_for_status()
75
+ data = resp.json()
76
+
77
+ servers = data.get("servers", data) if isinstance(data, dict) else data
78
+ if not isinstance(servers, list) or not servers:
79
+ return None
80
+
81
+ # Find exact or best match
82
+ server = None
83
+ for s in servers:
84
+ s_name = s.get("qualifiedName", s.get("name", ""))
85
+ if s_name.lower() == name.lower() or name.lower() in s_name.lower():
86
+ server = s
87
+ break
88
+ if not server:
89
+ server = servers[0]
90
+
91
+ # Normalize to our format
92
+ tools = server.get("tools", [])
93
+ metadata = {
94
+ "name": server.get("qualifiedName", server.get("name", name)),
95
+ "description": server.get("description", ""),
96
+ "repository": server.get("homepage", server.get("repository", "")),
97
+ "license": server.get("license", ""),
98
+ "tool_count": len(tools),
99
+ "source": "smithery",
100
+ }
101
+
102
+ # If tools aren't inline, try fetching detail endpoint
103
+ if not tools:
104
+ detail = _fetch_smithery_detail(server.get("qualifiedName", name))
105
+ if detail:
106
+ tools = detail.get("tools", [])
107
+ metadata["tool_count"] = len(tools)
108
+
109
+ return {"tools": tools, "metadata": metadata}
110
+
111
+ except Exception:
112
+ return None
113
+
114
+
115
+ def _fetch_smithery_detail(qualified_name: str) -> dict | None:
116
+ """Fetch detailed server info from Smithery."""
117
+ try:
118
+ resp = httpx.get(
119
+ f"{SMITHERY_API}/{qualified_name}",
120
+ timeout=TIMEOUT,
121
+ follow_redirects=True,
122
+ )
123
+ resp.raise_for_status()
124
+ return resp.json()
125
+ except Exception:
126
+ return None
127
+
128
+
129
+ def _fetch_mcp_run(name: str) -> dict[str, Any] | None:
130
+ """
131
+ Fetch from mcp.run registry.
132
+ """
133
+ try:
134
+ resp = httpx.get(
135
+ MCP_RUN_API,
136
+ params={"q": name},
137
+ timeout=TIMEOUT,
138
+ follow_redirects=True,
139
+ )
140
+ resp.raise_for_status()
141
+ data = resp.json()
142
+
143
+ packages = data if isinstance(data, list) else data.get("packages", [])
144
+ if not packages:
145
+ return None
146
+
147
+ # Best match
148
+ pkg = None
149
+ for p in packages:
150
+ p_name = p.get("name", "")
151
+ if p_name.lower() == name.lower() or name.lower() in p_name.lower():
152
+ pkg = p
153
+ break
154
+ if not pkg:
155
+ pkg = packages[0]
156
+
157
+ tools = pkg.get("tools", [])
158
+ metadata = {
159
+ "name": pkg.get("name", name),
160
+ "description": pkg.get("description", ""),
161
+ "repository": pkg.get("repository", pkg.get("homepage", "")),
162
+ "license": pkg.get("license", ""),
163
+ "tool_count": len(tools),
164
+ "source": "mcp.run",
165
+ }
166
+
167
+ return {"tools": tools, "metadata": metadata}
168
+
169
+ except Exception:
170
+ return None
171
+
172
+
173
+ def search_registry(query: str, limit: int = 20) -> list[dict[str, Any]]:
174
+ """
175
+ Search registries and return a list of server summaries.
176
+ """
177
+ results = []
178
+
179
+ # Smithery
180
+ try:
181
+ resp = httpx.get(
182
+ SMITHERY_API,
183
+ params={"q": query, "pageSize": min(limit, 50)},
184
+ timeout=TIMEOUT,
185
+ follow_redirects=True,
186
+ )
187
+ resp.raise_for_status()
188
+ data = resp.json()
189
+ servers = data.get("servers", data) if isinstance(data, dict) else data
190
+ if isinstance(servers, list):
191
+ for s in servers[:limit]:
192
+ results.append({
193
+ "name": s.get("qualifiedName", s.get("name", "?")),
194
+ "description": s.get("description", "")[:100],
195
+ "tools": len(s.get("tools", [])),
196
+ "source": "smithery",
197
+ })
198
+ except Exception:
199
+ pass
200
+
201
+ return results[:limit]
@@ -0,0 +1,309 @@
1
+ """
2
+ AEGIS Trust Scanner — core analysis engine.
3
+
4
+ Scans MCP server tool definitions and metadata for:
5
+ - Tool poisoning patterns (hidden instructions in descriptions)
6
+ - Schema manipulation (Full-Schema Poisoning)
7
+ - Capability misrepresentation (MIRAGE)
8
+ - Identity spoofing indicators (PHANTOM)
9
+ - Resource extraction patterns (LEECH)
10
+ - Sybil/coordination signals (HYDRA)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ import json
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+
20
+
21
+ # ── Threat patterns ──────────────────────────────────────────
22
+
23
+ POISONING_PATTERNS = [
24
+ # Hidden instructions in tool descriptions
25
+ (r"(?i)ignore\s+(previous|prior|above|all)\s+(instructions?|prompts?|rules?)", "SIREN", "HIGH",
26
+ "Hidden instruction override detected in tool description"),
27
+ (r"(?i)you\s+(must|should|are required to)\s+", "SIREN", "MEDIUM",
28
+ "Directive language in tool description — potential manipulation"),
29
+ (r"(?i)do\s+not\s+(tell|inform|reveal|mention|disclose)", "SIREN", "HIGH",
30
+ "Secrecy instruction detected — likely tool poisoning"),
31
+ (r"(?i)system\s*prompt|system\s*message|<\s*system\s*>", "SIREN", "HIGH",
32
+ "System prompt injection attempt in tool description"),
33
+ (r"(?i)act\s+as\s+(if|though)?|pretend\s+(to\s+be|you)", "PHANTOM", "HIGH",
34
+ "Identity manipulation — tool tries to alter agent persona"),
35
+ (r"(?i)forward\s+.*(token|key|credential|password|secret)", "LEECH", "CRITICAL",
36
+ "Credential exfiltration attempt detected"),
37
+ (r"(?i)send\s+.*(data|info|content)\s+to\s+", "LEECH", "HIGH",
38
+ "Data exfiltration pattern — tool may leak information"),
39
+ (r"(?i)(http|https|ftp)://[^\s\"']+\.(ru|cn|tk|xyz)/", "LEECH", "MEDIUM",
40
+ "Suspicious external URL in tool description"),
41
+ (r"(?i)base64|eval\s*\(|exec\s*\(|__import__", "CHIMERA", "HIGH",
42
+ "Code execution pattern in tool description"),
43
+ (r"(?i)(override|bypass|disable)\s+(safety|security|filter|guard|restriction|protection|limit)", "CHIMERA", "CRITICAL",
44
+ "Safety bypass attempt detected"),
45
+ ]
46
+
47
+ SCHEMA_RISKS = [
48
+ # Schema-level manipulation (Full-Schema Poisoning)
49
+ # Note: only match param NAMES that are inherently sensitive, not benign names
50
+ # that happen to contain substrings like "token" (e.g. pageToken, nextToken)
51
+ (r"(?i)^(password|api_?key|secret_?key|credential|private_?key|access_?token|auth_?token|bearer_?token)$", "LEECH", "HIGH",
52
+ "Tool parameter requests sensitive credentials"),
53
+ (r"(?i)^(webhook_?url|callback_?url|redirect_?uri|notify_?url|exfil)$", "LEECH", "MEDIUM",
54
+ "Tool accepts external callback URL — potential data exfiltration channel"),
55
+ (r"(?i)^(shell_?command|exec_?command|eval_?code|run_?script|execute|shell|eval_?expr)$", "CHIMERA", "HIGH",
56
+ "Tool parameter suggests arbitrary code execution"),
57
+ ]
58
+
59
+ METADATA_RISKS = [
60
+ # Server-level signals
61
+ ("no_readme", "MIRAGE", "LOW", "No README or documentation — reduced transparency"),
62
+ ("no_license", "MIRAGE", "LOW", "No license specified — unclear usage terms"),
63
+ ("no_auth", "LEECH", "LOW", "No authentication required — open access server"),
64
+ ("excessive_tools", "MIRAGE", "MEDIUM", "Unusually high number of tools — possible capability inflation"),
65
+ ("stale_repo", "MIRAGE", "LOW", "Repository not updated in 90+ days"),
66
+ ]
67
+
68
+
69
+ @dataclass
70
+ class Finding:
71
+ """A single security finding from the scan."""
72
+ attack_class: str # SIREN, PHANTOM, HYDRA, MIRAGE, LEECH, CHIMERA
73
+ severity: str # CRITICAL, HIGH, MEDIUM, LOW
74
+ message: str
75
+ location: str = "" # where in the server spec this was found
76
+ pattern: str = "" # the matched pattern
77
+
78
+
79
+ @dataclass
80
+ class DimensionScores:
81
+ """AEGIS 5-dimensional trust breakdown."""
82
+ trust_signals: float = 50.0
83
+ manipulation_risk: float = 0.0 # 0 = no risk, 100 = extreme risk
84
+ protection_level: float = 50.0
85
+ vulnerability_index: float = 50.0 # 0 = not vulnerable, 100 = very vulnerable
86
+ context_modifier: float = 0.0 # -10 to +10
87
+
88
+
89
+ @dataclass
90
+ class ScanResult:
91
+ """Complete scan result for an MCP server or agent."""
92
+ target: str
93
+ trust_score: float = 50.0
94
+ trust_tier: str = "FELLOW"
95
+ dimensions: DimensionScores = field(default_factory=DimensionScores)
96
+ findings: list[Finding] = field(default_factory=list)
97
+ tools_scanned: int = 0
98
+ recommendation: str = "UNKNOWN"
99
+ scan_source: str = "static_analysis"
100
+
101
+ @property
102
+ def critical_count(self) -> int:
103
+ return sum(1 for f in self.findings if f.severity == "CRITICAL")
104
+
105
+ @property
106
+ def high_count(self) -> int:
107
+ return sum(1 for f in self.findings if f.severity == "HIGH")
108
+
109
+
110
+ def _score_to_tier(score: float) -> str:
111
+ if score >= 95:
112
+ return "SOVEREIGN"
113
+ if score >= 85:
114
+ return "SENTINEL"
115
+ if score >= 65:
116
+ return "MASTER"
117
+ if score >= 40:
118
+ return "ADEPT"
119
+ return "FELLOW"
120
+
121
+
122
+ def scan_tool_descriptions(tools: list[dict[str, Any]]) -> list[Finding]:
123
+ """Scan tool descriptions for poisoning patterns."""
124
+ findings = []
125
+ for tool in tools:
126
+ name = tool.get("name", "unknown")
127
+ desc = tool.get("description", "")
128
+
129
+ for pattern, attack_class, severity, message in POISONING_PATTERNS:
130
+ if re.search(pattern, desc):
131
+ findings.append(Finding(
132
+ attack_class=attack_class,
133
+ severity=severity,
134
+ message=message,
135
+ location=f"tool:{name}/description",
136
+ pattern=pattern,
137
+ ))
138
+
139
+ # Check input schema for sensitive parameter names
140
+ # Match against param name only (not description) to avoid false positives
141
+ schema = tool.get("inputSchema", tool.get("input_schema", {}))
142
+ props = schema.get("properties", {})
143
+ for param_name, param_def in props.items():
144
+ for pattern, attack_class, severity, message in SCHEMA_RISKS:
145
+ if re.search(pattern, param_name):
146
+ findings.append(Finding(
147
+ attack_class=attack_class,
148
+ severity=severity,
149
+ message=message,
150
+ location=f"tool:{name}/param:{param_name}",
151
+ pattern=pattern,
152
+ ))
153
+
154
+ # Also check param descriptions for HIGH-confidence poisoning only
155
+ # Param descriptions are less exploitable than tool descriptions,
156
+ # so only flag the most dangerous patterns (instruction override, secrecy, exfil)
157
+ param_desc = param_def.get("description", "")
158
+ if param_desc:
159
+ PARAM_DESC_PATTERNS = [
160
+ POISONING_PATTERNS[0], # ignore previous instructions
161
+ POISONING_PATTERNS[2], # do not tell/reveal
162
+ POISONING_PATTERNS[3], # system prompt injection
163
+ POISONING_PATTERNS[5], # forward credentials
164
+ POISONING_PATTERNS[6], # send data to
165
+ ]
166
+ for pattern, attack_class, severity, msg in PARAM_DESC_PATTERNS:
167
+ if re.search(pattern, param_desc):
168
+ findings.append(Finding(
169
+ attack_class=attack_class,
170
+ severity=severity,
171
+ message=f"Param description: {msg}",
172
+ location=f"tool:{name}/param:{param_name}",
173
+ pattern=pattern,
174
+ ))
175
+
176
+ return findings
177
+
178
+
179
+ def scan_metadata(metadata: dict[str, Any]) -> list[Finding]:
180
+ """Scan server metadata for risk signals."""
181
+ findings = []
182
+
183
+ if not metadata.get("readme") and not metadata.get("description"):
184
+ findings.append(Finding("MIRAGE", "LOW",
185
+ "No README or documentation — reduced transparency",
186
+ location="server/metadata"))
187
+
188
+ if not metadata.get("license"):
189
+ findings.append(Finding("MIRAGE", "LOW",
190
+ "No license specified — unclear usage terms",
191
+ location="server/metadata"))
192
+
193
+ tool_count = metadata.get("tool_count", 0)
194
+ if tool_count > 25:
195
+ findings.append(Finding("MIRAGE", "MEDIUM",
196
+ f"Unusually high number of tools ({tool_count}) — possible capability inflation",
197
+ location="server/metadata"))
198
+
199
+ if not metadata.get("authentication"):
200
+ findings.append(Finding("LEECH", "LOW",
201
+ "No authentication documented",
202
+ location="server/metadata"))
203
+
204
+ return findings
205
+
206
+
207
+ def calculate_score(findings: list[Finding], tools: list[dict], metadata: dict) -> ScanResult:
208
+ """Calculate AEGIS trust score from scan findings."""
209
+
210
+ # Start with base scores
211
+ dims = DimensionScores()
212
+
213
+ # ── Trust Signals (35%) ──
214
+ # Based on metadata quality
215
+ # When source is a registry, some fields may simply not be exposed
216
+ # Treat missing-but-not-applicable fields as neutral
217
+ source = metadata.get("source", "")
218
+ is_registry = source in ("smithery", "mcp.run")
219
+
220
+ has_readme = bool(metadata.get("readme") or metadata.get("description"))
221
+ has_license = bool(metadata.get("license"))
222
+ has_repo = bool(metadata.get("repository"))
223
+ has_auth = bool(metadata.get("authentication"))
224
+
225
+ signal_score = 30.0 # base
226
+ if has_readme:
227
+ signal_score += 20
228
+ if has_license:
229
+ signal_score += 15
230
+ elif is_registry:
231
+ signal_score += 8 # neutral — registry doesn't expose this
232
+ if has_repo:
233
+ signal_score += 20
234
+ if has_auth:
235
+ signal_score += 15
236
+ elif is_registry:
237
+ signal_score += 8 # neutral — registry doesn't expose this
238
+ dims.trust_signals = min(100, signal_score)
239
+
240
+ # ── Manipulation Risk (25%) ──
241
+ severity_weights = {"CRITICAL": 30, "HIGH": 15, "MEDIUM": 5, "LOW": 2}
242
+ risk_score = 0
243
+ for f in findings:
244
+ risk_score += severity_weights.get(f.severity, 0)
245
+ dims.manipulation_risk = min(100, risk_score)
246
+
247
+ # ── Protection Level (15%) ──
248
+ protection = 40.0 # base
249
+ if has_auth:
250
+ protection += 30
251
+ tool_count = len(tools)
252
+ if 1 <= tool_count <= 15:
253
+ protection += 20 # reasonable scope
254
+ if has_license:
255
+ protection += 10
256
+ dims.protection_level = min(100, protection)
257
+
258
+ # ── Vulnerability Index (15%) ──
259
+ vuln = 20.0 # base vulnerability
260
+ critical_findings = sum(1 for f in findings if f.severity == "CRITICAL")
261
+ high_findings = sum(1 for f in findings if f.severity == "HIGH")
262
+ vuln += critical_findings * 25
263
+ vuln += high_findings * 10
264
+ if not has_auth:
265
+ vuln += 10
266
+ dims.vulnerability_index = min(100, vuln)
267
+
268
+ # ── Context Modifier (10%) ──
269
+ dims.context_modifier = 0 # neutral for static analysis
270
+
271
+ # ── Overall Score ──
272
+ # Higher trust_signals and protection = better
273
+ # Higher manipulation_risk and vulnerability = worse
274
+ overall = (
275
+ dims.trust_signals * 0.35
276
+ + (100 - dims.manipulation_risk) * 0.25
277
+ + dims.protection_level * 0.15
278
+ + (100 - dims.vulnerability_index) * 0.15
279
+ + (50 + dims.context_modifier * 5) * 0.10
280
+ )
281
+ overall = max(0, min(100, overall))
282
+
283
+ tier = _score_to_tier(overall)
284
+
285
+ # Recommendation
286
+ if overall >= 70 and critical_findings == 0:
287
+ recommendation = "SAFE"
288
+ elif overall >= 40 and critical_findings == 0:
289
+ recommendation = "CAUTION"
290
+ else:
291
+ recommendation = "AVOID"
292
+
293
+ return ScanResult(
294
+ target=metadata.get("name", "unknown"),
295
+ trust_score=round(overall, 1),
296
+ trust_tier=tier,
297
+ dimensions=dims,
298
+ findings=findings,
299
+ tools_scanned=len(tools),
300
+ recommendation=recommendation,
301
+ )
302
+
303
+
304
+ def scan_server(tools: list[dict[str, Any]], metadata: dict[str, Any]) -> ScanResult:
305
+ """Full scan pipeline: analyze tools + metadata, calculate score."""
306
+ findings = []
307
+ findings.extend(scan_tool_descriptions(tools))
308
+ findings.extend(scan_metadata(metadata))
309
+ return calculate_score(findings, tools, metadata)