agentmesh-platform 1.0.0a1__py3-none-any.whl → 1.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentmesh/__init__.py +6 -13
- agentmesh/cli/main.py +131 -0
- agentmesh/cli/proxy.py +448 -0
- agentmesh/core/__init__.py +7 -0
- agentmesh/core/identity/__init__.py +17 -0
- agentmesh/core/identity/ca.py +386 -0
- agentmesh/governance/policy.py +14 -11
- agentmesh/observability/__init__.py +16 -0
- agentmesh/observability/metrics.py +237 -0
- agentmesh/observability/tracing.py +203 -0
- agentmesh/services/__init__.py +10 -0
- agentmesh/services/audit/__init__.py +14 -0
- agentmesh/services/registry/__init__.py +12 -0
- agentmesh/services/registry/agent_registry.py +249 -0
- agentmesh/services/reward_engine/__init__.py +14 -0
- agentmesh/storage/__init__.py +18 -0
- agentmesh/storage/memory_provider.py +232 -0
- agentmesh/storage/postgres_provider.py +463 -0
- agentmesh/storage/provider.py +231 -0
- agentmesh/storage/redis_provider.py +223 -0
- agentmesh/trust/__init__.py +2 -1
- agentmesh/trust/bridge.py +37 -0
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/METADATA +132 -6
- agentmesh_platform-1.0.0a2.dist-info/RECORD +45 -0
- agentmesh_platform-1.0.0a1.dist-info/RECORD +0 -28
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/WHEEL +0 -0
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/entry_points.txt +0 -0
- {agentmesh_platform-1.0.0a1.dist-info → agentmesh_platform-1.0.0a2.dist-info}/licenses/LICENSE +0 -0
agentmesh/__init__.py
CHANGED
|
@@ -15,29 +15,25 @@ __version__ = "1.0.0-alpha"
|
|
|
15
15
|
from .identity import (
|
|
16
16
|
AgentIdentity,
|
|
17
17
|
AgentDID,
|
|
18
|
-
IdentityRegistry,
|
|
19
18
|
Credential,
|
|
20
19
|
CredentialManager,
|
|
21
20
|
DelegationChain,
|
|
22
21
|
DelegationLink,
|
|
23
22
|
HumanSponsor,
|
|
24
|
-
SponsorRegistry,
|
|
25
23
|
RiskScorer,
|
|
26
24
|
RiskScore,
|
|
27
25
|
SPIFFEIdentity,
|
|
26
|
+
SVID,
|
|
28
27
|
)
|
|
29
28
|
|
|
30
29
|
# Layer 2: Trust & Protocol Bridge
|
|
31
30
|
from .trust import (
|
|
32
31
|
TrustBridge,
|
|
33
32
|
ProtocolBridge,
|
|
34
|
-
A2AAdapter,
|
|
35
|
-
MCPAdapter,
|
|
36
33
|
TrustHandshake,
|
|
37
34
|
HandshakeResult,
|
|
38
35
|
CapabilityScope,
|
|
39
36
|
CapabilityGrant,
|
|
40
|
-
CapabilityRegistry,
|
|
41
37
|
)
|
|
42
38
|
|
|
43
39
|
# Layer 3: Governance & Compliance Plane
|
|
@@ -45,10 +41,10 @@ from .governance import (
|
|
|
45
41
|
PolicyEngine,
|
|
46
42
|
Policy,
|
|
47
43
|
PolicyRule,
|
|
48
|
-
|
|
44
|
+
PolicyDecision,
|
|
49
45
|
ComplianceEngine,
|
|
50
46
|
ComplianceFramework,
|
|
51
|
-
|
|
47
|
+
ComplianceReport,
|
|
52
48
|
AuditLog,
|
|
53
49
|
AuditEntry,
|
|
54
50
|
MerkleAuditChain,
|
|
@@ -73,22 +69,19 @@ __all__ = [
|
|
|
73
69
|
# Layer 1: Identity
|
|
74
70
|
"AgentIdentity",
|
|
75
71
|
"AgentDID",
|
|
76
|
-
"IdentityRegistry",
|
|
77
72
|
"Credential",
|
|
78
73
|
"CredentialManager",
|
|
79
74
|
"DelegationChain",
|
|
80
75
|
"DelegationLink",
|
|
81
76
|
"HumanSponsor",
|
|
82
|
-
"SponsorRegistry",
|
|
83
77
|
"RiskScorer",
|
|
84
78
|
"RiskScore",
|
|
85
79
|
"SPIFFEIdentity",
|
|
80
|
+
"SVID",
|
|
86
81
|
|
|
87
82
|
# Layer 2: Trust
|
|
88
83
|
"TrustBridge",
|
|
89
84
|
"ProtocolBridge",
|
|
90
|
-
"A2AAdapter",
|
|
91
|
-
"MCPAdapter",
|
|
92
85
|
"TrustHandshake",
|
|
93
86
|
"HandshakeResult",
|
|
94
87
|
"CapabilityScope",
|
|
@@ -99,10 +92,10 @@ __all__ = [
|
|
|
99
92
|
"PolicyEngine",
|
|
100
93
|
"Policy",
|
|
101
94
|
"PolicyRule",
|
|
102
|
-
"
|
|
95
|
+
"PolicyDecision",
|
|
103
96
|
"ComplianceEngine",
|
|
104
97
|
"ComplianceFramework",
|
|
105
|
-
"
|
|
98
|
+
"ComplianceReport",
|
|
106
99
|
"AuditLog",
|
|
107
100
|
"AuditEntry",
|
|
108
101
|
"MerkleAuditChain",
|
agentmesh/cli/main.py
CHANGED
|
@@ -3,6 +3,7 @@ AgentMesh CLI - Main Entry Point
|
|
|
3
3
|
|
|
4
4
|
Commands:
|
|
5
5
|
- init: Scaffold a governed agent in 30 seconds
|
|
6
|
+
- proxy: Start an MCP proxy with governance
|
|
6
7
|
- register: Register an agent with AgentMesh
|
|
7
8
|
- run: Run a governed agent
|
|
8
9
|
- status: Check agent status and trust score
|
|
@@ -16,6 +17,7 @@ from rich.table import Table
|
|
|
16
17
|
from rich.panel import Panel
|
|
17
18
|
from rich import box
|
|
18
19
|
from pathlib import Path
|
|
20
|
+
from typing import Optional
|
|
19
21
|
import json
|
|
20
22
|
import yaml
|
|
21
23
|
import os
|
|
@@ -396,6 +398,135 @@ def audit(agent: str, limit: int, fmt: str):
|
|
|
396
398
|
console.print(table)
|
|
397
399
|
|
|
398
400
|
|
|
401
|
+
# Import proxy command from proxy module
|
|
402
|
+
from .proxy import proxy
|
|
403
|
+
app.add_command(proxy)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@app.command()
|
|
407
|
+
@click.option("--claude", is_flag=True, help="Generate Claude Desktop config")
|
|
408
|
+
@click.option("--config-path", type=click.Path(), help="Path to claude_desktop_config.json")
|
|
409
|
+
@click.option("--backup/--no-backup", default=True, help="Backup existing config")
|
|
410
|
+
def init_integration(claude: bool, config_path: str, backup: bool):
|
|
411
|
+
"""
|
|
412
|
+
Initialize AgentMesh integration with existing tools.
|
|
413
|
+
|
|
414
|
+
Examples:
|
|
415
|
+
|
|
416
|
+
# Setup Claude Desktop to use AgentMesh proxy
|
|
417
|
+
agentmesh init-integration --claude
|
|
418
|
+
|
|
419
|
+
# Specify custom config path
|
|
420
|
+
agentmesh init-integration --claude --config-path ~/custom/config.json
|
|
421
|
+
"""
|
|
422
|
+
if claude:
|
|
423
|
+
_init_claude_integration(config_path, backup)
|
|
424
|
+
else:
|
|
425
|
+
console.print("[yellow]Please specify an integration type (e.g., --claude)[/yellow]")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _init_claude_integration(config_path: Optional[str], backup: bool):
|
|
429
|
+
"""Initialize Claude Desktop integration."""
|
|
430
|
+
console.print("\n[bold blue]🔧 Setting up Claude Desktop Integration[/bold blue]\n")
|
|
431
|
+
|
|
432
|
+
# Determine config path
|
|
433
|
+
if not config_path:
|
|
434
|
+
# Default Claude Desktop config locations
|
|
435
|
+
import platform
|
|
436
|
+
system = platform.system()
|
|
437
|
+
|
|
438
|
+
if system == "Darwin": # macOS
|
|
439
|
+
default_path = Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
440
|
+
elif system == "Windows":
|
|
441
|
+
default_path = Path.home() / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json"
|
|
442
|
+
else: # Linux
|
|
443
|
+
default_path = Path.home() / ".config" / "claude" / "claude_desktop_config.json"
|
|
444
|
+
|
|
445
|
+
config_path = default_path
|
|
446
|
+
else:
|
|
447
|
+
config_path = Path(config_path)
|
|
448
|
+
|
|
449
|
+
console.print(f"Config path: {config_path}")
|
|
450
|
+
|
|
451
|
+
# Check if config exists
|
|
452
|
+
if not config_path.exists():
|
|
453
|
+
console.print(f"[yellow]Config file not found at {config_path}[/yellow]")
|
|
454
|
+
console.print("Creating new config file...")
|
|
455
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
456
|
+
config = {"mcpServers": {}}
|
|
457
|
+
else:
|
|
458
|
+
# Backup existing config
|
|
459
|
+
if backup:
|
|
460
|
+
backup_path = config_path.with_suffix(".json.backup")
|
|
461
|
+
import shutil
|
|
462
|
+
shutil.copy(config_path, backup_path)
|
|
463
|
+
console.print(f"[dim]✓ Backed up existing config to {backup_path}[/dim]")
|
|
464
|
+
|
|
465
|
+
# Load existing config
|
|
466
|
+
with open(config_path) as f:
|
|
467
|
+
config = json.load(f)
|
|
468
|
+
|
|
469
|
+
# Ensure mcpServers exists
|
|
470
|
+
if "mcpServers" not in config:
|
|
471
|
+
config["mcpServers"] = {}
|
|
472
|
+
|
|
473
|
+
# Add example AgentMesh proxy configuration
|
|
474
|
+
example_server = {
|
|
475
|
+
"filesystem-protected": {
|
|
476
|
+
"command": "agentmesh",
|
|
477
|
+
"args": [
|
|
478
|
+
"proxy",
|
|
479
|
+
"--target", "npx",
|
|
480
|
+
"--target", "-y",
|
|
481
|
+
"--target", "@modelcontextprotocol/server-filesystem",
|
|
482
|
+
"--target", str(Path.home())
|
|
483
|
+
],
|
|
484
|
+
"env": {},
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# Check if already configured
|
|
489
|
+
has_agentmesh = any(
|
|
490
|
+
"agentmesh" in str(server.get("command", ""))
|
|
491
|
+
for server in config["mcpServers"].values()
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
if not has_agentmesh:
|
|
495
|
+
config["mcpServers"].update(example_server)
|
|
496
|
+
console.print("\n[green]✓ Added AgentMesh-protected filesystem server example[/green]")
|
|
497
|
+
else:
|
|
498
|
+
console.print("\n[yellow]⚠️ AgentMesh proxy already configured[/yellow]")
|
|
499
|
+
|
|
500
|
+
# Save updated config
|
|
501
|
+
with open(config_path, "w") as f:
|
|
502
|
+
json.dump(config, f, indent=2)
|
|
503
|
+
|
|
504
|
+
console.print(f"\n[green]✓ Updated {config_path}[/green]")
|
|
505
|
+
|
|
506
|
+
# Show instructions
|
|
507
|
+
console.print()
|
|
508
|
+
console.print(Panel(
|
|
509
|
+
"""[bold]Next Steps:[/bold]
|
|
510
|
+
|
|
511
|
+
1. Restart Claude Desktop
|
|
512
|
+
2. AgentMesh will now intercept all tool calls to the protected server
|
|
513
|
+
3. View logs in the terminal where Claude Desktop was launched
|
|
514
|
+
|
|
515
|
+
[bold]Customization:[/bold]
|
|
516
|
+
Edit {path} to:
|
|
517
|
+
- Add more protected servers
|
|
518
|
+
- Change policy level (--policy strict|moderate|permissive)
|
|
519
|
+
- Disable verification footers (--no-footer)
|
|
520
|
+
|
|
521
|
+
[bold]Example Usage:[/bold]
|
|
522
|
+
In Claude Desktop, try: "Read the contents of my home directory"
|
|
523
|
+
AgentMesh will enforce policies and add trust verification to outputs.
|
|
524
|
+
""".format(path=config_path),
|
|
525
|
+
title="🎉 Claude Desktop Integration Ready",
|
|
526
|
+
border_style="green",
|
|
527
|
+
))
|
|
528
|
+
|
|
529
|
+
|
|
399
530
|
def main():
|
|
400
531
|
"""Main entry point."""
|
|
401
532
|
app()
|
agentmesh/cli/proxy.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentMesh MCP Proxy
|
|
3
|
+
|
|
4
|
+
A transparent proxy for MCP (Model Context Protocol) servers that adds:
|
|
5
|
+
- Policy enforcement on tool calls
|
|
6
|
+
- Trust score tracking
|
|
7
|
+
- Audit logging
|
|
8
|
+
- Verification footers
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
agentmesh proxy --target npx -y @modelcontextprotocol/server-filesystem /path
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, Optional, List
|
|
20
|
+
import subprocess
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
|
|
25
|
+
from agentmesh import PolicyEngine, AuditLog, RewardEngine
|
|
26
|
+
from agentmesh.identity import AgentIdentity
|
|
27
|
+
|
|
28
|
+
console = Console(stderr=True) # Use stderr for logs to keep stdout clean for MCP
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MCPProxy:
|
|
32
|
+
"""
|
|
33
|
+
MCP Proxy that intercepts tool calls and enforces governance.
|
|
34
|
+
|
|
35
|
+
Sits between an MCP client (like Claude Desktop) and an MCP server,
|
|
36
|
+
intercepting JSON-RPC messages and enforcing policies.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
target_command: List[str],
|
|
42
|
+
policy: str = "strict",
|
|
43
|
+
identity_name: str = "mcp-proxy",
|
|
44
|
+
enable_footer: bool = True,
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Initialize the MCP proxy.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
target_command: Command to spawn the target MCP server
|
|
51
|
+
policy: Policy level (strict, moderate, permissive)
|
|
52
|
+
identity_name: Name for the proxy agent identity
|
|
53
|
+
enable_footer: Whether to add verification footers to outputs
|
|
54
|
+
"""
|
|
55
|
+
self.target_command = target_command
|
|
56
|
+
self.policy_level = policy
|
|
57
|
+
self.enable_footer = enable_footer
|
|
58
|
+
|
|
59
|
+
# Create proxy identity
|
|
60
|
+
console.print(f"[dim]🔐 Initializing AgentMesh proxy identity...[/dim]")
|
|
61
|
+
self.identity = AgentIdentity.create(
|
|
62
|
+
name=identity_name,
|
|
63
|
+
sponsor="proxy@agentmesh.ai",
|
|
64
|
+
capabilities=["tool:*"]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Initialize governance components
|
|
68
|
+
self.policy_engine = PolicyEngine()
|
|
69
|
+
self._load_default_policies()
|
|
70
|
+
|
|
71
|
+
self.audit_log = AuditLog()
|
|
72
|
+
|
|
73
|
+
self.reward_engine = RewardEngine()
|
|
74
|
+
self.trust_score = 800 # Starting score
|
|
75
|
+
|
|
76
|
+
# Process handle
|
|
77
|
+
self.target_process: Optional[subprocess.Popen] = None
|
|
78
|
+
|
|
79
|
+
console.print(f"[dim]✓ Proxy initialized with trust score: {self.trust_score}/1000[/dim]")
|
|
80
|
+
|
|
81
|
+
def _load_default_policies(self):
|
|
82
|
+
"""Load default policies based on policy level."""
|
|
83
|
+
if self.policy_level == "strict":
|
|
84
|
+
policy_yaml = """
|
|
85
|
+
version: "1.0"
|
|
86
|
+
name: "strict-mcp-policy"
|
|
87
|
+
description: "Strict policy for MCP tool calls"
|
|
88
|
+
agents: ["*"]
|
|
89
|
+
default_action: "deny"
|
|
90
|
+
rules:
|
|
91
|
+
- name: "block-etc-access"
|
|
92
|
+
description: "Block access to /etc"
|
|
93
|
+
condition: "action.path == '/etc/passwd' or action.path == '/etc/shadow'"
|
|
94
|
+
action: "deny"
|
|
95
|
+
priority: 100
|
|
96
|
+
enabled: true
|
|
97
|
+
|
|
98
|
+
- name: "block-root-access"
|
|
99
|
+
description: "Block access to /root"
|
|
100
|
+
condition: "action.path == '/root/.ssh'"
|
|
101
|
+
action: "deny"
|
|
102
|
+
priority: 100
|
|
103
|
+
enabled: true
|
|
104
|
+
|
|
105
|
+
- name: "block-dangerous-filesystem-ops"
|
|
106
|
+
description: "Block dangerous filesystem operations"
|
|
107
|
+
condition: "action.tool == 'filesystem_write' or action.tool == 'filesystem_delete'"
|
|
108
|
+
action: "deny"
|
|
109
|
+
priority: 90
|
|
110
|
+
enabled: true
|
|
111
|
+
|
|
112
|
+
- name: "allow-read-operations"
|
|
113
|
+
description: "Allow filesystem read operations"
|
|
114
|
+
condition: "action.tool == 'filesystem_read'"
|
|
115
|
+
action: "allow"
|
|
116
|
+
priority: 50
|
|
117
|
+
enabled: true
|
|
118
|
+
"""
|
|
119
|
+
elif self.policy_level == "moderate":
|
|
120
|
+
policy_yaml = """
|
|
121
|
+
version: "1.0"
|
|
122
|
+
name: "moderate-mcp-policy"
|
|
123
|
+
description: "Moderate policy for MCP tool calls"
|
|
124
|
+
agents: ["*"]
|
|
125
|
+
default_action: "allow"
|
|
126
|
+
rules:
|
|
127
|
+
- name: "warn-on-write"
|
|
128
|
+
description: "Warn on write operations"
|
|
129
|
+
condition: "action.tool == 'filesystem_write'"
|
|
130
|
+
action: "warn"
|
|
131
|
+
priority: 50
|
|
132
|
+
enabled: true
|
|
133
|
+
"""
|
|
134
|
+
else: # permissive
|
|
135
|
+
policy_yaml = """
|
|
136
|
+
version: "1.0"
|
|
137
|
+
name: "permissive-mcp-policy"
|
|
138
|
+
description: "Permissive policy for MCP tool calls"
|
|
139
|
+
agents: ["*"]
|
|
140
|
+
default_action: "allow"
|
|
141
|
+
rules: []
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
self.policy_engine.load_yaml(policy_yaml)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
console.print(f"[yellow]⚠️ Warning: Could not load policy: {e}[/yellow]")
|
|
148
|
+
|
|
149
|
+
async def start(self):
|
|
150
|
+
"""Start the proxy server."""
|
|
151
|
+
console.print(f"[dim]🚀 Starting MCP proxy...[/dim]")
|
|
152
|
+
console.print(f"[dim] Target: {' '.join(self.target_command)}[/dim]")
|
|
153
|
+
console.print(f"[dim] Policy: {self.policy_level}[/dim]")
|
|
154
|
+
console.print(f"[dim] Agent DID: {self.identity.did}[/dim]")
|
|
155
|
+
|
|
156
|
+
# Start target MCP server as subprocess
|
|
157
|
+
self.target_process = subprocess.Popen(
|
|
158
|
+
self.target_command,
|
|
159
|
+
stdin=subprocess.PIPE,
|
|
160
|
+
stdout=subprocess.PIPE,
|
|
161
|
+
stderr=subprocess.PIPE,
|
|
162
|
+
bufsize=0,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
console.print(f"[dim]✓ Target server started (PID: {self.target_process.pid})[/dim]")
|
|
166
|
+
console.print(f"[dim]🔒 AgentMesh governance active[/dim]")
|
|
167
|
+
|
|
168
|
+
# Start message handling loops
|
|
169
|
+
read_task = asyncio.create_task(self._read_from_target())
|
|
170
|
+
write_task = asyncio.create_task(self._read_from_client())
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
await asyncio.gather(read_task, write_task)
|
|
174
|
+
except KeyboardInterrupt:
|
|
175
|
+
console.print("\n[dim]🛑 Shutting down proxy...[/dim]")
|
|
176
|
+
finally:
|
|
177
|
+
if self.target_process:
|
|
178
|
+
self.target_process.terminate()
|
|
179
|
+
self.target_process.wait()
|
|
180
|
+
|
|
181
|
+
async def _read_from_client(self):
|
|
182
|
+
"""Read JSON-RPC messages from stdin (MCP client)."""
|
|
183
|
+
loop = asyncio.get_event_loop()
|
|
184
|
+
|
|
185
|
+
while True:
|
|
186
|
+
try:
|
|
187
|
+
# Read a line from stdin
|
|
188
|
+
line = await loop.run_in_executor(None, sys.stdin.readline)
|
|
189
|
+
if not line:
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
# Parse JSON-RPC message
|
|
193
|
+
try:
|
|
194
|
+
message = json.loads(line.strip())
|
|
195
|
+
except json.JSONDecodeError:
|
|
196
|
+
# Not a JSON message, pass through
|
|
197
|
+
self._write_to_target(line)
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
# Intercept tool calls
|
|
201
|
+
if message.get("method") == "tools/call":
|
|
202
|
+
message = await self._handle_tool_call(message)
|
|
203
|
+
|
|
204
|
+
# Forward to target
|
|
205
|
+
self._write_to_target(json.dumps(message) + "\n")
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
console.print(f"[red]Error reading from client: {e}[/red]")
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
async def _read_from_target(self):
|
|
212
|
+
"""Read responses from target MCP server."""
|
|
213
|
+
loop = asyncio.get_event_loop()
|
|
214
|
+
|
|
215
|
+
while True:
|
|
216
|
+
try:
|
|
217
|
+
# Read from target stdout
|
|
218
|
+
line = await loop.run_in_executor(
|
|
219
|
+
None,
|
|
220
|
+
self.target_process.stdout.readline
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if not line:
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
# Parse JSON-RPC response
|
|
227
|
+
try:
|
|
228
|
+
message = json.loads(line.decode().strip())
|
|
229
|
+
|
|
230
|
+
# Add verification footer if enabled
|
|
231
|
+
if self.enable_footer and "result" in message:
|
|
232
|
+
message = self._add_verification_footer(message)
|
|
233
|
+
|
|
234
|
+
sys.stdout.write(json.dumps(message) + "\n")
|
|
235
|
+
sys.stdout.flush()
|
|
236
|
+
except json.JSONDecodeError:
|
|
237
|
+
# Not JSON, pass through
|
|
238
|
+
sys.stdout.write(line.decode())
|
|
239
|
+
sys.stdout.flush()
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
console.print(f"[red]Error reading from target: {e}[/red]")
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
def _write_to_target(self, data: str):
|
|
246
|
+
"""Write data to target server stdin."""
|
|
247
|
+
try:
|
|
248
|
+
self.target_process.stdin.write(data.encode())
|
|
249
|
+
self.target_process.stdin.flush()
|
|
250
|
+
except Exception as e:
|
|
251
|
+
console.print(f"[red]Error writing to target: {e}[/red]")
|
|
252
|
+
|
|
253
|
+
async def _handle_tool_call(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
254
|
+
"""
|
|
255
|
+
Handle a tools/call request - enforce policy.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
message: The JSON-RPC message
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Modified message (allowed) or error response (blocked)
|
|
262
|
+
"""
|
|
263
|
+
params = message.get("params", {})
|
|
264
|
+
tool_name = params.get("name", "unknown")
|
|
265
|
+
arguments = params.get("arguments", {})
|
|
266
|
+
|
|
267
|
+
# Build policy context
|
|
268
|
+
context = {
|
|
269
|
+
"action": {
|
|
270
|
+
"tool": tool_name,
|
|
271
|
+
"path": arguments.get("path", ""),
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# Check policy
|
|
276
|
+
decision = self.policy_engine.evaluate(self.identity.did, context)
|
|
277
|
+
|
|
278
|
+
# Log the decision
|
|
279
|
+
self._audit_log_tool_call(tool_name, arguments, decision)
|
|
280
|
+
|
|
281
|
+
if not decision.allowed:
|
|
282
|
+
console.print(
|
|
283
|
+
f"[yellow]🔒 BLOCKED: {tool_name} - {decision.reason}[/yellow]"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Return error response
|
|
287
|
+
error_response = {
|
|
288
|
+
"jsonrpc": "2.0",
|
|
289
|
+
"id": message.get("id"),
|
|
290
|
+
"error": {
|
|
291
|
+
"code": -32001,
|
|
292
|
+
"message": f"Policy violation: {decision.reason}",
|
|
293
|
+
"data": {
|
|
294
|
+
"agentmesh": {
|
|
295
|
+
"blocked": True,
|
|
296
|
+
"policy": decision.policy_name,
|
|
297
|
+
"rule": decision.matched_rule,
|
|
298
|
+
"trust_score": self.trust_score,
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Don't forward to target, return error directly
|
|
305
|
+
sys.stdout.write(json.dumps(error_response) + "\n")
|
|
306
|
+
sys.stdout.flush()
|
|
307
|
+
|
|
308
|
+
# Return a marker to skip forwarding
|
|
309
|
+
return {"_agentmesh_blocked": True}
|
|
310
|
+
|
|
311
|
+
if decision.action == "warn":
|
|
312
|
+
console.print(
|
|
313
|
+
f"[yellow]⚠️ WARNING: {tool_name} - {decision.reason}[/yellow]"
|
|
314
|
+
)
|
|
315
|
+
else:
|
|
316
|
+
console.print(f"[dim]✓ Allowed: {tool_name}[/dim]")
|
|
317
|
+
|
|
318
|
+
# Update trust score
|
|
319
|
+
self._update_trust_score(tool_name, allowed=True)
|
|
320
|
+
|
|
321
|
+
return message
|
|
322
|
+
|
|
323
|
+
def _add_verification_footer(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
324
|
+
"""Add AgentMesh verification footer to tool output."""
|
|
325
|
+
result = message.get("result", {})
|
|
326
|
+
|
|
327
|
+
# Check if result has content we can append to
|
|
328
|
+
if isinstance(result, dict) and "content" in result:
|
|
329
|
+
content_list = result.get("content", [])
|
|
330
|
+
|
|
331
|
+
# Get DID as string
|
|
332
|
+
did_str = str(self.identity.did) if hasattr(self.identity.did, '__str__') else self.identity.did
|
|
333
|
+
|
|
334
|
+
# Add footer as a new content item
|
|
335
|
+
footer = {
|
|
336
|
+
"type": "text",
|
|
337
|
+
"text": (
|
|
338
|
+
f"\n\n> 🔒 Verified by AgentMesh (Trust Score: {self.trust_score}/1000)\n"
|
|
339
|
+
f"> Agent: {did_str[:40]}...\n"
|
|
340
|
+
f"> Policy: {self.policy_level} | Audit: Enabled"
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if isinstance(content_list, list):
|
|
345
|
+
content_list.append(footer)
|
|
346
|
+
|
|
347
|
+
result["content"] = content_list
|
|
348
|
+
message["result"] = result
|
|
349
|
+
|
|
350
|
+
return message
|
|
351
|
+
|
|
352
|
+
def _audit_log_tool_call(
|
|
353
|
+
self,
|
|
354
|
+
tool_name: str,
|
|
355
|
+
arguments: Dict[str, Any],
|
|
356
|
+
decision: Any
|
|
357
|
+
):
|
|
358
|
+
"""Log tool call to audit trail."""
|
|
359
|
+
entry = {
|
|
360
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
361
|
+
"agent": self.identity.did,
|
|
362
|
+
"action": "mcp_tool_call",
|
|
363
|
+
"tool": tool_name,
|
|
364
|
+
"arguments": arguments,
|
|
365
|
+
"decision": decision.action,
|
|
366
|
+
"allowed": decision.allowed,
|
|
367
|
+
"policy": decision.policy_name,
|
|
368
|
+
"rule": decision.matched_rule,
|
|
369
|
+
"trust_score": self.trust_score,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
# In production, would write to persistent audit log
|
|
373
|
+
# For now, just track in memory
|
|
374
|
+
console.print(f"[dim]📋 Audit: {tool_name} - {decision.action}[/dim]")
|
|
375
|
+
|
|
376
|
+
def _update_trust_score(self, tool_name: str, allowed: bool):
|
|
377
|
+
"""Update trust score based on tool usage."""
|
|
378
|
+
if allowed:
|
|
379
|
+
# Small increase for allowed actions
|
|
380
|
+
self.trust_score = min(1000, self.trust_score + 1)
|
|
381
|
+
else:
|
|
382
|
+
# Larger decrease for blocked actions
|
|
383
|
+
self.trust_score = max(0, self.trust_score - 10)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@click.command()
|
|
387
|
+
@click.option(
|
|
388
|
+
"--target",
|
|
389
|
+
"-t",
|
|
390
|
+
multiple=True,
|
|
391
|
+
required=True,
|
|
392
|
+
help="Target MCP server command (can specify multiple times for args)",
|
|
393
|
+
)
|
|
394
|
+
@click.option(
|
|
395
|
+
"--policy",
|
|
396
|
+
"-p",
|
|
397
|
+
type=click.Choice(["strict", "moderate", "permissive"]),
|
|
398
|
+
default="strict",
|
|
399
|
+
help="Policy enforcement level",
|
|
400
|
+
)
|
|
401
|
+
@click.option(
|
|
402
|
+
"--no-footer",
|
|
403
|
+
is_flag=True,
|
|
404
|
+
help="Disable verification footers in output",
|
|
405
|
+
)
|
|
406
|
+
@click.option(
|
|
407
|
+
"--identity",
|
|
408
|
+
"-i",
|
|
409
|
+
default="mcp-proxy",
|
|
410
|
+
help="Agent identity name",
|
|
411
|
+
)
|
|
412
|
+
def proxy(target: tuple, policy: str, no_footer: bool, identity: str):
|
|
413
|
+
"""
|
|
414
|
+
Start an AgentMesh MCP proxy.
|
|
415
|
+
|
|
416
|
+
Wraps an existing MCP server with governance, policy enforcement,
|
|
417
|
+
and trust scoring.
|
|
418
|
+
|
|
419
|
+
Examples:
|
|
420
|
+
|
|
421
|
+
# Proxy a filesystem server
|
|
422
|
+
agentmesh proxy --target npx --target -y \\
|
|
423
|
+
--target @modelcontextprotocol/server-filesystem --target /Users/me
|
|
424
|
+
|
|
425
|
+
# Moderate policy with no footers
|
|
426
|
+
agentmesh proxy --policy moderate --no-footer \\
|
|
427
|
+
--target python --target my_mcp_server.py
|
|
428
|
+
"""
|
|
429
|
+
# Convert tuple to list
|
|
430
|
+
target_cmd = list(target)
|
|
431
|
+
|
|
432
|
+
if not target_cmd:
|
|
433
|
+
console.print("[red]Error: --target is required[/red]")
|
|
434
|
+
sys.exit(1)
|
|
435
|
+
|
|
436
|
+
# Create and start proxy
|
|
437
|
+
proxy_server = MCPProxy(
|
|
438
|
+
target_command=target_cmd,
|
|
439
|
+
policy=policy,
|
|
440
|
+
identity_name=identity,
|
|
441
|
+
enable_footer=not no_footer,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
asyncio.run(proxy_server.start())
|
|
446
|
+
except KeyboardInterrupt:
|
|
447
|
+
console.print("\n[dim]Proxy stopped[/dim]")
|
|
448
|
+
sys.exit(0)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Identity Module
|
|
3
|
+
|
|
4
|
+
Certificate Authority for issuing SPIFFE/SVID certificates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .ca import (
|
|
8
|
+
CertificateAuthority,
|
|
9
|
+
RegistrationRequest,
|
|
10
|
+
RegistrationResponse,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CertificateAuthority",
|
|
15
|
+
"RegistrationRequest",
|
|
16
|
+
"RegistrationResponse",
|
|
17
|
+
]
|