agent2win 1.0.0__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.
- agent2win/__init__.py +3 -0
- agent2win/audio.py +160 -0
- agent2win/cli.py +113 -0
- agent2win/clipboard.py +90 -0
- agent2win/commands.py +89 -0
- agent2win/config.py +64 -0
- agent2win/environment.py +220 -0
- agent2win/filesystem.py +197 -0
- agent2win/gui.py +141 -0
- agent2win/input_control.py +155 -0
- agent2win/logger.py +41 -0
- agent2win/network.py +199 -0
- agent2win/notifications.py +207 -0
- agent2win/processes.py +144 -0
- agent2win/registry.py +160 -0
- agent2win/screen.py +94 -0
- agent2win/server.py +1061 -0
- agent2win/services.py +124 -0
- agent2win/system_power.py +142 -0
- agent2win/tray.py +111 -0
- agent2win/tunnel.py +207 -0
- agent2win/virtual_desktop.py +232 -0
- agent2win/window_manager.py +313 -0
- agent2win-1.0.0.dist-info/METADATA +204 -0
- agent2win-1.0.0.dist-info/RECORD +29 -0
- agent2win-1.0.0.dist-info/WHEEL +5 -0
- agent2win-1.0.0.dist-info/entry_points.txt +2 -0
- agent2win-1.0.0.dist-info/licenses/LICENSE +21 -0
- agent2win-1.0.0.dist-info/top_level.txt +1 -0
agent2win/__init__.py
ADDED
agent2win/audio.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Windows Arena AI — Audio Control
|
|
3
|
+
Control system volume, mute, and audio devices.
|
|
4
|
+
"""
|
|
5
|
+
import subprocess
|
|
6
|
+
import ctypes
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from .config import Settings
|
|
9
|
+
from .logger import audit_log
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
|
|
13
|
+
from comtypes import CLSCTX_ALL, CoCreateInstance
|
|
14
|
+
from ctypes import POINTER
|
|
15
|
+
HAS_PYCAW = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
HAS_PYCAW = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AudioController:
|
|
21
|
+
"""System audio control."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, settings: Settings, logger):
|
|
24
|
+
self.settings = settings
|
|
25
|
+
self.logger = logger
|
|
26
|
+
self._volume_interface = None
|
|
27
|
+
|
|
28
|
+
def _get_volume_interface(self):
|
|
29
|
+
if self._volume_interface is None and HAS_PYCAW:
|
|
30
|
+
try:
|
|
31
|
+
devices = AudioUtilities.GetSpeakers()
|
|
32
|
+
interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
|
|
33
|
+
self._volume_interface = interface.QueryInterface(POINTER(IAudioEndpointVolume))
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
return self._volume_interface
|
|
37
|
+
|
|
38
|
+
async def get_volume(self) -> dict:
|
|
39
|
+
"""Get current system volume (0-100)."""
|
|
40
|
+
audit_log(self.settings, "get_volume", {})
|
|
41
|
+
if HAS_PYCAW:
|
|
42
|
+
try:
|
|
43
|
+
vol = self._get_volume_interface()
|
|
44
|
+
if vol:
|
|
45
|
+
level = vol.GetMasterVolumeLevelScalar()
|
|
46
|
+
muted = vol.GetMute()
|
|
47
|
+
return {"success": True, "volume": round(level * 100), "muted": bool(muted), "level_scalar": round(level, 3)}
|
|
48
|
+
except Exception as e:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# Fallback: nircmd or PowerShell
|
|
52
|
+
try:
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
["powershell", "-command",
|
|
55
|
+
"Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('')"],
|
|
56
|
+
capture_output=True, text=True, timeout=5,
|
|
57
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
58
|
+
)
|
|
59
|
+
return {"success": True, "note": "Install pycaw for precise volume control: pip install pycaw"}
|
|
60
|
+
except Exception:
|
|
61
|
+
return {"success": False, "error": "Install pycaw: pip install pycaw"}
|
|
62
|
+
|
|
63
|
+
async def set_volume(self, level: int) -> dict:
|
|
64
|
+
"""Set system volume (0-100)."""
|
|
65
|
+
audit_log(self.settings, "set_volume", {"level": level})
|
|
66
|
+
level = max(0, min(100, level))
|
|
67
|
+
|
|
68
|
+
if HAS_PYCAW:
|
|
69
|
+
try:
|
|
70
|
+
vol = self._get_volume_interface()
|
|
71
|
+
if vol:
|
|
72
|
+
vol.SetMasterVolumeLevelScalar(level / 100.0, None)
|
|
73
|
+
return {"success": True, "volume": level}
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
# Fallback: nircmd
|
|
78
|
+
try:
|
|
79
|
+
vol_val = int(level * 65535 / 100)
|
|
80
|
+
subprocess.run(
|
|
81
|
+
["nircmd", "setsysvolume", str(vol_val)],
|
|
82
|
+
capture_output=True, timeout=5,
|
|
83
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
84
|
+
)
|
|
85
|
+
return {"success": True, "volume": level, "method": "nircmd"}
|
|
86
|
+
except Exception:
|
|
87
|
+
return {"success": False, "error": "Install pycaw: pip install pycaw"}
|
|
88
|
+
|
|
89
|
+
async def mute(self) -> dict:
|
|
90
|
+
"""Mute system audio."""
|
|
91
|
+
audit_log(self.settings, "mute", {})
|
|
92
|
+
if HAS_PYCAW:
|
|
93
|
+
try:
|
|
94
|
+
vol = self._get_volume_interface()
|
|
95
|
+
if vol:
|
|
96
|
+
vol.SetMute(True, None)
|
|
97
|
+
return {"success": True, "muted": True}
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
return {"success": False, "error": "Install pycaw: pip install pycaw"}
|
|
101
|
+
|
|
102
|
+
async def unmute(self) -> dict:
|
|
103
|
+
"""Unmute system audio."""
|
|
104
|
+
audit_log(self.settings, "unmute", {})
|
|
105
|
+
if HAS_PYCAW:
|
|
106
|
+
try:
|
|
107
|
+
vol = self._get_volume_interface()
|
|
108
|
+
if vol:
|
|
109
|
+
vol.SetMute(False, None)
|
|
110
|
+
return {"success": True, "muted": False}
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
return {"success": False, "error": "Install pycaw: pip install pycaw"}
|
|
114
|
+
|
|
115
|
+
async def toggle_mute(self) -> dict:
|
|
116
|
+
"""Toggle mute state."""
|
|
117
|
+
if HAS_PYCAW:
|
|
118
|
+
try:
|
|
119
|
+
vol = self._get_volume_interface()
|
|
120
|
+
if vol:
|
|
121
|
+
current = vol.GetMute()
|
|
122
|
+
vol.SetMute(not current, None)
|
|
123
|
+
return {"success": True, "muted": not bool(current)}
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
return {"success": False, "error": "Install pycaw: pip install pycaw"}
|
|
127
|
+
|
|
128
|
+
async def volume_up(self, step: int = 5) -> dict:
|
|
129
|
+
"""Increase volume by step."""
|
|
130
|
+
current = await self.get_volume()
|
|
131
|
+
if current.get("success"):
|
|
132
|
+
new_level = min(100, current["volume"] + step)
|
|
133
|
+
return await self.set_volume(new_level)
|
|
134
|
+
return current
|
|
135
|
+
|
|
136
|
+
async def volume_down(self, step: int = 5) -> dict:
|
|
137
|
+
"""Decrease volume by step."""
|
|
138
|
+
current = await self.get_volume()
|
|
139
|
+
if current.get("success"):
|
|
140
|
+
new_level = max(0, current["volume"] - step)
|
|
141
|
+
return await self.set_volume(new_level)
|
|
142
|
+
return current
|
|
143
|
+
|
|
144
|
+
async def list_devices(self) -> dict:
|
|
145
|
+
"""List audio devices."""
|
|
146
|
+
audit_log(self.settings, "list_audio_devices", {})
|
|
147
|
+
if HAS_PYCAW:
|
|
148
|
+
try:
|
|
149
|
+
devices = AudioUtilities.GetAllDevices()
|
|
150
|
+
result = []
|
|
151
|
+
for d in devices:
|
|
152
|
+
result.append({
|
|
153
|
+
"id": str(d.id) if d.id else "",
|
|
154
|
+
"name": str(d.FriendlyName) if d.FriendlyName else "",
|
|
155
|
+
"state": str(d.State) if d.State else "",
|
|
156
|
+
})
|
|
157
|
+
return {"success": True, "devices": result, "count": len(result)}
|
|
158
|
+
except Exception as e:
|
|
159
|
+
return {"success": False, "error": str(e)}
|
|
160
|
+
return {"success": False, "error": "Install pycaw: pip install pycaw"}
|
agent2win/cli.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent2win — CLI Entry Point for PyPI package
|
|
3
|
+
"""
|
|
4
|
+
import asyncio
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from .config import Settings
|
|
10
|
+
from .server import ArenaServer
|
|
11
|
+
from .tray import TrayApp
|
|
12
|
+
from .gui import SettingsGUI
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_args():
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
description="agent2win — Universal Bridge Between Web/Cloud AI Agents & Windows OS",
|
|
18
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
19
|
+
epilog="""
|
|
20
|
+
Examples:
|
|
21
|
+
agent2win Start server on port 7770
|
|
22
|
+
agent2win --port 8080 --key abc Custom port with API key
|
|
23
|
+
agent2win --unrestricted No approval prompts (⚠️ use carefully)
|
|
24
|
+
agent2win --settings Open settings GUI
|
|
25
|
+
agent2win --tunnel cloudflared Use Cloudflare tunnel
|
|
26
|
+
agent2win --tunnel ngrok Use ngrok tunnel
|
|
27
|
+
agent2win --no-tunnel Disable tunnels
|
|
28
|
+
""",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument("--port", type=int, help="Server port (default: 7770)")
|
|
31
|
+
parser.add_argument("--host", type=str, help="Bind address (default: 0.0.0.0)")
|
|
32
|
+
parser.add_argument("--key", type=str, help="API key for authentication")
|
|
33
|
+
parser.add_argument("--unrestricted", action="store_true", help="Enable unrestricted mode (no approval prompts)")
|
|
34
|
+
parser.add_argument("--no-tray", action="store_true", help="Don't show system tray icon")
|
|
35
|
+
parser.add_argument("--no-tunnel", action="store_true", help="Disable tunnel")
|
|
36
|
+
parser.add_argument("--tunnel", type=str, choices=["cloudflared", "ngrok"], help="Tunnel provider")
|
|
37
|
+
parser.add_argument("--settings", action="store_true", help="Open settings GUI and exit")
|
|
38
|
+
parser.add_argument("--config", type=str, help="Path to config file")
|
|
39
|
+
return parser.parse_args()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main():
|
|
43
|
+
args = parse_args()
|
|
44
|
+
|
|
45
|
+
# Load settings
|
|
46
|
+
settings = Settings.load()
|
|
47
|
+
|
|
48
|
+
# Apply CLI overrides
|
|
49
|
+
if args.port:
|
|
50
|
+
settings.port = args.port
|
|
51
|
+
if args.host:
|
|
52
|
+
settings.host = args.host
|
|
53
|
+
if args.key:
|
|
54
|
+
settings.api_key = args.key
|
|
55
|
+
if args.unrestricted:
|
|
56
|
+
settings.unrestricted_mode = True
|
|
57
|
+
if args.no_tunnel:
|
|
58
|
+
settings.tunnel_provider = "none"
|
|
59
|
+
if args.tunnel:
|
|
60
|
+
settings.tunnel_provider = args.tunnel
|
|
61
|
+
|
|
62
|
+
settings.save()
|
|
63
|
+
|
|
64
|
+
# Settings GUI mode
|
|
65
|
+
if args.settings:
|
|
66
|
+
gui = SettingsGUI(settings)
|
|
67
|
+
gui.show()
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# Print banner
|
|
71
|
+
print(r"""
|
|
72
|
+
╔═══════════════════════════════════════════════════════╗
|
|
73
|
+
║ 🚀 agent2win v1.0.0 ║
|
|
74
|
+
║ Bridge Web/Cloud AI (ChatGPT, Gemini, Grok) to Win ║
|
|
75
|
+
╠═══════════════════════════════════════════════════════╣
|
|
76
|
+
║ Server: http://0.0.0.0:{port:<5} ║
|
|
77
|
+
║ Mode: {mode:<20} ║
|
|
78
|
+
║ Tunnel: {tunnel:<20} ║
|
|
79
|
+
╚═══════════════════════════════════════════════════════╝
|
|
80
|
+
""".format(
|
|
81
|
+
port=settings.port,
|
|
82
|
+
mode="UNRESTRICTED ⚠️" if settings.unrestricted_mode else "SECURE 🔒",
|
|
83
|
+
tunnel=settings.tunnel_provider,
|
|
84
|
+
))
|
|
85
|
+
|
|
86
|
+
server = ArenaServer(settings)
|
|
87
|
+
|
|
88
|
+
# Start system tray
|
|
89
|
+
tray = None
|
|
90
|
+
if not args.no_tray:
|
|
91
|
+
try:
|
|
92
|
+
tray = TrayApp(settings, server.notifications, settings.port)
|
|
93
|
+
tray.start()
|
|
94
|
+
print(" ✅ System tray icon active")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f" ⚠️ System tray unavailable: {e}")
|
|
97
|
+
|
|
98
|
+
# Start server
|
|
99
|
+
print(f" 🚀 Starting server on port {settings.port}...")
|
|
100
|
+
print(f" 📖 Open http://localhost:{settings.port} for API docs")
|
|
101
|
+
print(f" ⏹️ Press Ctrl+C to stop\n")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
asyncio.run(server.start())
|
|
105
|
+
except KeyboardInterrupt:
|
|
106
|
+
print("\n 👋 Shutting down...")
|
|
107
|
+
finally:
|
|
108
|
+
if tray:
|
|
109
|
+
tray.stop()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
main()
|
agent2win/clipboard.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Windows Arena AI — Clipboard Manager
|
|
3
|
+
Read and write the system clipboard (text, images).
|
|
4
|
+
"""
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from .config import Settings
|
|
9
|
+
from .logger import audit_log
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import pyperclip
|
|
13
|
+
HAS_PYPERCLIP = True
|
|
14
|
+
except ImportError:
|
|
15
|
+
HAS_PYPERCLIP = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ClipboardManager:
|
|
19
|
+
"""System clipboard operations."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, settings: Settings, logger):
|
|
22
|
+
self.settings = settings
|
|
23
|
+
self.logger = logger
|
|
24
|
+
|
|
25
|
+
async def get_text(self) -> dict:
|
|
26
|
+
"""Get current clipboard text."""
|
|
27
|
+
audit_log(self.settings, "clipboard_get", {})
|
|
28
|
+
if HAS_PYPERCLIP:
|
|
29
|
+
try:
|
|
30
|
+
text = pyperclip.paste()
|
|
31
|
+
return {"success": True, "text": text, "length": len(text)}
|
|
32
|
+
except Exception as e:
|
|
33
|
+
return {"success": False, "error": str(e)}
|
|
34
|
+
|
|
35
|
+
# Fallback: PowerShell
|
|
36
|
+
try:
|
|
37
|
+
result = subprocess.run(
|
|
38
|
+
["powershell", "-command", "Get-Clipboard"],
|
|
39
|
+
capture_output=True, text=True, timeout=5,
|
|
40
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
41
|
+
)
|
|
42
|
+
return {"success": True, "text": result.stdout.strip(), "length": len(result.stdout.strip())}
|
|
43
|
+
except Exception as e:
|
|
44
|
+
return {"success": False, "error": str(e)}
|
|
45
|
+
|
|
46
|
+
async def set_text(self, text: str) -> dict:
|
|
47
|
+
"""Set clipboard text."""
|
|
48
|
+
audit_log(self.settings, "clipboard_set", {"length": len(text)})
|
|
49
|
+
if HAS_PYPERCLIP:
|
|
50
|
+
try:
|
|
51
|
+
pyperclip.copy(text)
|
|
52
|
+
return {"success": True, "text_length": len(text)}
|
|
53
|
+
except Exception as e:
|
|
54
|
+
return {"success": False, "error": str(e)}
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
subprocess.run(
|
|
58
|
+
["powershell", "-command", f'Set-Clipboard -Value @"\\n{text}\\n"@'],
|
|
59
|
+
capture_output=True, text=True, timeout=5,
|
|
60
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
61
|
+
)
|
|
62
|
+
return {"success": True, "text_length": len(text)}
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return {"success": False, "error": str(e)}
|
|
65
|
+
|
|
66
|
+
async def get_history(self) -> dict:
|
|
67
|
+
"""Get clipboard history (Windows 10+)."""
|
|
68
|
+
audit_log(self.settings, "clipboard_history", {})
|
|
69
|
+
try:
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
["powershell", "-command", "Get-Clipboard -TextFormatType Text"],
|
|
72
|
+
capture_output=True, text=True, timeout=5,
|
|
73
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
74
|
+
)
|
|
75
|
+
return {"success": True, "history": result.stdout.strip()}
|
|
76
|
+
except Exception as e:
|
|
77
|
+
return {"success": False, "error": str(e)}
|
|
78
|
+
|
|
79
|
+
async def clear(self) -> dict:
|
|
80
|
+
"""Clear the clipboard."""
|
|
81
|
+
audit_log(self.settings, "clipboard_clear", {})
|
|
82
|
+
try:
|
|
83
|
+
subprocess.run(
|
|
84
|
+
["powershell", "-command", "Clear-Clipboard"],
|
|
85
|
+
capture_output=True, text=True, timeout=5,
|
|
86
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
87
|
+
)
|
|
88
|
+
return {"success": True}
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return {"success": False, "error": str(e)}
|
agent2win/commands.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Windows Arena AI — Command Execution Engine
|
|
3
|
+
Runs shell commands with safety checks, timeouts, and approval flow.
|
|
4
|
+
"""
|
|
5
|
+
import subprocess
|
|
6
|
+
import shlex
|
|
7
|
+
import time
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from .config import Settings
|
|
11
|
+
from .logger import audit_log
|
|
12
|
+
|
|
13
|
+
class CommandEngine:
|
|
14
|
+
def __init__(self, settings: Settings, logger, approval_callback=None):
|
|
15
|
+
self.settings = settings
|
|
16
|
+
self.logger = logger
|
|
17
|
+
self.approval_callback = approval_callback # async fn(action, details) -> bool
|
|
18
|
+
|
|
19
|
+
def _is_blocked(self, cmd: str) -> bool:
|
|
20
|
+
cmd_lower = cmd.lower().strip()
|
|
21
|
+
for blocked in self.settings.blocked_commands:
|
|
22
|
+
if blocked.lower() in cmd_lower:
|
|
23
|
+
return True
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
def _needs_approval(self, cmd: str) -> bool:
|
|
27
|
+
if self.settings.unrestricted_mode:
|
|
28
|
+
return False
|
|
29
|
+
if not self.settings.require_approval:
|
|
30
|
+
return False
|
|
31
|
+
cmd_lower = cmd.lower().strip()
|
|
32
|
+
# Known safe read-only commands pass without approval
|
|
33
|
+
safe_prefixes = ("dir", "cd", "echo", "whoami", "hostname", "ipconfig",
|
|
34
|
+
"systeminfo", "tasklist", "where", "type", "tree",
|
|
35
|
+
"netstat", "ping", "tracert", "ver", "date", "time")
|
|
36
|
+
for safe in safe_prefixes:
|
|
37
|
+
if cmd_lower.startswith(safe):
|
|
38
|
+
return False
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
async def execute(self, cmd: str, cwd: Optional[str] = None, timeout: Optional[int] = None) -> dict:
|
|
42
|
+
"""
|
|
43
|
+
Execute a Windows command. Returns dict with:
|
|
44
|
+
success, stdout, stderr, returncode, duration_sec, approved
|
|
45
|
+
"""
|
|
46
|
+
if self._is_blocked(cmd):
|
|
47
|
+
audit_log(self.settings, "command_blocked", {"cmd": cmd}, approved=False)
|
|
48
|
+
return {"success": False, "error": f"Command blocked by security policy: {cmd}", "stdout": "", "stderr": "", "returncode": -1}
|
|
49
|
+
|
|
50
|
+
approved = True
|
|
51
|
+
if self._needs_approval(cmd):
|
|
52
|
+
if self.approval_callback:
|
|
53
|
+
approved = await self.approval_callback("command", {"cmd": cmd, "cwd": cwd})
|
|
54
|
+
else:
|
|
55
|
+
approved = False
|
|
56
|
+
if not approved:
|
|
57
|
+
audit_log(self.settings, "command_denied", {"cmd": cmd}, approved=False)
|
|
58
|
+
return {"success": False, "error": "Command denied by user", "stdout": "", "stderr": "", "returncode": -1}
|
|
59
|
+
|
|
60
|
+
audit_log(self.settings, "command_executed", {"cmd": cmd, "cwd": cwd}, approved=True)
|
|
61
|
+
self.logger.info(f"Executing: {cmd}")
|
|
62
|
+
|
|
63
|
+
effective_timeout = timeout or self.settings.max_command_timeout_sec
|
|
64
|
+
t0 = time.time()
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
proc = subprocess.run(
|
|
68
|
+
cmd,
|
|
69
|
+
shell=True,
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
cwd=cwd,
|
|
73
|
+
timeout=effective_timeout,
|
|
74
|
+
encoding="utf-8",
|
|
75
|
+
errors="replace",
|
|
76
|
+
)
|
|
77
|
+
duration = round(time.time() - t0, 3)
|
|
78
|
+
return {
|
|
79
|
+
"success": proc.returncode == 0,
|
|
80
|
+
"stdout": proc.stdout,
|
|
81
|
+
"stderr": proc.stderr,
|
|
82
|
+
"returncode": proc.returncode,
|
|
83
|
+
"duration_sec": duration,
|
|
84
|
+
"approved": approved,
|
|
85
|
+
}
|
|
86
|
+
except subprocess.TimeoutExpired:
|
|
87
|
+
return {"success": False, "error": f"Command timed out after {effective_timeout}s", "stdout": "", "stderr": "", "returncode": -1}
|
|
88
|
+
except Exception as e:
|
|
89
|
+
return {"success": False, "error": str(e), "stdout": "", "stderr": "", "returncode": -1}
|
agent2win/config.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent2win — Configuration & Settings Manager
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from dataclasses import dataclass, field, asdict
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
CONFIG_DIR = Path(os.environ.get("APPDATA", "~")) / "agent2win"
|
|
11
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Settings:
|
|
15
|
+
# Server
|
|
16
|
+
host: str = "0.0.0.0"
|
|
17
|
+
port: int = 7770
|
|
18
|
+
api_key: str = ""
|
|
19
|
+
auto_start: bool = False
|
|
20
|
+
|
|
21
|
+
# Security
|
|
22
|
+
unrestricted_mode: bool = False # True = no approval prompts
|
|
23
|
+
require_approval: bool = True # Show notification before actions
|
|
24
|
+
approval_timeout_sec: int = 60 # Auto-deny after timeout
|
|
25
|
+
allowed_commands: list = field(default_factory=lambda: ["dir", "cd", "type", "echo", "whoami", "hostname", "ipconfig", "systeminfo", "tasklist", "where"])
|
|
26
|
+
blocked_commands: list = field(default_factory=lambda: ["format", "del /s", "rd /s", "reg delete", "bcdedit"])
|
|
27
|
+
max_command_timeout_sec: int = 30
|
|
28
|
+
|
|
29
|
+
# Screen
|
|
30
|
+
screen_capture_fps: int = 2 # Frames per second for live view
|
|
31
|
+
screen_quality: int = 60 # JPEG quality 1-100
|
|
32
|
+
screen_scale: float = 0.75 # Downscale factor
|
|
33
|
+
|
|
34
|
+
# Tunnel
|
|
35
|
+
tunnel_provider: str = "cloudflared" # cloudflared | ngrok | none
|
|
36
|
+
tunnel_custom_url: str = "" # If you have your own tunnel
|
|
37
|
+
|
|
38
|
+
# Logging
|
|
39
|
+
log_file: str = str(CONFIG_DIR / "agent2win.log")
|
|
40
|
+
log_level: str = "INFO"
|
|
41
|
+
audit_log: str = str(CONFIG_DIR / "audit.jsonl")
|
|
42
|
+
|
|
43
|
+
def save(self):
|
|
44
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
CONFIG_FILE.write_text(json.dumps(asdict(self), indent=2, ensure_ascii=False), encoding="utf-8")
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def load(cls) -> "Settings":
|
|
49
|
+
if CONFIG_FILE.exists():
|
|
50
|
+
try:
|
|
51
|
+
data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
52
|
+
known = {f.name for f in cls.__dataclass_fields__.values()}
|
|
53
|
+
return cls(**{k: v for k, v in data.items() if k in known})
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
s = cls()
|
|
57
|
+
s.save()
|
|
58
|
+
return s
|
|
59
|
+
|
|
60
|
+
def update(self, **kwargs):
|
|
61
|
+
for k, v in kwargs.items():
|
|
62
|
+
if hasattr(self, k):
|
|
63
|
+
setattr(self, k, v)
|
|
64
|
+
self.save()
|