aegis-framework 0.1.0

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,97 @@
1
+ """
2
+ Aegis App - Main Application Class
3
+ High-level API for creating Aegis applications
4
+ """
5
+
6
+ import os
7
+ import json
8
+ from aegis.core.window import AegisWindow
9
+
10
+
11
+ class AegisApp:
12
+ """
13
+ Main entry point for Aegis applications
14
+
15
+ Usage:
16
+ app = AegisApp()
17
+ app.run()
18
+ """
19
+
20
+ def __init__(self, config_path=None):
21
+ """
22
+ Initialize Aegis application
23
+
24
+ Args:
25
+ config_path: Path to aegis.config.json (optional)
26
+ """
27
+ self.config = self._load_config(config_path)
28
+ self.window = None
29
+ self.project_dir = os.getcwd()
30
+
31
+ def _load_config(self, config_path=None):
32
+ """Load configuration from aegis.config.json"""
33
+ if config_path is None:
34
+ config_path = 'aegis.config.json'
35
+
36
+ if os.path.exists(config_path):
37
+ with open(config_path, 'r') as f:
38
+ return json.load(f)
39
+
40
+ # Default configuration
41
+ return {
42
+ 'name': 'Aegis App',
43
+ 'title': 'Aegis App',
44
+ 'version': '1.0.0',
45
+ 'main': 'index.html',
46
+ 'preload': 'preload.js',
47
+ 'width': 1200,
48
+ 'height': 800,
49
+ 'resizable': True,
50
+ 'frame': True,
51
+ 'devTools': True,
52
+ 'contextMenu': True
53
+ }
54
+
55
+ def create_window(self, **options):
56
+ """
57
+ Create a new window with optional overrides
58
+
59
+ Args:
60
+ **options: Override config options for this window
61
+ """
62
+ window_config = {**self.config, **options}
63
+ self.window = AegisWindow(window_config)
64
+
65
+ # Inject Aegis API
66
+ preload_path = os.path.join(self.project_dir, self.config.get('preload', 'preload.js'))
67
+ self.window.inject_aegis_api(preload_path)
68
+
69
+ return self.window
70
+
71
+ def run(self):
72
+ """Start the application"""
73
+ if self.window is None:
74
+ self.create_window()
75
+
76
+ # Load main HTML file
77
+ main_file = os.path.join(self.project_dir, self.config.get('main', 'index.html'))
78
+ self.window.load_file(main_file)
79
+
80
+ # Start the main loop
81
+ self.window.run()
82
+
83
+
84
+ def run_app(config_path=None):
85
+ """
86
+ Convenience function to run an Aegis app
87
+
88
+ Args:
89
+ config_path: Optional path to config file
90
+ """
91
+ app = AegisApp(config_path)
92
+ app.run()
93
+
94
+
95
+ # Allow running as module: python -m aegis.core.aegis
96
+ if __name__ == '__main__':
97
+ run_app()
@@ -0,0 +1,270 @@
1
+ """
2
+ Aegis Bridge - Python ↔ JavaScript Communication
3
+ Handles IPC messaging between frontend and backend
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import threading
10
+ from typing import Callable, Dict, Any, Optional
11
+
12
+
13
+ class AegisBridge:
14
+ """
15
+ Manages communication between Python backend and JavaScript frontend
16
+ """
17
+
18
+ def __init__(self):
19
+ self._handlers: Dict[str, Callable] = {}
20
+ self._allowed_actions: set = set()
21
+ self._register_default_handlers()
22
+
23
+ def _register_default_handlers(self):
24
+ """Register built-in action handlers"""
25
+ self.register('read', self._handle_read)
26
+ self.register('write', self._handle_write)
27
+ self.register('run', self._handle_run)
28
+ self.register('exists', self._handle_exists)
29
+ self.register('mkdir', self._handle_mkdir)
30
+ self.register('remove', self._handle_remove)
31
+ self.register('copy', self._handle_copy)
32
+ self.register('move', self._handle_move)
33
+ self.register('env', self._handle_env)
34
+
35
+ def register(self, action: str, handler: Callable):
36
+ """
37
+ Register a custom action handler
38
+
39
+ Args:
40
+ action: Action name (e.g., 'myAction')
41
+ handler: Function that takes payload dict and returns result dict
42
+ """
43
+ self._handlers[action] = handler
44
+
45
+ def allow(self, *actions: str):
46
+ """
47
+ Set which actions are allowed (preload security)
48
+
49
+ Args:
50
+ *actions: Action names to allow
51
+ """
52
+ self._allowed_actions.update(actions)
53
+
54
+ def allow_all(self):
55
+ """Allow all registered actions"""
56
+ self._allowed_actions = set(self._handlers.keys())
57
+
58
+ def is_allowed(self, action: str) -> bool:
59
+ """Check if an action is allowed"""
60
+ # If no restrictions set, allow all
61
+ if not self._allowed_actions:
62
+ return True
63
+ return action in self._allowed_actions
64
+
65
+ def process(self, action: str, payload: Dict[str, Any]) -> Dict[str, Any]:
66
+ """
67
+ Process an action request
68
+
69
+ Args:
70
+ action: Action name
71
+ payload: Action payload
72
+
73
+ Returns:
74
+ Result dictionary
75
+ """
76
+ if not self.is_allowed(action):
77
+ raise PermissionError(f"Action '{action}' is not allowed by preload")
78
+
79
+ handler = self._handlers.get(action)
80
+ if not handler:
81
+ raise ValueError(f"Unknown action: {action}")
82
+
83
+ return handler(payload)
84
+
85
+ # ==================== Built-in Handlers ====================
86
+
87
+ def _handle_read(self, payload: Dict) -> Dict:
88
+ """Read file contents"""
89
+ path = payload.get('path', '.')
90
+ file = payload.get('file')
91
+ encoding = payload.get('encoding', 'utf-8')
92
+ binary = payload.get('binary', False)
93
+
94
+ if file:
95
+ full_path = os.path.join(path, file)
96
+ else:
97
+ full_path = path
98
+
99
+ if os.path.isdir(full_path):
100
+ # List directory
101
+ entries = []
102
+ for entry in os.listdir(full_path):
103
+ entry_path = os.path.join(full_path, entry)
104
+ stat = os.stat(entry_path)
105
+ entries.append({
106
+ 'name': entry,
107
+ 'isDirectory': os.path.isdir(entry_path),
108
+ 'isFile': os.path.isfile(entry_path),
109
+ 'size': stat.st_size,
110
+ 'modified': stat.st_mtime
111
+ })
112
+ return {'entries': entries, 'path': full_path}
113
+ else:
114
+ # Read file
115
+ mode = 'rb' if binary else 'r'
116
+ with open(full_path, mode, encoding=None if binary else encoding) as f:
117
+ content = f.read()
118
+ if binary:
119
+ import base64
120
+ content = base64.b64encode(content).decode('ascii')
121
+ return {'content': content, 'path': full_path}
122
+
123
+ def _handle_write(self, payload: Dict) -> Dict:
124
+ """Write file contents"""
125
+ path = payload.get('path', '.')
126
+ file = payload.get('file')
127
+ content = payload.get('content', '')
128
+ encoding = payload.get('encoding', 'utf-8')
129
+ append = payload.get('append', False)
130
+ binary = payload.get('binary', False)
131
+
132
+ if file:
133
+ full_path = os.path.join(path, file)
134
+ else:
135
+ full_path = path
136
+
137
+ # Create parent directories
138
+ parent = os.path.dirname(full_path)
139
+ if parent:
140
+ os.makedirs(parent, exist_ok=True)
141
+
142
+ mode = 'ab' if append and binary else 'a' if append else 'wb' if binary else 'w'
143
+
144
+ with open(full_path, mode, encoding=None if binary else encoding) as f:
145
+ if binary:
146
+ import base64
147
+ f.write(base64.b64decode(content))
148
+ else:
149
+ f.write(content)
150
+
151
+ return {'success': True, 'path': full_path}
152
+
153
+ def _handle_run(self, payload: Dict) -> Dict:
154
+ """Execute commands"""
155
+ if 'py' in payload:
156
+ # Execute Python code
157
+ code = payload['py']
158
+ local_vars = {}
159
+ try:
160
+ # Try eval first (for expressions)
161
+ result = eval(code)
162
+ return {'output': str(result), 'exitCode': 0}
163
+ except SyntaxError:
164
+ # Fall back to exec (for statements)
165
+ exec(code, {}, local_vars)
166
+ return {'output': str(local_vars), 'exitCode': 0}
167
+ except Exception as e:
168
+ return {'error': str(e), 'exitCode': 1}
169
+
170
+ elif 'sh' in payload:
171
+ # Execute shell command
172
+ cmd = payload['sh']
173
+ cwd = payload.get('cwd', os.getcwd())
174
+ timeout = payload.get('timeout', None)
175
+
176
+ try:
177
+ result = subprocess.run(
178
+ cmd,
179
+ shell=True,
180
+ capture_output=True,
181
+ text=True,
182
+ cwd=cwd,
183
+ timeout=timeout
184
+ )
185
+ return {
186
+ 'output': result.stdout,
187
+ 'error': result.stderr,
188
+ 'exitCode': result.returncode
189
+ }
190
+ except subprocess.TimeoutExpired:
191
+ return {'error': 'Command timed out', 'exitCode': -1}
192
+ except Exception as e:
193
+ return {'error': str(e), 'exitCode': -1}
194
+
195
+ return {'error': 'No py or sh command specified', 'exitCode': -1}
196
+
197
+ def _handle_exists(self, payload: Dict) -> Dict:
198
+ """Check if path exists"""
199
+ path = payload.get('path')
200
+ return {
201
+ 'exists': os.path.exists(path),
202
+ 'isFile': os.path.isfile(path),
203
+ 'isDirectory': os.path.isdir(path)
204
+ }
205
+
206
+ def _handle_mkdir(self, payload: Dict) -> Dict:
207
+ """Create directory"""
208
+ path = payload.get('path')
209
+ recursive = payload.get('recursive', True)
210
+
211
+ if recursive:
212
+ os.makedirs(path, exist_ok=True)
213
+ else:
214
+ os.mkdir(path)
215
+
216
+ return {'success': True, 'path': path}
217
+
218
+ def _handle_remove(self, payload: Dict) -> Dict:
219
+ """Remove file or directory"""
220
+ import shutil
221
+ path = payload.get('path')
222
+ recursive = payload.get('recursive', False)
223
+
224
+ if os.path.isdir(path):
225
+ if recursive:
226
+ shutil.rmtree(path)
227
+ else:
228
+ os.rmdir(path)
229
+ else:
230
+ os.remove(path)
231
+
232
+ return {'success': True}
233
+
234
+ def _handle_copy(self, payload: Dict) -> Dict:
235
+ """Copy file or directory"""
236
+ import shutil
237
+ src = payload.get('src')
238
+ dest = payload.get('dest')
239
+
240
+ if os.path.isdir(src):
241
+ shutil.copytree(src, dest)
242
+ else:
243
+ shutil.copy2(src, dest)
244
+
245
+ return {'success': True, 'dest': dest}
246
+
247
+ def _handle_move(self, payload: Dict) -> Dict:
248
+ """Move/rename file or directory"""
249
+ import shutil
250
+ src = payload.get('src')
251
+ dest = payload.get('dest')
252
+
253
+ shutil.move(src, dest)
254
+ return {'success': True, 'dest': dest}
255
+
256
+ def _handle_env(self, payload: Dict) -> Dict:
257
+ """Get/set environment variables"""
258
+ name = payload.get('name')
259
+ value = payload.get('value')
260
+
261
+ if value is not None:
262
+ # Set environment variable
263
+ os.environ[name] = value
264
+ return {'success': True}
265
+ elif name:
266
+ # Get single variable
267
+ return {'value': os.environ.get(name)}
268
+ else:
269
+ # Get all variables
270
+ return {'env': dict(os.environ)}
@@ -0,0 +1,160 @@
1
+ """
2
+ Aegis Preload Security System
3
+ Parses preload.js to determine which APIs should be exposed
4
+ """
5
+
6
+ import re
7
+ import json
8
+ import os
9
+ from typing import Set, Dict, Any, List
10
+
11
+
12
+ class PreloadManager:
13
+ """
14
+ Manages the preload security configuration
15
+
16
+ The preload.js file defines which Aegis APIs are exposed to the frontend.
17
+ This provides a security layer to prevent malicious code from accessing
18
+ sensitive system operations.
19
+ """
20
+
21
+ def __init__(self):
22
+ self.allowed_apis: Set[str] = set()
23
+ self.custom_handlers: Dict[str, str] = {}
24
+ self.config: Dict[str, Any] = {}
25
+
26
+ def load(self, preload_path: str) -> bool:
27
+ """
28
+ Load and parse a preload.js file
29
+
30
+ Args:
31
+ preload_path: Path to preload.js
32
+
33
+ Returns:
34
+ True if loaded successfully
35
+ """
36
+ if not os.path.exists(preload_path):
37
+ return False
38
+
39
+ with open(preload_path, 'r') as f:
40
+ content = f.read()
41
+
42
+ self._parse_preload(content)
43
+ return True
44
+
45
+ def _parse_preload(self, content: str):
46
+ """Parse preload.js content to extract configuration"""
47
+
48
+ # Look for Aegis.expose() calls
49
+ # Aegis.expose(['read', 'write', 'run'])
50
+ expose_pattern = r'Aegis\.expose\s*\(\s*\[(.*?)\]\s*\)'
51
+ matches = re.findall(expose_pattern, content, re.DOTALL)
52
+
53
+ for match in matches:
54
+ # Extract API names from the array
55
+ apis = re.findall(r'["\'](\w+(?:\.\w+)*)["\']', match)
56
+ self.allowed_apis.update(apis)
57
+
58
+ # Look for Aegis.exposeAll() - expose everything
59
+ if 'Aegis.exposeAll()' in content:
60
+ self.allowed_apis = {'*'}
61
+
62
+ # Look for Aegis.config() calls
63
+ config_pattern = r'Aegis\.config\s*\(\s*(\{.*?\})\s*\)'
64
+ config_matches = re.findall(config_pattern, content, re.DOTALL)
65
+
66
+ for match in config_matches:
67
+ try:
68
+ # Try to parse as JSON (basic JS object syntax)
69
+ # Replace single quotes with double quotes for JSON
70
+ json_str = match.replace("'", '"')
71
+ self.config.update(json.loads(json_str))
72
+ except json.JSONDecodeError:
73
+ pass
74
+
75
+ # Look for custom handler definitions
76
+ # Aegis.handle('myAction', async (data) => { ... })
77
+ handler_pattern = r'Aegis\.handle\s*\(\s*["\'](\w+)["\']\s*,'
78
+ handler_matches = re.findall(handler_pattern, content)
79
+
80
+ for handler_name in handler_matches:
81
+ self.custom_handlers[handler_name] = content
82
+
83
+ def is_api_allowed(self, api_name: str) -> bool:
84
+ """
85
+ Check if an API is allowed
86
+
87
+ Args:
88
+ api_name: Full API name (e.g., 'read', 'dialog.open')
89
+
90
+ Returns:
91
+ True if the API is allowed
92
+ """
93
+ # If exposeAll was used, everything is allowed
94
+ if '*' in self.allowed_apis:
95
+ return True
96
+
97
+ # Check exact match
98
+ if api_name in self.allowed_apis:
99
+ return True
100
+
101
+ # Check namespace match (e.g., 'dialog' allows 'dialog.open')
102
+ namespace = api_name.split('.')[0]
103
+ if namespace in self.allowed_apis:
104
+ return True
105
+
106
+ # If no APIs are explicitly allowed, allow all by default
107
+ # (for backwards compatibility and ease of use)
108
+ if not self.allowed_apis:
109
+ return True
110
+
111
+ return False
112
+
113
+ def get_allowed_list(self) -> List[str]:
114
+ """Get list of allowed APIs"""
115
+ return list(self.allowed_apis)
116
+
117
+ def generate_js_config(self) -> str:
118
+ """Generate JavaScript configuration for the frontend"""
119
+ if '*' in self.allowed_apis:
120
+ allowed_js = '["*"]'
121
+ else:
122
+ allowed_js = json.dumps(list(self.allowed_apis))
123
+
124
+ return f"""
125
+ // Aegis Preload Configuration (auto-generated)
126
+ window.__aegisAllowedAPIs = {allowed_js};
127
+ window.__aegisConfig = {json.dumps(self.config)};
128
+ """
129
+
130
+
131
+ def create_default_preload() -> str:
132
+ """Generate default preload.js content"""
133
+ return '''/**
134
+ * Aegis Preload Configuration
135
+ *
136
+ * This file controls which Aegis APIs are exposed to your frontend.
137
+ * For security, only expose the APIs you actually need.
138
+ */
139
+
140
+ // Expose specific APIs
141
+ Aegis.expose([
142
+ 'read', // File reading
143
+ 'write', // File writing
144
+ 'run', // Command execution
145
+ 'dialog', // Native dialogs (open, save, message)
146
+ 'app', // App control (quit, minimize, maximize)
147
+ 'exists', // Check file/directory existence
148
+ 'mkdir', // Create directories
149
+ 'env' // Environment variables
150
+ ]);
151
+
152
+ // Or expose everything (not recommended for production):
153
+ // Aegis.exposeAll();
154
+
155
+ // Optional: Configure Aegis behavior
156
+ Aegis.config({
157
+ allowRemoteContent: false,
158
+ enableDevTools: true
159
+ });
160
+ '''