synapse-sdk 1.0.0b2__py3-none-any.whl → 1.0.0b4__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.
Potentially problematic release.
This version of synapse-sdk might be problematic. Click here for more details.
- synapse_sdk/cli/__init__.py +1 -1
- synapse_sdk/cli/code_server.py +319 -73
- synapse_sdk/clients/agent/core.py +15 -0
- synapse_sdk/plugins/categories/export/actions/export.py +16 -1
- synapse_sdk/utils/encryption.py +158 -0
- {synapse_sdk-1.0.0b2.dist-info → synapse_sdk-1.0.0b4.dist-info}/METADATA +1 -1
- {synapse_sdk-1.0.0b2.dist-info → synapse_sdk-1.0.0b4.dist-info}/RECORD +11 -10
- {synapse_sdk-1.0.0b2.dist-info → synapse_sdk-1.0.0b4.dist-info}/WHEEL +0 -0
- {synapse_sdk-1.0.0b2.dist-info → synapse_sdk-1.0.0b4.dist-info}/entry_points.txt +0 -0
- {synapse_sdk-1.0.0b2.dist-info → synapse_sdk-1.0.0b4.dist-info}/licenses/LICENSE +0 -0
- {synapse_sdk-1.0.0b2.dist-info → synapse_sdk-1.0.0b4.dist-info}/top_level.txt +0 -0
synapse_sdk/cli/__init__.py
CHANGED
|
@@ -273,7 +273,7 @@ def cli(ctx, dev_tools):
|
|
|
273
273
|
message='Select an option:',
|
|
274
274
|
choices=[
|
|
275
275
|
('🌐 Run Dev Tools', 'devtools'),
|
|
276
|
-
('💻 Code-Server IDE', 'code_server'),
|
|
276
|
+
('💻 Open Code-Server IDE', 'code_server'),
|
|
277
277
|
('⚙️ Configuration', 'config'),
|
|
278
278
|
('🔌 Plugin Management', 'plugin'),
|
|
279
279
|
('🚪 Exit', 'exit'),
|
synapse_sdk/cli/code_server.py
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
"""Code-server integration for remote plugin development."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
3
7
|
from typing import Optional
|
|
8
|
+
from urllib.parse import quote
|
|
4
9
|
|
|
5
10
|
import click
|
|
11
|
+
import inquirer
|
|
12
|
+
import yaml
|
|
6
13
|
|
|
7
14
|
from synapse_sdk.cli.config import fetch_agents_from_backend, get_agent_config
|
|
8
15
|
from synapse_sdk.devtools.config import get_backend_config
|
|
16
|
+
from synapse_sdk.utils.encryption import encrypt_plugin, get_plugin_info, is_plugin_directory
|
|
9
17
|
|
|
10
18
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@click.option('--open-browser/--no-open-browser', default=True, help='Open in browser')
|
|
14
|
-
def code_server(agent: Optional[str], open_browser: bool):
|
|
15
|
-
"""Connect to web-based code-server on an agent for plugin development."""
|
|
19
|
+
def get_agent_client(agent: Optional[str] = None):
|
|
20
|
+
"""Helper function to get an agent client.
|
|
16
21
|
|
|
22
|
+
Args:
|
|
23
|
+
agent: Optional agent ID. If not provided, uses current agent or prompts user.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
tuple: (AgentClient instance, agent_id) or (None, None) if failed
|
|
27
|
+
"""
|
|
17
28
|
# Get current agent configuration
|
|
18
29
|
agent_config = get_agent_config()
|
|
19
30
|
backend_config = get_backend_config()
|
|
20
31
|
|
|
21
32
|
if not backend_config:
|
|
22
33
|
click.echo("❌ No backend configured. Run 'synapse config' first.")
|
|
23
|
-
return
|
|
34
|
+
return None, None
|
|
24
35
|
|
|
25
36
|
# If no agent specified, use current agent or let user choose
|
|
26
37
|
if not agent:
|
|
@@ -32,7 +43,7 @@ def code_server(agent: Optional[str], open_browser: bool):
|
|
|
32
43
|
agents, error = fetch_agents_from_backend()
|
|
33
44
|
if not agents:
|
|
34
45
|
click.echo('❌ No agents available. Check your backend configuration.')
|
|
35
|
-
return
|
|
46
|
+
return None, None
|
|
36
47
|
|
|
37
48
|
if len(agents) == 1:
|
|
38
49
|
# If only one agent, use it
|
|
@@ -52,14 +63,13 @@ def code_server(agent: Optional[str], open_browser: bool):
|
|
|
52
63
|
agent = agents[choice - 1]['id']
|
|
53
64
|
else:
|
|
54
65
|
click.echo('❌ Invalid selection')
|
|
55
|
-
return
|
|
66
|
+
return None, None
|
|
56
67
|
except (ValueError, EOFError, KeyboardInterrupt):
|
|
57
68
|
click.echo('\n❌ Cancelled')
|
|
58
|
-
return
|
|
69
|
+
return None, None
|
|
59
70
|
|
|
60
|
-
#
|
|
71
|
+
# Get agent details from backend
|
|
61
72
|
try:
|
|
62
|
-
# Get agent details from backend to get the agent URL
|
|
63
73
|
from synapse_sdk.clients.backend import BackendClient
|
|
64
74
|
|
|
65
75
|
backend_client = BackendClient(backend_config['host'], access_token=backend_config['token'])
|
|
@@ -70,100 +80,336 @@ def code_server(agent: Optional[str], open_browser: bool):
|
|
|
70
80
|
except Exception as e:
|
|
71
81
|
click.echo(f'❌ Failed to get agent information for: {agent}')
|
|
72
82
|
click.echo(f'Error: {e}')
|
|
73
|
-
return
|
|
83
|
+
return None, None
|
|
74
84
|
|
|
75
85
|
if not agent_info or not agent_info.get('url'):
|
|
76
86
|
click.echo(f'❌ Agent {agent} does not have a valid URL')
|
|
77
|
-
|
|
78
|
-
return
|
|
87
|
+
return None, None
|
|
79
88
|
|
|
80
89
|
# Get the agent token from local configuration
|
|
81
90
|
agent_token = agent_config.get('token')
|
|
82
91
|
if not agent_token:
|
|
83
92
|
click.echo('❌ No agent token found in configuration')
|
|
84
93
|
click.echo("Run 'synapse config' to configure the agent")
|
|
85
|
-
return
|
|
94
|
+
return None, None
|
|
86
95
|
|
|
87
96
|
# Create agent client
|
|
88
97
|
from synapse_sdk.clients.agent import AgentClient
|
|
89
98
|
|
|
90
99
|
client = AgentClient(base_url=agent_info['url'], agent_token=agent_token, user_token=backend_config['token'])
|
|
100
|
+
return client, agent
|
|
91
101
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
except AttributeError:
|
|
96
|
-
# Fallback to direct API call if method doesn't exist
|
|
97
|
-
response = client._get('code-server/info/')
|
|
98
|
-
info = response if isinstance(response, dict) else {}
|
|
99
|
-
except Exception as e:
|
|
100
|
-
# Handle other errors
|
|
101
|
-
click.echo(f'❌ Failed to get code-server info: {e}')
|
|
102
|
-
click.echo(f'Agent URL: {agent_info.get("url")}')
|
|
103
|
-
click.echo('\nNote: The agent might not have code-server endpoint implemented yet.')
|
|
104
|
-
return
|
|
102
|
+
except Exception as e:
|
|
103
|
+
click.echo(f'❌ Failed to connect to agent: {e}')
|
|
104
|
+
return None, None
|
|
105
105
|
|
|
106
|
-
if not info or not info.get('available', False):
|
|
107
|
-
message = info.get('message', 'Code-server is not available') if info else 'Failed to get code-server info'
|
|
108
|
-
click.echo(f'❌ {message}')
|
|
109
|
-
click.echo('\nTo enable code-server, reinstall the agent with code-server support.')
|
|
110
|
-
return
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
def detect_and_encrypt_plugin(workspace_path: str) -> Optional[dict]:
|
|
108
|
+
"""Detect and encrypt plugin code in the workspace.
|
|
114
109
|
|
|
115
|
-
|
|
110
|
+
Args:
|
|
111
|
+
workspace_path: Path to check for plugin
|
|
116
112
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
Returns:
|
|
114
|
+
dict: Encrypted plugin data or None if no plugin found
|
|
115
|
+
"""
|
|
116
|
+
plugin_path = Path(workspace_path)
|
|
117
|
+
|
|
118
|
+
if not is_plugin_directory(plugin_path):
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
plugin_info = get_plugin_info(plugin_path)
|
|
123
|
+
click.echo(f'🔍 Detected plugin: {plugin_info["name"]}')
|
|
124
|
+
|
|
125
|
+
if 'version' in plugin_info:
|
|
126
|
+
click.echo(f' Version: {plugin_info["version"]}')
|
|
127
|
+
if 'description' in plugin_info:
|
|
128
|
+
click.echo(f' Description: {plugin_info["description"]}')
|
|
129
|
+
|
|
130
|
+
click.echo('🔐 Encrypting plugin code...')
|
|
131
|
+
encrypted_package, password = encrypt_plugin(plugin_path)
|
|
132
|
+
|
|
133
|
+
# Add password to the package (in real implementation, this would be handled securely)
|
|
134
|
+
encrypted_package['password'] = password
|
|
135
|
+
|
|
136
|
+
click.echo('✅ Plugin code encrypted successfully')
|
|
137
|
+
return encrypted_package
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
click.echo(f'❌ Failed to encrypt plugin: {e}')
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def is_code_server_installed() -> bool:
|
|
145
|
+
"""Check if code-server is installed locally.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
bool: True if code-server is available in PATH
|
|
149
|
+
"""
|
|
150
|
+
return shutil.which('code-server') is not None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_code_server_port() -> int:
|
|
154
|
+
"""Get code-server port from config file.
|
|
125
155
|
|
|
126
|
-
|
|
156
|
+
Returns:
|
|
157
|
+
int: Port number from config, defaults to 8880 if not found
|
|
158
|
+
"""
|
|
159
|
+
config_path = Path.home() / '.config' / 'code-server' / 'config.yaml'
|
|
127
160
|
|
|
128
|
-
|
|
129
|
-
if
|
|
130
|
-
|
|
161
|
+
try:
|
|
162
|
+
if config_path.exists():
|
|
163
|
+
with open(config_path, 'r') as f:
|
|
164
|
+
config = yaml.safe_load(f)
|
|
165
|
+
|
|
166
|
+
# Parse bind-addr which can be in format "127.0.0.1:8880" or just ":8880"
|
|
167
|
+
bind_addr = config.get('bind-addr', '')
|
|
168
|
+
if ':' in bind_addr:
|
|
169
|
+
port_str = bind_addr.split(':')[-1]
|
|
170
|
+
try:
|
|
171
|
+
return int(port_str)
|
|
172
|
+
except ValueError:
|
|
173
|
+
pass
|
|
174
|
+
except Exception:
|
|
175
|
+
# If any error occurs reading config, fall back to default
|
|
176
|
+
pass
|
|
131
177
|
|
|
132
|
-
|
|
178
|
+
# Default port if config not found or invalid
|
|
179
|
+
return 8880
|
|
133
180
|
|
|
134
|
-
|
|
181
|
+
|
|
182
|
+
def launch_local_code_server(workspace_path: str, open_browser: bool = True) -> None:
|
|
183
|
+
"""Launch local code-server instance.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
workspace_path: Directory to open in code-server
|
|
187
|
+
open_browser: Whether to open browser automatically
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
# Get port from config
|
|
191
|
+
port = get_code_server_port()
|
|
192
|
+
|
|
193
|
+
# Create URL with folder query parameter
|
|
194
|
+
encoded_path = quote(workspace_path)
|
|
195
|
+
url_with_folder = f'http://localhost:{port}/?folder={encoded_path}'
|
|
196
|
+
|
|
197
|
+
# Basic code-server command - let code-server handle the workspace internally
|
|
198
|
+
cmd = ['code-server', workspace_path]
|
|
199
|
+
|
|
200
|
+
if not open_browser:
|
|
201
|
+
cmd.append('--disable-getting-started-override')
|
|
202
|
+
|
|
203
|
+
click.echo(f'🚀 Starting local code-server for workspace: {workspace_path}')
|
|
204
|
+
click.echo(f' URL: {url_with_folder}')
|
|
205
|
+
click.echo(' Press Ctrl+C to stop the server')
|
|
206
|
+
|
|
207
|
+
# Start code-server in background if we need to open browser
|
|
208
|
+
if open_browser:
|
|
209
|
+
# Start code-server in background
|
|
210
|
+
import threading
|
|
211
|
+
import time
|
|
212
|
+
|
|
213
|
+
def start_server():
|
|
214
|
+
subprocess.run(cmd)
|
|
215
|
+
|
|
216
|
+
server_thread = threading.Thread(target=start_server, daemon=True)
|
|
217
|
+
server_thread.start()
|
|
218
|
+
|
|
219
|
+
# Give server a moment to start
|
|
220
|
+
click.echo(' Waiting for server to start...')
|
|
221
|
+
time.sleep(3)
|
|
222
|
+
|
|
223
|
+
# Open browser with folder parameter
|
|
135
224
|
try:
|
|
136
|
-
|
|
137
|
-
result = subprocess.run(['xdg-open', info['url']], capture_output=True, text=True, timeout=2)
|
|
225
|
+
result = subprocess.run(['xdg-open', url_with_folder], capture_output=True, text=True, timeout=2)
|
|
138
226
|
if result.returncode != 0:
|
|
139
227
|
click.echo('⚠️ Could not open browser automatically (no display?)')
|
|
140
|
-
click.echo(f'👉 Please manually open: {
|
|
228
|
+
click.echo(f'👉 Please manually open: {url_with_folder}')
|
|
229
|
+
else:
|
|
230
|
+
click.echo('✅ Browser opened successfully')
|
|
141
231
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
142
|
-
# xdg-open not available or timed out
|
|
143
232
|
click.echo('⚠️ Could not open browser (headless environment)')
|
|
144
|
-
click.echo(f'👉 Please manually open: {
|
|
233
|
+
click.echo(f'👉 Please manually open: {url_with_folder}')
|
|
145
234
|
except Exception:
|
|
146
|
-
|
|
147
|
-
|
|
235
|
+
click.echo(f'👉 Please manually open: {url_with_folder}')
|
|
236
|
+
|
|
237
|
+
# Wait for the server thread (blocking)
|
|
238
|
+
try:
|
|
239
|
+
server_thread.join()
|
|
240
|
+
except KeyboardInterrupt:
|
|
241
|
+
click.echo('\n\n✅ Code-server stopped')
|
|
242
|
+
else:
|
|
243
|
+
# Start code-server normally (blocking)
|
|
244
|
+
subprocess.run(cmd)
|
|
245
|
+
|
|
246
|
+
except KeyboardInterrupt:
|
|
247
|
+
click.echo('\n\n✅ Code-server stopped')
|
|
248
|
+
except Exception as e:
|
|
249
|
+
click.echo(f'❌ Failed to start local code-server: {e}')
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def show_code_server_installation_help() -> None:
|
|
253
|
+
"""Show installation instructions for code-server."""
|
|
254
|
+
click.echo('\n❌ Code-server is not installed locally')
|
|
255
|
+
click.echo('\n📦 To install code-server, choose one of these options:')
|
|
256
|
+
click.echo('\n1. Install script (recommended):')
|
|
257
|
+
click.echo(' curl -fsSL https://code-server.dev/install.sh | sh')
|
|
258
|
+
click.echo('\n2. Using npm:')
|
|
259
|
+
click.echo(' npm install -g code-server')
|
|
260
|
+
click.echo('\n3. Using yarn:')
|
|
261
|
+
click.echo(' yarn global add code-server')
|
|
262
|
+
click.echo('\n4. Download from releases:')
|
|
263
|
+
click.echo(' https://github.com/coder/code-server/releases')
|
|
264
|
+
click.echo('\n📚 For more installation options, visit: https://coder.com/docs/code-server/latest/install')
|
|
265
|
+
|
|
148
266
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
click.echo('1. Open the URL in your browser')
|
|
152
|
-
click.echo('2. Enter the password if prompted')
|
|
153
|
-
click.echo('3. Start coding in the web-based VS Code!')
|
|
267
|
+
def run_agent_code_server(agent: Optional[str], workspace: str, open_browser: bool) -> None:
|
|
268
|
+
"""Run code-server through agent (existing functionality).
|
|
154
269
|
|
|
270
|
+
Args:
|
|
271
|
+
agent: Agent name or ID
|
|
272
|
+
workspace: Workspace directory path
|
|
273
|
+
open_browser: Whether to open browser automatically
|
|
274
|
+
"""
|
|
275
|
+
client, _ = get_agent_client(agent)
|
|
276
|
+
if not client:
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Check for plugin and show info if found
|
|
280
|
+
plugin_data = detect_and_encrypt_plugin(workspace)
|
|
281
|
+
if plugin_data:
|
|
282
|
+
click.echo('📦 Plugin detected and encrypted for secure transfer')
|
|
283
|
+
|
|
284
|
+
# Get code-server information
|
|
285
|
+
try:
|
|
286
|
+
info = client.get_code_server_info(workspace_path=workspace)
|
|
155
287
|
except Exception as e:
|
|
156
|
-
|
|
288
|
+
# Handle other errors
|
|
289
|
+
click.echo(f'❌ Failed to get code-server info: {e}')
|
|
290
|
+
click.echo('\nNote: The agent might not have code-server endpoint implemented yet.')
|
|
291
|
+
return
|
|
157
292
|
|
|
158
|
-
|
|
293
|
+
# Ensure info is a dictionary
|
|
294
|
+
if not isinstance(info, dict):
|
|
295
|
+
click.echo('❌ Invalid response from agent')
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
if not info.get('available', False):
|
|
299
|
+
message = info.get('message', 'Code-server is not available')
|
|
300
|
+
click.echo(f'❌ {message}')
|
|
301
|
+
click.echo('\nTo enable code-server, reinstall the agent with code-server support.')
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
# Display connection information
|
|
305
|
+
click.echo('\n✅ Code-Server is available!')
|
|
306
|
+
|
|
307
|
+
# Get the workspace path from response or use the requested one
|
|
308
|
+
actual_workspace = info.get('workspace', workspace)
|
|
309
|
+
|
|
310
|
+
# Show web browser access
|
|
311
|
+
click.echo('\n🌐 Web-based VS Code:')
|
|
312
|
+
url = info.get('url')
|
|
313
|
+
if not url:
|
|
314
|
+
click.echo('❌ No URL provided by agent')
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
click.echo(f' URL: {url}')
|
|
318
|
+
password = info.get('password')
|
|
319
|
+
if password:
|
|
320
|
+
click.echo(f' Password: {password}')
|
|
321
|
+
else:
|
|
322
|
+
click.echo(' Password: Not required (passwordless mode)')
|
|
323
|
+
|
|
324
|
+
# Show workspace information with better context
|
|
325
|
+
click.echo(f'\n📁 Agent Workspace: {actual_workspace}')
|
|
326
|
+
click.echo(f'📂 Local Project: {workspace}')
|
|
327
|
+
|
|
328
|
+
# Only show warning if the paths are drastically different and it's not the expected container path
|
|
329
|
+
if actual_workspace != workspace and not actual_workspace.startswith('/home/coder'):
|
|
330
|
+
click.echo(' ⚠️ Note: Agent workspace differs from local project path')
|
|
331
|
+
|
|
332
|
+
# Optionally open in browser
|
|
333
|
+
if open_browser and url:
|
|
334
|
+
click.echo('\nAttempting to open in browser...')
|
|
159
335
|
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
336
|
+
# Try to open browser, suppressing stderr to avoid xdg-open noise
|
|
337
|
+
try:
|
|
338
|
+
# Use subprocess to suppress xdg-open errors
|
|
339
|
+
result = subprocess.run(['xdg-open', url], capture_output=True, text=True, timeout=2)
|
|
340
|
+
if result.returncode != 0:
|
|
341
|
+
click.echo('⚠️ Could not open browser automatically (no display?)')
|
|
342
|
+
click.echo(f'👉 Please manually open: {url}')
|
|
343
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
344
|
+
# xdg-open not available or timed out
|
|
345
|
+
click.echo('⚠️ Could not open browser (headless environment)')
|
|
346
|
+
click.echo(f'👉 Please manually open: {url}')
|
|
347
|
+
except Exception:
|
|
348
|
+
# Fallback for other errors
|
|
349
|
+
click.echo(f'👉 Please manually open: {url}')
|
|
350
|
+
|
|
351
|
+
# Show additional instructions
|
|
352
|
+
click.echo('\n📝 Quick Start:')
|
|
353
|
+
click.echo('1. Open the URL in your browser')
|
|
354
|
+
click.echo('2. Enter the password if prompted')
|
|
355
|
+
click.echo('3. Start coding in the web-based VS Code!')
|
|
356
|
+
|
|
357
|
+
# Add note about workspace synchronization
|
|
358
|
+
if actual_workspace.startswith('/home/coder'):
|
|
359
|
+
click.echo("\n💡 Note: Your local project files will be available in the agent's workspace.")
|
|
360
|
+
click.echo(' Changes made in code-server will be reflected in your local project.')
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@click.command()
|
|
364
|
+
@click.option('--agent', help='Agent name or ID')
|
|
365
|
+
@click.option('--open-browser/--no-open-browser', default=True, help='Open in browser')
|
|
366
|
+
@click.option('--workspace', help='Workspace directory path (defaults to current directory)')
|
|
367
|
+
def code_server(agent: Optional[str], open_browser: bool, workspace: Optional[str]):
|
|
368
|
+
"""Open code-server either through agent or locally."""
|
|
369
|
+
|
|
370
|
+
# Get current working directory if workspace not specified
|
|
371
|
+
if not workspace:
|
|
372
|
+
workspace = os.getcwd()
|
|
373
|
+
|
|
374
|
+
click.echo(f'Using workspace: {workspace}')
|
|
375
|
+
|
|
376
|
+
# Check if local code-server is available
|
|
377
|
+
local_available = is_code_server_installed()
|
|
378
|
+
|
|
379
|
+
# Create menu options based on availability
|
|
380
|
+
choices = []
|
|
164
381
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
382
|
+
# Always offer agent option
|
|
383
|
+
choices.append(('Open code-server through agent', 'agent'))
|
|
384
|
+
|
|
385
|
+
# Add local option if available
|
|
386
|
+
if local_available:
|
|
387
|
+
choices.append(('Open local code-server', 'local'))
|
|
388
|
+
else:
|
|
389
|
+
choices.append(('Install local code-server (not installed)', 'install'))
|
|
390
|
+
|
|
391
|
+
choices.append(('Cancel', 'cancel'))
|
|
392
|
+
|
|
393
|
+
# Show selection menu
|
|
394
|
+
questions = [inquirer.List('option', message='How would you like to open code-server?', choices=choices)]
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
answers = inquirer.prompt(questions)
|
|
398
|
+
if not answers or answers['option'] == 'cancel':
|
|
399
|
+
click.echo('Cancelled')
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
if answers['option'] == 'agent':
|
|
403
|
+
click.echo('\n🤖 Opening code-server through agent...')
|
|
404
|
+
run_agent_code_server(agent, workspace, open_browser)
|
|
405
|
+
|
|
406
|
+
elif answers['option'] == 'local':
|
|
407
|
+
click.echo('\n💻 Starting local code-server...')
|
|
408
|
+
launch_local_code_server(workspace, open_browser)
|
|
409
|
+
|
|
410
|
+
elif answers['option'] == 'install':
|
|
411
|
+
show_code_server_installation_help()
|
|
412
|
+
|
|
413
|
+
except (KeyboardInterrupt, EOFError):
|
|
414
|
+
click.echo('\n\nCancelled')
|
|
415
|
+
return
|
|
@@ -9,3 +9,18 @@ class CoreClientMixin(BaseClient):
|
|
|
9
9
|
def get_metrics(self, panel):
|
|
10
10
|
path = f'metrics/{panel}/'
|
|
11
11
|
return self._get(path)
|
|
12
|
+
|
|
13
|
+
def get_code_server_info(self, workspace_path=None):
|
|
14
|
+
"""Get code-server connection information from the agent.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
workspace_path: Optional path to set as the workspace directory
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
dict: Code-server connection information
|
|
21
|
+
"""
|
|
22
|
+
path = 'code-server/info/'
|
|
23
|
+
params = {}
|
|
24
|
+
if workspace_path:
|
|
25
|
+
params['workspace'] = workspace_path
|
|
26
|
+
return self._get(path, params=params)
|
|
@@ -40,7 +40,7 @@ class ExportRun(Run):
|
|
|
40
40
|
"""Log export file information.
|
|
41
41
|
|
|
42
42
|
Args:
|
|
43
|
-
log_type (str): The type of log ('export_data_file' or 'export_original_file').
|
|
43
|
+
log_type (str): The type of log ('export_data_file' or 'export_original_file' or 'etc').
|
|
44
44
|
target_id (int): The ID of the data file.
|
|
45
45
|
data_file_info (dict): The JSON info of the data file.
|
|
46
46
|
status (ExportStatus): The status of the data file.
|
|
@@ -88,6 +88,16 @@ class ExportRun(Run):
|
|
|
88
88
|
"""Log export origin data file."""
|
|
89
89
|
self.log_file('export_original_file', target_id, data_file_info, status, error)
|
|
90
90
|
|
|
91
|
+
def export_log_etc_file(
|
|
92
|
+
self,
|
|
93
|
+
target_id: int,
|
|
94
|
+
data_file_info: dict,
|
|
95
|
+
status: ExportStatus = ExportStatus.STAND_BY,
|
|
96
|
+
error: str | None = None,
|
|
97
|
+
):
|
|
98
|
+
"""Log export etc file."""
|
|
99
|
+
self.log_file('etc', target_id, data_file_info, status, error)
|
|
100
|
+
|
|
91
101
|
|
|
92
102
|
class ExportTargetHandler(ABC):
|
|
93
103
|
"""
|
|
@@ -291,6 +301,11 @@ class ExportAction(Action):
|
|
|
291
301
|
'failed': 0,
|
|
292
302
|
'success': 0,
|
|
293
303
|
},
|
|
304
|
+
'etc': {
|
|
305
|
+
'stand_by': 0,
|
|
306
|
+
'failed': 0,
|
|
307
|
+
'success': 0,
|
|
308
|
+
},
|
|
294
309
|
}
|
|
295
310
|
|
|
296
311
|
def get_filtered_results(self, filters, handler):
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Encryption utilities for plugin code security."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
import zipfile
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from cryptography.fernet import Fernet
|
|
11
|
+
from cryptography.hazmat.primitives import hashes
|
|
12
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_key(password: str, salt: bytes) -> bytes:
|
|
16
|
+
"""Generate encryption key from password.
|
|
17
|
+
|
|
18
|
+
* Argument iterations should be at least 500,000.
|
|
19
|
+
"""
|
|
20
|
+
kdf = PBKDF2HMAC(
|
|
21
|
+
algorithm=hashes.SHA256(),
|
|
22
|
+
length=32,
|
|
23
|
+
salt=salt,
|
|
24
|
+
iterations=700000,
|
|
25
|
+
)
|
|
26
|
+
return base64.urlsafe_b64encode(kdf.derive(password.encode()))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def encrypt_data(data: bytes, password: str) -> Dict:
|
|
30
|
+
"""Encrypt data with password."""
|
|
31
|
+
salt = os.urandom(16)
|
|
32
|
+
key = generate_key(password, salt)
|
|
33
|
+
fernet = Fernet(key)
|
|
34
|
+
encrypted_data = fernet.encrypt(data)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
'encrypted_data': base64.b64encode(encrypted_data).decode(),
|
|
38
|
+
'salt': base64.b64encode(salt).decode(),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def decrypt_data(encrypted_package: Dict, password: str) -> bytes:
|
|
43
|
+
"""Decrypt data with password."""
|
|
44
|
+
salt = base64.b64decode(encrypted_package['salt'])
|
|
45
|
+
encrypted_data = base64.b64decode(encrypted_package['encrypted_data'])
|
|
46
|
+
|
|
47
|
+
key = generate_key(password, salt)
|
|
48
|
+
fernet = Fernet(key)
|
|
49
|
+
return fernet.decrypt(encrypted_data)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_plugin_directory(path: Path) -> bool:
|
|
53
|
+
"""Check if directory contains a Synapse plugin."""
|
|
54
|
+
config_file = path / 'config.yaml'
|
|
55
|
+
plugin_dir = path / 'plugin'
|
|
56
|
+
|
|
57
|
+
return config_file.exists() and plugin_dir.exists() and plugin_dir.is_dir()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_plugin_files(plugin_path: Path) -> List[Tuple[Path, str]]:
|
|
61
|
+
"""Get all plugin files with their relative paths."""
|
|
62
|
+
plugin_files = []
|
|
63
|
+
|
|
64
|
+
# Essential plugin files
|
|
65
|
+
essential_patterns = [
|
|
66
|
+
'config.yaml',
|
|
67
|
+
'requirements.txt',
|
|
68
|
+
'README.md',
|
|
69
|
+
'pyproject.toml',
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
for pattern in essential_patterns:
|
|
73
|
+
file_path = plugin_path / pattern
|
|
74
|
+
if file_path.exists():
|
|
75
|
+
plugin_files.append((file_path, pattern))
|
|
76
|
+
|
|
77
|
+
# Plugin source code
|
|
78
|
+
plugin_dir = plugin_path / 'plugin'
|
|
79
|
+
if plugin_dir.exists():
|
|
80
|
+
for file_path in plugin_dir.rglob('*'):
|
|
81
|
+
if file_path.is_file() and not file_path.name.startswith('.'):
|
|
82
|
+
relative_path = f'plugin/{file_path.relative_to(plugin_dir)}'
|
|
83
|
+
plugin_files.append((file_path, relative_path))
|
|
84
|
+
|
|
85
|
+
# Additional common directories
|
|
86
|
+
for additional_dir in ['tests', 'docs', 'data']:
|
|
87
|
+
dir_path = plugin_path / additional_dir
|
|
88
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
89
|
+
for file_path in dir_path.rglob('*'):
|
|
90
|
+
if file_path.is_file() and not file_path.name.startswith('.'):
|
|
91
|
+
relative_path = f'{additional_dir}/{file_path.relative_to(dir_path)}'
|
|
92
|
+
plugin_files.append((file_path, relative_path))
|
|
93
|
+
|
|
94
|
+
return plugin_files
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def create_plugin_archive(plugin_path: Path) -> bytes:
|
|
98
|
+
"""Create a zip archive of the plugin."""
|
|
99
|
+
plugin_files = get_plugin_files(plugin_path)
|
|
100
|
+
|
|
101
|
+
archive_buffer = BytesIO()
|
|
102
|
+
with zipfile.ZipFile(archive_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
|
103
|
+
for file_path, archive_path in plugin_files:
|
|
104
|
+
zip_file.write(file_path, archive_path)
|
|
105
|
+
|
|
106
|
+
return archive_buffer.getvalue()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def encrypt_plugin(plugin_path: Path, password: Optional[str] = None) -> Dict:
|
|
110
|
+
"""Encrypt a plugin directory."""
|
|
111
|
+
if not is_plugin_directory(plugin_path):
|
|
112
|
+
raise ValueError(f'Directory {plugin_path} is not a valid plugin directory')
|
|
113
|
+
|
|
114
|
+
# Generate password if not provided
|
|
115
|
+
if password is None:
|
|
116
|
+
password = base64.urlsafe_b64encode(os.urandom(32)).decode()
|
|
117
|
+
|
|
118
|
+
# Create plugin archive
|
|
119
|
+
archive_data = create_plugin_archive(plugin_path)
|
|
120
|
+
|
|
121
|
+
# Encrypt the archive
|
|
122
|
+
encrypted_package = encrypt_data(archive_data, password)
|
|
123
|
+
|
|
124
|
+
# Add metadata
|
|
125
|
+
encrypted_package.update({
|
|
126
|
+
'plugin_name': plugin_path.name,
|
|
127
|
+
'plugin_path': str(plugin_path),
|
|
128
|
+
'encryption_method': 'fernet',
|
|
129
|
+
'archive_format': 'zip',
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
return encrypted_package, password
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_plugin_info(plugin_path: Path) -> Optional[Dict]:
|
|
136
|
+
"""Get basic plugin information."""
|
|
137
|
+
if not is_plugin_directory(plugin_path):
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
info = {'name': plugin_path.name, 'path': str(plugin_path), 'is_plugin': True}
|
|
141
|
+
|
|
142
|
+
# Try to read config.yaml for additional info
|
|
143
|
+
config_file = plugin_path / 'config.yaml'
|
|
144
|
+
if config_file.exists():
|
|
145
|
+
try:
|
|
146
|
+
import yaml
|
|
147
|
+
|
|
148
|
+
with open(config_file, 'r', encoding='utf-8') as f:
|
|
149
|
+
config = yaml.safe_load(f)
|
|
150
|
+
info.update({
|
|
151
|
+
'version': config.get('version', 'unknown'),
|
|
152
|
+
'description': config.get('description', ''),
|
|
153
|
+
'category': config.get('category', 'unknown'),
|
|
154
|
+
})
|
|
155
|
+
except Exception:
|
|
156
|
+
pass # Continue without config details if parsing fails
|
|
157
|
+
|
|
158
|
+
return info
|
|
@@ -6,8 +6,8 @@ synapse_sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
6
6
|
synapse_sdk/i18n.py,sha256=VXMR-Zm_1hTAg9iPk3YZNNq-T1Bhx1J2fEtRT6kyYbg,766
|
|
7
7
|
synapse_sdk/loggers.py,sha256=xK48h3ZaDDZLaF-qsdnv1-6-4vw_cYlgpSCKHYUQw1g,6549
|
|
8
8
|
synapse_sdk/types.py,sha256=khzn8KpgxFdn1SrpbcuX84m_Md1Mz_HIoUoPq8uok40,698
|
|
9
|
-
synapse_sdk/cli/__init__.py,sha256=
|
|
10
|
-
synapse_sdk/cli/code_server.py,sha256=
|
|
9
|
+
synapse_sdk/cli/__init__.py,sha256=64Qaxak5rwhEKUj5xLkdg1vtBgyl4Do1Z97eNiqNqEE,11668
|
|
10
|
+
synapse_sdk/cli/code_server.py,sha256=0AnyZSrHxfH7YU02PXPjcvekH-R1BRRrIA0NbA8Ckd8,15421
|
|
11
11
|
synapse_sdk/cli/config.py,sha256=2WVKeAdcDeiwRb4JnNEOeZoAVlTHrY2jx4NAUiAZO2c,16156
|
|
12
12
|
synapse_sdk/cli/devtools.py,sha256=hi06utdLllptlUy3HhPfmfRqjc9bdiVGezY0U5s_0pY,2617
|
|
13
13
|
synapse_sdk/cli/alias/__init__.py,sha256=jDy8N_KupVy7n_jKKWhjQOj76-mR-uoVvMoyzObUkuI,405
|
|
@@ -28,7 +28,7 @@ synapse_sdk/clients/base.py,sha256=2-aCbWx0LcFGXXgd9nkWtaaSroRJybjRAHAT4qCjhv0,1
|
|
|
28
28
|
synapse_sdk/clients/exceptions.py,sha256=ylv7x10eOp4aA3a48jwonnvqvkiYwzJYXjkVkRTAjwk,220
|
|
29
29
|
synapse_sdk/clients/utils.py,sha256=8pPJTdzHiRPSbZMoQYHAgR2BAMO6u_R_jMV6a2p34iQ,392
|
|
30
30
|
synapse_sdk/clients/agent/__init__.py,sha256=FqYbtzMJdzRfuU2SA-Yxdc0JKmVP1wxH6OlUNmB4lH8,2230
|
|
31
|
-
synapse_sdk/clients/agent/core.py,sha256=
|
|
31
|
+
synapse_sdk/clients/agent/core.py,sha256=aeMSzf8BF7LjVcmHaL8zC7ofBZUff8kIeqkW1xUJ6Sk,745
|
|
32
32
|
synapse_sdk/clients/agent/ray.py,sha256=1EDl-bMN2CvKl07-qMidSWNOGpvIvzcWl7jDBCza65o,3248
|
|
33
33
|
synapse_sdk/clients/agent/service.py,sha256=s7KuPK_DB1nr2VHrigttV1WyFonaGHNrPvU8loRxHcE,478
|
|
34
34
|
synapse_sdk/clients/backend/__init__.py,sha256=9FzjQn0ljRhtdaoG3n38Mdgte7GFwIh4OtEmoqVg2_E,2098
|
|
@@ -113,7 +113,7 @@ synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py,sh
|
|
|
113
113
|
synapse_sdk/plugins/categories/export/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
114
114
|
synapse_sdk/plugins/categories/export/enums.py,sha256=gtyngvQ1DKkos9iKGcbecwTVQQ6sDwbrBPSGPNb5Am0,127
|
|
115
115
|
synapse_sdk/plugins/categories/export/actions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
116
|
-
synapse_sdk/plugins/categories/export/actions/export.py,sha256=
|
|
116
|
+
synapse_sdk/plugins/categories/export/actions/export.py,sha256=7OCU3kwkYqYnelG6vfZ4ClyyMj_FHT-WQoUmgcfH080,12012
|
|
117
117
|
synapse_sdk/plugins/categories/export/templates/config.yaml,sha256=N7YmnFROb3s3M35SA9nmabyzoSb5O2t2TRPicwFNN2o,56
|
|
118
118
|
synapse_sdk/plugins/categories/export/templates/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
119
119
|
synapse_sdk/plugins/categories/export/templates/plugin/export.py,sha256=GDb6Ucodsr5aBPMU4alr68-DyFoLR5TyhC_MCaJrkF0,6411
|
|
@@ -180,6 +180,7 @@ synapse_sdk/shared/enums.py,sha256=5uy4HGKtGCAvrBMuVSzwloJ6f41sOk0ty__605zF8hg,3
|
|
|
180
180
|
synapse_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
181
181
|
synapse_sdk/utils/dataset.py,sha256=zWTzFmv589izFr62BDuApi3r5FpTsdm-5AmriC0AEdM,1865
|
|
182
182
|
synapse_sdk/utils/debug.py,sha256=F7JlUwYjTFZAMRbBqKm6hxOIz-_IXYA8lBInOS4jbS4,100
|
|
183
|
+
synapse_sdk/utils/encryption.py,sha256=KMARrAk5aIHfBLC8CvdXiSIuaGvxljluubjF9PVLf7c,5100
|
|
183
184
|
synapse_sdk/utils/file.py,sha256=wWBQAx0cB5a-fjfRMeJV-KjBil1ZyKRz-vXno3xBSoo,6834
|
|
184
185
|
synapse_sdk/utils/http.py,sha256=yRxYfru8tMnBVeBK-7S0Ga13yOf8oRHquG5e8K_FWcI,4759
|
|
185
186
|
synapse_sdk/utils/module_loading.py,sha256=chHpU-BZjtYaTBD_q0T7LcKWtqKvYBS4L0lPlKkoMQ8,1020
|
|
@@ -210,9 +211,9 @@ synapse_sdk/utils/storage/providers/gcp.py,sha256=i2BQCu1Kej1If9SuNr2_lEyTcr5M_n
|
|
|
210
211
|
synapse_sdk/utils/storage/providers/http.py,sha256=2DhIulND47JOnS5ZY7MZUex7Su3peAPksGo1Wwg07L4,5828
|
|
211
212
|
synapse_sdk/utils/storage/providers/s3.py,sha256=ZmqekAvIgcQBdRU-QVJYv1Rlp6VHfXwtbtjTSphua94,2573
|
|
212
213
|
synapse_sdk/utils/storage/providers/sftp.py,sha256=_8s9hf0JXIO21gvm-JVS00FbLsbtvly4c-ETLRax68A,1426
|
|
213
|
-
synapse_sdk-1.0.
|
|
214
|
-
synapse_sdk-1.0.
|
|
215
|
-
synapse_sdk-1.0.
|
|
216
|
-
synapse_sdk-1.0.
|
|
217
|
-
synapse_sdk-1.0.
|
|
218
|
-
synapse_sdk-1.0.
|
|
214
|
+
synapse_sdk-1.0.0b4.dist-info/licenses/LICENSE,sha256=bKzmC5YAg4V1Fhl8OO_tqY8j62hgdncAkN7VrdjmrGk,1101
|
|
215
|
+
synapse_sdk-1.0.0b4.dist-info/METADATA,sha256=5fqI0sn3Ro1CRZQVGAputa73_iZGd360Cxww_o6_K3Y,3718
|
|
216
|
+
synapse_sdk-1.0.0b4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
217
|
+
synapse_sdk-1.0.0b4.dist-info/entry_points.txt,sha256=VNptJoGoNJI8yLXfBmhgUefMsmGI0m3-0YoMvrOgbxo,48
|
|
218
|
+
synapse_sdk-1.0.0b4.dist-info/top_level.txt,sha256=ytgJMRK1slVOKUpgcw3LEyHHP7S34J6n_gJzdkcSsw8,12
|
|
219
|
+
synapse_sdk-1.0.0b4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|