tunnel-manager 1.0.9__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.
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ import asyncio
3
+ import httpx
4
+ import json
5
+ import uuid
6
+
7
+ # Configuration
8
+ A2A_URL = (
9
+ "http://localhost:9002/a2a/" # Discovered endpoint is POST / based on 405 on GET /
10
+ )
11
+
12
+
13
+ async def main():
14
+ print(f"Validating A2A Agent at {A2A_URL}...")
15
+
16
+ questions = [
17
+ "Run 'ls' command on the remote host and share the output",
18
+ ]
19
+
20
+ async with httpx.AsyncClient(timeout=10000.0) as client:
21
+ # First, let's verify connectivity and maybe infer output schema
22
+
23
+ for q in questions:
24
+ print(f"\n\n\nUser: {q}")
25
+ print("--- Sending Request ---")
26
+
27
+ # Construct JSON-RPC payload
28
+ payload = {
29
+ "jsonrpc": "2.0",
30
+ "method": "message/send",
31
+ "params": {
32
+ "message": {
33
+ "kind": "message",
34
+ "role": "user",
35
+ "parts": [{"kind": "text", "text": q}],
36
+ "messageId": str(uuid.uuid4()),
37
+ }
38
+ },
39
+ "id": 1,
40
+ }
41
+
42
+ try:
43
+ # Attempt POST to root
44
+ url = A2A_URL
45
+ print(f"Trying POST {url} with JSON-RPC (message/send)...")
46
+ resp = await client.post(
47
+ url, json=payload, headers={"Content-Type": "application/json"}
48
+ )
49
+
50
+ print(f"Status Code: {resp.status_code}")
51
+ if resp.status_code == 200:
52
+ try:
53
+ data = resp.json()
54
+ print(f"Response (JSON):\n{json.dumps(data, indent=2)}")
55
+
56
+ if "result" in data and "id" in data["result"]:
57
+ task_id = data["result"]["id"]
58
+ print(
59
+ f"\nTask Submitted with ID: {task_id}. Polling for result..."
60
+ )
61
+
62
+ # Poll tasks/get
63
+ while True:
64
+ await asyncio.sleep(2) # Wait a bit
65
+ poll_payload = {
66
+ "jsonrpc": "2.0",
67
+ "method": "tasks/get",
68
+ "params": {"id": task_id},
69
+ "id": 2,
70
+ }
71
+ poll_resp = await client.post(
72
+ url,
73
+ json=poll_payload,
74
+ headers={"Content-Type": "application/json"},
75
+ )
76
+ if poll_resp.status_code == 200:
77
+ poll_data = poll_resp.json()
78
+ if "result" in poll_data:
79
+ state = poll_data["result"]["status"]["state"]
80
+ print(f"Task State: {state}")
81
+ if state not in [
82
+ "submitted",
83
+ "running",
84
+ "working",
85
+ ]: # Assuming terminal states
86
+ print(
87
+ f"\nTask Finished with state: {state}"
88
+ )
89
+
90
+ # Extract final result
91
+ if "history" in poll_data["result"]:
92
+ history = poll_data["result"]["history"]
93
+ if history:
94
+ # Find last non-user message
95
+ last_msg = None
96
+ for msg in reversed(history):
97
+ if msg.get("role") != "user":
98
+ last_msg = msg
99
+ break
100
+
101
+ if last_msg and "parts" in last_msg:
102
+ print(
103
+ "\n--- Agent Response ---"
104
+ )
105
+ for part in last_msg["parts"]:
106
+ if "text" in part:
107
+ print(part["text"])
108
+ elif "content" in part:
109
+ print(part["content"])
110
+ elif last_msg:
111
+ print(
112
+ f"Final Message (No parts): {last_msg}"
113
+ )
114
+ else:
115
+ print(
116
+ "\n--- No Agent Response Found in History ---"
117
+ )
118
+
119
+ print(
120
+ f"Full Result Debug:\n{json.dumps(poll_data, indent=2)}"
121
+ )
122
+ break
123
+ else:
124
+ print("Starting polling error key check...")
125
+ if "error" in poll_data:
126
+ print(
127
+ f"Polling Error: {poll_data['error']}"
128
+ )
129
+ break
130
+ else:
131
+ print(f"Polling Failed: {poll_resp.status_code}")
132
+ print(f"Polling Error Details: {poll_resp.text}")
133
+ break
134
+
135
+ if "error" in data:
136
+ print(f"JSON-RPC Error: {data['error']}")
137
+ except json.JSONDecodeError:
138
+ print(f"Response (Text):\n{resp.text}")
139
+ else:
140
+ print(f"Error: {resp.status_code}")
141
+ print(resp.text)
142
+
143
+ except httpx.RequestError as e:
144
+ print(f"Connection failed to {url}: {e}")
145
+
146
+
147
+ if __name__ == "__main__":
148
+ asyncio.run(main())
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ import asyncio
3
+ import sys
4
+ from servicenow_api.servicenow_agent import stream_chat, chat, node_chat
5
+
6
+
7
+ # Attempt to import assuming dependencies are installed
8
+ import os
9
+
10
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
11
+
12
+ try:
13
+ from servicenow_api.servicenow_agent import create_agent
14
+ except ImportError as e:
15
+ print(f"Import Error: {e}")
16
+ print("Please install dependencies via `pip install .[all]`")
17
+ sys.exit(1)
18
+
19
+
20
+ async def main():
21
+ print("Initializing A2A Agent...")
22
+ try:
23
+ agent = create_agent(
24
+ provider="openai",
25
+ model_id=os.getenv("MODEL_ID", "qwen/qwen3-8b"),
26
+ base_url=os.getenv(
27
+ "OPENAI_BASE_URL", "http://localhost:1234/v1"
28
+ ), # 127.0.0.1
29
+ api_key=os.getenv("OPENAI_API_KEY", "llama"),
30
+ mcp_url=os.getenv("MCP_URL", "http://localhost:8005/mcp"), # 127.0.0.1
31
+ mcp_config=None,
32
+ )
33
+
34
+ print("Agent initialized successfully.")
35
+
36
+ # Define sample questions
37
+ questions = [
38
+ "Can you list the incidents",
39
+ # "Can you create an incident with the description: Test, Title: Test, ...."
40
+ ]
41
+
42
+ print("\n--- Starting Sample Chat Validation ---\n")
43
+
44
+ for q in questions:
45
+ print(f"\n\n\nUser: {q}")
46
+ try:
47
+ # Only run one to test
48
+ await stream_chat(agent=agent, prompt=q)
49
+ await chat(agent=agent, prompt=q)
50
+ await node_chat(agent=agent, prompt=q)
51
+ if hasattr(agent, "tools"):
52
+ print(f"Agent Tools: {[t.__name__ for t in agent.tools]}")
53
+ elif hasattr(agent, "_tools"):
54
+ print(f"Agent Tools: {[t.__name__ for t in agent._tools]}")
55
+
56
+ except Exception as e:
57
+ print(f"\n\nError processing question '{q}': {e}")
58
+
59
+ except Exception as e:
60
+ print(f"Validation failed with error: {e}")
61
+ import traceback
62
+
63
+ traceback.print_exc()
64
+
65
+
66
+ if __name__ == "__main__":
67
+ asyncio.run(main())
tests/test_tunnel.py ADDED
@@ -0,0 +1,76 @@
1
+ import os
2
+ from tunnel_manager.tunnel_manager import Tunnel
3
+
4
+ username = os.environ.get("TUNNEL_USERNAME")
5
+ password = os.environ.get("TUNNEL_PASSWORD")
6
+
7
+
8
+ def test_password_authentication():
9
+ print("Testing password-based authentication...")
10
+ try:
11
+ # Initialize tunnel with username and password
12
+ tunnel = Tunnel(
13
+ remote_host="10.0.0.11",
14
+ username=username,
15
+ password=password,
16
+ )
17
+
18
+ # Connect to the remote host
19
+ tunnel.connect()
20
+
21
+ # Run a simple command
22
+ out, err = tunnel.run_command("whoami; cd ~/Development/; ls -la")
23
+ print(f"Command 'whoami' output: {out}")
24
+ if err:
25
+ print(f"Command error: {err}")
26
+
27
+ print(f"Command output: {out}")
28
+ # Example file transfer (uncomment to test, ensure files exist)
29
+ tunnel.send_file(
30
+ "/home/genius/Development/inventory/inventory.yml",
31
+ "/home/genius/Downloads/remote_test.txt",
32
+ )
33
+ tunnel.receive_file(
34
+ "/home/genius/Downloads/remote_test.txt", "./tests/downloaded_inventory.txt"
35
+ )
36
+
37
+ tunnel.close()
38
+ print("Password-based authentication test completed successfully.")
39
+ except Exception as e:
40
+ print(f"Password-based authentication test failed: {str(e)}")
41
+
42
+
43
+ def test_key_authentication():
44
+ print("\nTesting key-based authentication...")
45
+ try:
46
+ # Initialize tunnel with identity file
47
+ tunnel = Tunnel(
48
+ remote_host="10.0.0.11",
49
+ username=username,
50
+ identity_file=os.path.expanduser("~/.ssh/id_rsa"),
51
+ )
52
+
53
+ # Connect to the remote host
54
+ tunnel.connect()
55
+
56
+ # Run a simple command
57
+ out, err = tunnel.run_command("whoami")
58
+ print(f"Command 'whoami' output: {out}")
59
+ if err:
60
+ print(f"Command error: {err}")
61
+
62
+ # Example file transfer (uncomment to test, ensure files exist)
63
+ # tunnel.send_file("local_test.txt", "/home/genius/remote_test.txt")
64
+ # tunnel.receive_file("/home/genius/remote_test.txt", "downloaded_test.txt")
65
+
66
+ tunnel.close()
67
+ print("Key-based authentication test completed successfully.")
68
+ except Exception as e:
69
+ print(f"Key-based authentication test failed: {str(e)}")
70
+
71
+
72
+ if __name__ == "__main__":
73
+ print("Starting SSH Tunnel Tests\n")
74
+ test_password_authentication()
75
+ test_key_authentication()
76
+ print("\nAll tests completed.")
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ import importlib
5
+ import inspect
6
+ from typing import List
7
+
8
+ __all__: List[str] = []
9
+
10
+ # Core modules – always available
11
+ CORE_MODULES = [
12
+ "tunnel_manager.tunnel_manager_mcp",
13
+ ]
14
+
15
+ # Optional modules
16
+ OPTIONAL_MODULES = {
17
+ "tunnel_manager.tunnel_manager_agent": "a2a",
18
+ "tunnel_manager.tunnel_manager_mcp": "mcp",
19
+ }
20
+
21
+
22
+ def _import_module_safely(module_name: str):
23
+ """Try to import a module and return it, or None if not available."""
24
+ try:
25
+ return importlib.import_module(module_name)
26
+ except ImportError:
27
+ return None
28
+
29
+
30
+ def _expose_members(module):
31
+ """Expose public classes and functions from a module into globals and __all__."""
32
+ for name, obj in inspect.getmembers(module):
33
+ if (inspect.isclass(obj) or inspect.isfunction(obj)) and not name.startswith(
34
+ "_"
35
+ ):
36
+ globals()[name] = obj
37
+ __all__.append(name)
38
+
39
+
40
+ # Always import core modules
41
+ for module_name in CORE_MODULES:
42
+ module = importlib.import_module(module_name)
43
+ _expose_members(module)
44
+
45
+ # Conditionally import optional modules
46
+ for module_name, extra_name in OPTIONAL_MODULES.items():
47
+ module = _import_module_safely(module_name)
48
+ if module is not None:
49
+ _expose_members(module)
50
+ globals()[f"_{extra_name.upper()}_AVAILABLE"] = True
51
+ else:
52
+ globals()[f"_{extra_name.upper()}_AVAILABLE"] = False
53
+
54
+ _MCP_AVAILABLE = OPTIONAL_MODULES.get("tunnel_manager.tunnel_manager_mcp") in [
55
+ m.__name__ for m in globals().values() if hasattr(m, "__name__")
56
+ ]
57
+ _A2A_AVAILABLE = "tunnel_manager.tunnel_manager_agent" in globals()
58
+
59
+ __all__.extend(["_MCP_AVAILABLE", "_A2A_AVAILABLE"])
60
+
61
+
62
+ """
63
+ tunnel-manager
64
+
65
+ Tunnel Manager MCP Server
66
+ """
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/python
2
+ # coding: utf-8
3
+ from tunnel_manager.tunnel_manager_mcp import tunnel_manager_mcp
4
+
5
+ if __name__ == "__main__":
6
+ tunnel_manager_mcp()
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "tunnel-manager": {
4
+ "url": "${MCP_URL:-http://localhost:8000/mcp}",
5
+ "timeout": 300000
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,53 @@
1
+ import threading
2
+ from fastmcp.server.middleware import MiddlewareContext, Middleware
3
+ from fastmcp.utilities.logging import get_logger
4
+
5
+ # Thread-local storage for user token
6
+ local = threading.local()
7
+ logger = get_logger(name="TokenMiddleware")
8
+
9
+
10
+ class UserTokenMiddleware(Middleware):
11
+ def __init__(self, config: dict):
12
+ self.config = config
13
+
14
+ async def on_request(self, context: MiddlewareContext, call_next):
15
+ logger.debug(f"Delegation enabled: {self.config['enable_delegation']}")
16
+ if self.config["enable_delegation"]:
17
+ headers = getattr(context.message, "headers", {})
18
+ auth = headers.get("Authorization")
19
+ if auth and auth.startswith("Bearer "):
20
+ token = auth.split(" ")[1]
21
+ local.user_token = token
22
+ local.user_claims = None # Will be populated by JWTVerifier
23
+
24
+ # Extract claims if JWTVerifier already validated
25
+ if hasattr(context, "auth") and hasattr(context.auth, "claims"):
26
+ local.user_claims = context.auth.claims
27
+ logger.info(
28
+ "Stored JWT claims for delegation",
29
+ extra={"subject": context.auth.claims.get("sub")},
30
+ )
31
+ else:
32
+ logger.debug("JWT claims not yet available (will be after auth)")
33
+
34
+ logger.info("Extracted Bearer token for delegation")
35
+ else:
36
+ logger.error("Missing or invalid Authorization header")
37
+ raise ValueError("Missing or invalid Authorization header")
38
+ return await call_next(context)
39
+
40
+
41
+ class JWTClaimsLoggingMiddleware(Middleware):
42
+ async def on_response(self, context: MiddlewareContext, call_next):
43
+ response = await call_next(context)
44
+ logger.info(f"JWT Response: {response}")
45
+ if hasattr(context, "auth") and hasattr(context.auth, "claims"):
46
+ logger.info(
47
+ "JWT Authentication Success",
48
+ extra={
49
+ "subject": context.auth.claims.get("sub"),
50
+ "client_id": context.auth.claims.get("client_id"),
51
+ "scopes": context.auth.claims.get("scope"),
52
+ },
53
+ )
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: tunnel-manager-remote-access
3
+ description: Tunnel Manager Remote Access capabilities for A2A Agent.
4
+ ---
5
+ ### Overview
6
+ This skill provides access to remote_access operations.
7
+
8
+ ### Capabilities
9
+ - **run_command_on_remote_host**: Run shell command on remote host. Expected return object type: dict
10
+ - **send_file_to_remote_host**: Upload file to remote host. Expected return object type: dict
11
+ - **receive_file_from_remote_host**: Download file from remote host. Expected return object type: dict
12
+ - **check_ssh_server**: Check SSH server status. Expected return object type: dict
13
+ - **test_key_auth**: Test key-based auth. Expected return object type: dict
14
+ - **setup_passwordless_ssh**: Setup passwordless SSH. Expected return object type: dict
15
+ - **copy_ssh_config**: Copy SSH config to remote host. Expected return object type: dict
16
+ - **rotate_ssh_key**: Rotate SSH key on remote host. Expected return object type: dict
17
+ - **remove_host_key**: Remove host key from known_hosts. Expected return object type: dict
18
+ - **configure_key_auth_on_inventory**: Setup passwordless SSH for all hosts in group. Expected return object type: dict
19
+ - **run_command_on_inventory**: Run command on all hosts in group. Expected return object type: dict
20
+ - **copy_ssh_config_on_inventory**: Copy SSH config to all hosts in YAML group. Expected return object type: dict
21
+ - **rotate_ssh_key_on_inventory**: Rotate SSH keys for all hosts in YAML group. Expected return object type: dict
22
+ - **send_file_to_inventory**: Upload a file to all hosts in the specified inventory group. Expected return object type: dict
23
+ - **receive_file_from_inventory**: Download a file from all hosts in the specified inventory group. Expected return object type: dict
24
+
25
+ ### Common Tools
26
+ - `run_command_on_remote_host`: Run shell command on remote host. Expected return object type: dict
27
+ - `send_file_to_remote_host`: Upload file to remote host. Expected return object type: dict
28
+ - `receive_file_from_remote_host`: Download file from remote host. Expected return object type: dict
29
+ - `check_ssh_server`: Check SSH server status. Expected return object type: dict
30
+ - `test_key_auth`: Test key-based auth. Expected return object type: dict
31
+ - `setup_passwordless_ssh`: Setup passwordless SSH. Expected return object type: dict
32
+ - `copy_ssh_config`: Copy SSH config to remote host. Expected return object type: dict
33
+ - `rotate_ssh_key`: Rotate SSH key on remote host. Expected return object type: dict
34
+ - `remove_host_key`: Remove host key from known_hosts. Expected return object type: dict
35
+ - `configure_key_auth_on_inventory`: Setup passwordless SSH for all hosts in group. Expected return object type: dict
36
+ - `run_command_on_inventory`: Run command on all hosts in group. Expected return object type: dict
37
+ - `copy_ssh_config_on_inventory`: Copy SSH config to all hosts in YAML group. Expected return object type: dict
38
+ - `rotate_ssh_key_on_inventory`: Rotate SSH keys for all hosts in YAML group. Expected return object type: dict
39
+ - `send_file_to_inventory`: Upload a file to all hosts in the specified inventory group. Expected return object type: dict
40
+ - `receive_file_from_inventory`: Download a file from all hosts in the specified inventory group. Expected return object type: dict
41
+
42
+ ### Usage Rules
43
+ - Use these tools when the user requests actions related to **remote_access**.
44
+ - Always interpret the output of these tools to provide a concise summary to the user.
45
+
46
+ ### Example Prompts
47
+ - "Please run command on remote host"
48
+ - "Please receive file from inventory"
49
+ - "Please remove host key"
50
+ - "Please copy ssh config"
51
+ - "Please run command on inventory"