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.
- scripts/validate_a2a_agent.py +148 -0
- scripts/validate_agent.py +67 -0
- tests/test_tunnel.py +76 -0
- tunnel_manager/__init__.py +66 -0
- tunnel_manager/__main__.py +6 -0
- tunnel_manager/mcp_config.json +8 -0
- tunnel_manager/middlewares.py +53 -0
- tunnel_manager/skills/tunnel-manager-remote-access/SKILL.md +51 -0
- tunnel_manager/tunnel_manager.py +990 -0
- tunnel_manager/tunnel_manager_agent.py +350 -0
- tunnel_manager/tunnel_manager_mcp.py +2600 -0
- tunnel_manager/utils.py +110 -0
- tunnel_manager-1.0.9.dist-info/METADATA +565 -0
- tunnel_manager-1.0.9.dist-info/RECORD +18 -0
- tunnel_manager-1.0.9.dist-info/WHEEL +5 -0
- tunnel_manager-1.0.9.dist-info/entry_points.txt +4 -0
- tunnel_manager-1.0.9.dist-info/licenses/LICENSE +20 -0
- tunnel_manager-1.0.9.dist-info/top_level.txt +3 -0
|
@@ -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,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"
|