hopx-ai 0.1.15__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 hopx-ai might be problematic. Click here for more details.
- hopx_ai/__init__.py +114 -0
- hopx_ai/_agent_client.py +391 -0
- hopx_ai/_async_agent_client.py +223 -0
- hopx_ai/_async_cache.py +38 -0
- hopx_ai/_async_client.py +230 -0
- hopx_ai/_async_commands.py +58 -0
- hopx_ai/_async_env_vars.py +151 -0
- hopx_ai/_async_files.py +81 -0
- hopx_ai/_async_files_clean.py +489 -0
- hopx_ai/_async_terminal.py +184 -0
- hopx_ai/_client.py +230 -0
- hopx_ai/_generated/__init__.py +22 -0
- hopx_ai/_generated/models.py +502 -0
- hopx_ai/_temp_async_token.py +14 -0
- hopx_ai/_test_env_fix.py +30 -0
- hopx_ai/_utils.py +9 -0
- hopx_ai/_ws_client.py +141 -0
- hopx_ai/async_sandbox.py +763 -0
- hopx_ai/cache.py +97 -0
- hopx_ai/commands.py +174 -0
- hopx_ai/desktop.py +1227 -0
- hopx_ai/env_vars.py +244 -0
- hopx_ai/errors.py +249 -0
- hopx_ai/files.py +489 -0
- hopx_ai/models.py +274 -0
- hopx_ai/models_updated.py +270 -0
- hopx_ai/sandbox.py +1447 -0
- hopx_ai/template/__init__.py +47 -0
- hopx_ai/template/build_flow.py +540 -0
- hopx_ai/template/builder.py +300 -0
- hopx_ai/template/file_hasher.py +81 -0
- hopx_ai/template/ready_checks.py +106 -0
- hopx_ai/template/tar_creator.py +122 -0
- hopx_ai/template/types.py +199 -0
- hopx_ai/terminal.py +164 -0
- hopx_ai-0.1.15.dist-info/METADATA +462 -0
- hopx_ai-0.1.15.dist-info/RECORD +38 -0
- hopx_ai-0.1.15.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: openapi_specs_3.1.md
|
|
3
|
+
# timestamp: 2025-10-22T08:21:26+00:00
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Annotated, Any, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
from pydantic import AnyUrl, AwareDatetime, BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Features(BaseModel):
|
|
14
|
+
code_execution: Optional[bool] = None
|
|
15
|
+
file_operations: Optional[bool] = None
|
|
16
|
+
terminal_access: Optional[bool] = None
|
|
17
|
+
websocket_streaming: Optional[bool] = None
|
|
18
|
+
rich_output: Optional[bool] = None
|
|
19
|
+
background_jobs: Optional[bool] = None
|
|
20
|
+
ipython_kernel: Optional[bool] = None
|
|
21
|
+
system_metrics: Optional[bool] = None
|
|
22
|
+
languages: Annotated[
|
|
23
|
+
Optional[List[str]], Field(examples=[['python', 'javascript', 'bash', 'go']])
|
|
24
|
+
] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HealthResponse(BaseModel):
|
|
28
|
+
status: Annotated[Optional[str], Field(examples=['healthy'])] = None
|
|
29
|
+
agent: Annotated[Optional[str], Field(examples=['hopx-vm-agent-desktop'])] = None
|
|
30
|
+
version: Annotated[Optional[str], Field(examples=['3.1.1'])] = None
|
|
31
|
+
uptime: Annotated[Optional[str], Field(examples=['2h34m12s'])] = None
|
|
32
|
+
go_version: Annotated[Optional[str], Field(examples=['go1.22.2'])] = None
|
|
33
|
+
vm_id: Annotated[Optional[str], Field(examples=['1760954509layu9lw0'])] = None
|
|
34
|
+
features: Optional[Features] = None
|
|
35
|
+
active_streams: Annotated[Optional[int], Field(examples=[0])] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InfoResponse(BaseModel):
|
|
39
|
+
vm_id: Optional[str] = None
|
|
40
|
+
agent: Optional[str] = None
|
|
41
|
+
agent_version: Optional[str] = None
|
|
42
|
+
os: Optional[str] = None
|
|
43
|
+
arch: Optional[str] = None
|
|
44
|
+
go_version: Optional[str] = None
|
|
45
|
+
vm_ip: Optional[str] = None
|
|
46
|
+
vm_port: Optional[str] = None
|
|
47
|
+
start_time: Optional[AwareDatetime] = None
|
|
48
|
+
uptime: Annotated[Optional[float], Field(description='Uptime in seconds')] = None
|
|
49
|
+
endpoints: Annotated[
|
|
50
|
+
Optional[Dict[str, str]], Field(description='Map of endpoint names to HTTP methods + paths')
|
|
51
|
+
] = None
|
|
52
|
+
features: Annotated[Optional[Dict[str, Any]], Field(description='Available features')] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Cpu(BaseModel):
|
|
56
|
+
usage_percent: Optional[float] = None
|
|
57
|
+
cores: Optional[int] = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Memory(BaseModel):
|
|
61
|
+
total: Optional[int] = None
|
|
62
|
+
used: Optional[int] = None
|
|
63
|
+
free: Optional[int] = None
|
|
64
|
+
usage_percent: Optional[float] = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Disk(BaseModel):
|
|
68
|
+
total: Optional[int] = None
|
|
69
|
+
used: Optional[int] = None
|
|
70
|
+
free: Optional[int] = None
|
|
71
|
+
usage_percent: Optional[float] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SystemMetrics(BaseModel):
|
|
75
|
+
cpu: Optional[Cpu] = None
|
|
76
|
+
memory: Optional[Memory] = None
|
|
77
|
+
disk: Optional[Disk] = None
|
|
78
|
+
uptime: Annotated[Optional[float], Field(description='System uptime in seconds')] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Language(Enum):
|
|
82
|
+
PYTHON = 'python'
|
|
83
|
+
PYTHON3 = 'python3'
|
|
84
|
+
NODE = 'node'
|
|
85
|
+
NODEJS = 'nodejs'
|
|
86
|
+
JAVASCRIPT = 'javascript'
|
|
87
|
+
JS = 'js'
|
|
88
|
+
BASH = 'bash'
|
|
89
|
+
SH = 'sh'
|
|
90
|
+
SHELL = 'shell'
|
|
91
|
+
GO = 'go'
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ExecuteRequest(BaseModel):
|
|
95
|
+
code: Annotated[str, Field(description='Code to execute', examples=['print("Hello, World!")'])]
|
|
96
|
+
language: Annotated[Language, Field(description='Programming language', examples=['python'])]
|
|
97
|
+
env: Annotated[
|
|
98
|
+
Optional[Dict[str, str]],
|
|
99
|
+
Field(
|
|
100
|
+
description='Optional environment variables for this execution only.\n\n**Priority**: Request env > Global env > Agent env\n\n**Example**: `{"DATABASE_URL": "postgres://localhost/app", "DEBUG": "true"}`\n',
|
|
101
|
+
examples=[{'DATABASE_URL': 'postgres://localhost/testdb', 'DEBUG': 'true'}],
|
|
102
|
+
),
|
|
103
|
+
] = None
|
|
104
|
+
timeout: Annotated[Optional[int], Field(description='Timeout in seconds', ge=1, le=300)] = 30
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ExecuteResponse(BaseModel):
|
|
108
|
+
stdout: Annotated[Optional[str], Field(description='Standard output')] = None
|
|
109
|
+
stderr: Annotated[Optional[str], Field(description='Standard error')] = None
|
|
110
|
+
exit_code: Annotated[Optional[int], Field(description='Exit code (0 = success)')] = None
|
|
111
|
+
execution_time: Annotated[Optional[float], Field(description='Execution time in seconds')] = (
|
|
112
|
+
None
|
|
113
|
+
)
|
|
114
|
+
timestamp: Optional[AwareDatetime] = None
|
|
115
|
+
language: Optional[str] = None
|
|
116
|
+
success: Annotated[
|
|
117
|
+
Optional[bool], Field(description='Whether execution succeeded (exit_code == 0)')
|
|
118
|
+
] = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class BackgroundExecuteRequest(ExecuteRequest):
|
|
122
|
+
name: Annotated[
|
|
123
|
+
Optional[str], Field(description='Optional process name for identification')
|
|
124
|
+
] = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Status(Enum):
|
|
128
|
+
running = 'running'
|
|
129
|
+
completed = 'completed'
|
|
130
|
+
failed = 'failed'
|
|
131
|
+
killed = 'killed'
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class BackgroundExecuteResponse(BaseModel):
|
|
135
|
+
process_id: Annotated[Optional[str], Field(description='Unique process identifier')] = None
|
|
136
|
+
execution_id: Annotated[Optional[str], Field(description='Execution identifier')] = None
|
|
137
|
+
status: Optional[Status] = None
|
|
138
|
+
start_time: Optional[AwareDatetime] = None
|
|
139
|
+
message: Optional[str] = None
|
|
140
|
+
name: Annotated[Optional[str], Field(description='Process name (if provided)')] = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Status1(Enum):
|
|
144
|
+
queued = 'queued'
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class AsyncExecuteResponse(BaseModel):
|
|
148
|
+
execution_id: Annotated[
|
|
149
|
+
Optional[str], Field(description='Unique execution identifier', examples=['abc123-def456'])
|
|
150
|
+
] = None
|
|
151
|
+
status: Annotated[
|
|
152
|
+
Optional[Status1], Field(description='Execution status (always "queued" initially)')
|
|
153
|
+
] = None
|
|
154
|
+
callback_url: Annotated[
|
|
155
|
+
Optional[AnyUrl],
|
|
156
|
+
Field(
|
|
157
|
+
description='URL that will receive execution results',
|
|
158
|
+
examples=['https://client.com/webhooks/execution'],
|
|
159
|
+
),
|
|
160
|
+
] = None
|
|
161
|
+
message: Annotated[
|
|
162
|
+
Optional[str],
|
|
163
|
+
Field(
|
|
164
|
+
description='Human-readable message',
|
|
165
|
+
examples=['Execution queued. Will POST to callback_url when complete.'],
|
|
166
|
+
),
|
|
167
|
+
] = None
|
|
168
|
+
timestamp: Annotated[Optional[AwareDatetime], Field(description='Queue timestamp')] = None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class Status2(Enum):
|
|
172
|
+
COMPLETED = 'completed'
|
|
173
|
+
FAILED = 'failed'
|
|
174
|
+
TIMEOUT = 'timeout'
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class WebhookExecutionComplete(BaseModel):
|
|
178
|
+
execution_id: Annotated[
|
|
179
|
+
Optional[str],
|
|
180
|
+
Field(
|
|
181
|
+
description='Execution identifier (matches AsyncExecuteResponse)',
|
|
182
|
+
examples=['abc123-def456'],
|
|
183
|
+
),
|
|
184
|
+
] = None
|
|
185
|
+
status: Annotated[Optional[Status2], Field(description='Final execution status')] = None
|
|
186
|
+
stdout: Annotated[Optional[str], Field(description='Standard output')] = None
|
|
187
|
+
stderr: Annotated[Optional[str], Field(description='Standard error')] = None
|
|
188
|
+
exit_code: Annotated[Optional[int], Field(description='Exit code (0 = success)')] = None
|
|
189
|
+
execution_time: Annotated[
|
|
190
|
+
Optional[float], Field(description='Execution time in seconds', examples=[600.123])
|
|
191
|
+
] = None
|
|
192
|
+
timestamp: Annotated[Optional[AwareDatetime], Field(description='Completion timestamp')] = None
|
|
193
|
+
error: Annotated[
|
|
194
|
+
Optional[str], Field(description='Error message (if status is "failed" or "timeout")')
|
|
195
|
+
] = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class Status3(Enum):
|
|
199
|
+
running = 'running'
|
|
200
|
+
completed = 'completed'
|
|
201
|
+
failed = 'failed'
|
|
202
|
+
killed = 'killed'
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class ProcessInfo(BaseModel):
|
|
206
|
+
process_id: Optional[str] = None
|
|
207
|
+
execution_id: Optional[str] = None
|
|
208
|
+
name: Optional[str] = None
|
|
209
|
+
status: Optional[Status3] = None
|
|
210
|
+
language: Optional[str] = None
|
|
211
|
+
start_time: Optional[AwareDatetime] = None
|
|
212
|
+
end_time: Optional[AwareDatetime] = None
|
|
213
|
+
exit_code: Optional[int] = None
|
|
214
|
+
duration: Annotated[Optional[float], Field(description='Duration in seconds')] = None
|
|
215
|
+
pid: Annotated[Optional[int], Field(description='System process ID')] = None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class ProcessListResponse(BaseModel):
|
|
219
|
+
processes: Optional[List[ProcessInfo]] = None
|
|
220
|
+
count: Optional[int] = None
|
|
221
|
+
timestamp: Optional[AwareDatetime] = None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class Type(Enum):
|
|
225
|
+
image_png = 'image/png'
|
|
226
|
+
text_html = 'text/html'
|
|
227
|
+
application_json = 'application/json'
|
|
228
|
+
application_vnd_dataframe_json = 'application/vnd.dataframe+json' # Pandas dataframe
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class Format(Enum):
|
|
232
|
+
base64 = 'base64'
|
|
233
|
+
html = 'html'
|
|
234
|
+
json = 'json'
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class Source(Enum):
|
|
238
|
+
matplotlib = 'matplotlib'
|
|
239
|
+
pandas = 'pandas'
|
|
240
|
+
plotly = 'plotly'
|
|
241
|
+
pandas_stdout = 'pandas_stdout' # Pandas to stdout
|
|
242
|
+
other = 'other'
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class Metadata(BaseModel):
|
|
246
|
+
source: Optional[Source] = None
|
|
247
|
+
filename: Optional[str] = None
|
|
248
|
+
size: Optional[int] = None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class RichOutput(BaseModel):
|
|
252
|
+
type: Annotated[Optional[Type], Field(description='MIME type')] = None
|
|
253
|
+
format: Optional[Format] = None
|
|
254
|
+
data: Annotated[
|
|
255
|
+
Optional[Union[str, Dict[str, Any]]], Field(description='Output data (base64 for images, HTML for tables, dict for dataframes)')
|
|
256
|
+
] = None
|
|
257
|
+
metadata: Optional[Metadata] = None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class CommandResponse(BaseModel):
|
|
261
|
+
stdout: Optional[str] = None
|
|
262
|
+
stderr: Optional[str] = None
|
|
263
|
+
exit_code: Optional[int] = None
|
|
264
|
+
execution_time: Optional[float] = None
|
|
265
|
+
command: Optional[str] = None
|
|
266
|
+
timestamp: Optional[AwareDatetime] = None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class EnvVarsSetRequest(BaseModel):
|
|
270
|
+
env_vars: Annotated[
|
|
271
|
+
Dict[str, str],
|
|
272
|
+
Field(
|
|
273
|
+
description='Environment variables as key-value pairs',
|
|
274
|
+
examples=[
|
|
275
|
+
{
|
|
276
|
+
'DATABASE_URL': 'postgres://prod-db/app',
|
|
277
|
+
'API_KEY': 'sk-1234567890abcdef',
|
|
278
|
+
'ENVIRONMENT': 'production',
|
|
279
|
+
}
|
|
280
|
+
],
|
|
281
|
+
),
|
|
282
|
+
]
|
|
283
|
+
timestamp: Annotated[
|
|
284
|
+
Optional[AwareDatetime],
|
|
285
|
+
Field(
|
|
286
|
+
description='Optional timestamp for deduplication (prevents old requests from overwriting new ones)'
|
|
287
|
+
),
|
|
288
|
+
] = None
|
|
289
|
+
merge: Annotated[
|
|
290
|
+
Optional[bool],
|
|
291
|
+
Field(
|
|
292
|
+
description='If true, merge with existing env vars. If false (default), replace all.\n\n**Note**: PATCH /env always merges, PUT /env always replaces (this field is ignored for those endpoints)\n'
|
|
293
|
+
),
|
|
294
|
+
] = False
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class EnvVarsResponse(BaseModel):
|
|
298
|
+
env_vars: Annotated[
|
|
299
|
+
Optional[Dict[str, str]],
|
|
300
|
+
Field(
|
|
301
|
+
description='Environment variables as key-value pairs.\n\n**Security**: Sensitive values (containing KEY, SECRET, PASSWORD, TOKEN, etc.) are masked.\n',
|
|
302
|
+
examples=[
|
|
303
|
+
{
|
|
304
|
+
'DATABASE_URL': 'postgres://localhost/app',
|
|
305
|
+
'API_KEY': '***MASKED***',
|
|
306
|
+
'DEBUG': 'true',
|
|
307
|
+
}
|
|
308
|
+
],
|
|
309
|
+
),
|
|
310
|
+
] = None
|
|
311
|
+
count: Annotated[
|
|
312
|
+
Optional[int], Field(description='Number of environment variables', examples=[3])
|
|
313
|
+
] = None
|
|
314
|
+
timestamp: Annotated[Optional[AwareDatetime], Field(description='Response timestamp')] = None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class FileInfo(BaseModel):
|
|
318
|
+
name: Annotated[Optional[str], Field(description='File or directory name')] = None
|
|
319
|
+
path: Annotated[Optional[str], Field(description='Full path')] = None
|
|
320
|
+
size: Annotated[Optional[int], Field(description='Size in bytes')] = None
|
|
321
|
+
is_directory: Optional[bool] = None
|
|
322
|
+
modified_time: Optional[AwareDatetime] = None
|
|
323
|
+
permissions: Annotated[
|
|
324
|
+
Optional[str], Field(description='Unix permissions (e.g., drwxr-xr-x)')
|
|
325
|
+
] = None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class FileListResponse(BaseModel):
|
|
329
|
+
files: Optional[List[FileInfo]] = None
|
|
330
|
+
path: Annotated[Optional[str], Field(description='Directory path')] = None
|
|
331
|
+
count: Annotated[Optional[int], Field(description='Number of files')] = None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class FileContentResponse(BaseModel):
|
|
335
|
+
content: Annotated[Optional[str], Field(description='File contents')] = None
|
|
336
|
+
path: Optional[str] = None
|
|
337
|
+
size: Annotated[Optional[int], Field(description='Size in bytes')] = None
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class FileWriteRequest(BaseModel):
|
|
341
|
+
path: Annotated[str, Field(description='File path to write', examples=['/workspace/script.py'])]
|
|
342
|
+
content: Annotated[str, Field(description='File contents')]
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class FileResponse(BaseModel):
|
|
346
|
+
message: Optional[str] = None
|
|
347
|
+
path: Optional[str] = None
|
|
348
|
+
success: Optional[bool] = None
|
|
349
|
+
size: Annotated[Optional[int], Field(description='File size (for write operations)')] = None
|
|
350
|
+
timestamp: Optional[AwareDatetime] = None
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class VNCInfo(BaseModel):
|
|
354
|
+
url: Annotated[
|
|
355
|
+
Optional[str], Field(description='VNC connection URL', examples=['vnc://vm.hopx.dev:5901'])
|
|
356
|
+
] = None
|
|
357
|
+
password: Annotated[Optional[str], Field(description='VNC password')] = None
|
|
358
|
+
display: Annotated[Optional[int], Field(description='X11 display number', examples=[1])] = None
|
|
359
|
+
port: Annotated[Optional[int], Field(description='VNC port', examples=[5901])] = None
|
|
360
|
+
websocket_url: Annotated[
|
|
361
|
+
Optional[str], Field(description='noVNC WebSocket URL (if available)')
|
|
362
|
+
] = None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class WindowInfo(BaseModel):
|
|
366
|
+
id: Annotated[Optional[int], Field(description='Window ID (X11 window identifier)')] = None
|
|
367
|
+
title: Annotated[
|
|
368
|
+
Optional[str], Field(description='Window title', examples=['Firefox - Mozilla Firefox'])
|
|
369
|
+
] = None
|
|
370
|
+
x: Annotated[Optional[int], Field(description='X coordinate')] = None
|
|
371
|
+
y: Annotated[Optional[int], Field(description='Y coordinate')] = None
|
|
372
|
+
width: Annotated[Optional[int], Field(description='Window width')] = None
|
|
373
|
+
height: Annotated[Optional[int], Field(description='Window height')] = None
|
|
374
|
+
is_active: Annotated[
|
|
375
|
+
Optional[bool], Field(description='Whether this window is currently active')
|
|
376
|
+
] = None
|
|
377
|
+
is_minimized: Annotated[
|
|
378
|
+
Optional[bool], Field(description='Whether this window is minimized')
|
|
379
|
+
] = None
|
|
380
|
+
pid: Annotated[Optional[int], Field(description='Process ID owning this window')] = None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class Status4(Enum):
|
|
384
|
+
RECORDING = 'recording'
|
|
385
|
+
STOPPED = 'stopped'
|
|
386
|
+
FAILED = 'failed'
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class Format1(Enum):
|
|
390
|
+
MP4 = 'mp4'
|
|
391
|
+
WEBM = 'webm'
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class RecordingInfo(BaseModel):
|
|
395
|
+
recording_id: Annotated[Optional[str], Field(description='Unique recording identifier')] = None
|
|
396
|
+
status: Annotated[Optional[Status4], Field(description='Recording status')] = None
|
|
397
|
+
start_time: Annotated[Optional[AwareDatetime], Field(description='Recording start time')] = None
|
|
398
|
+
end_time: Annotated[
|
|
399
|
+
Optional[AwareDatetime], Field(description='Recording end time (if stopped)')
|
|
400
|
+
] = None
|
|
401
|
+
duration: Annotated[Optional[float], Field(description='Recording duration in seconds')] = None
|
|
402
|
+
file_path: Annotated[Optional[str], Field(description='Path to recorded video file')] = None
|
|
403
|
+
file_size: Annotated[Optional[int], Field(description='Video file size in bytes')] = None
|
|
404
|
+
format: Annotated[Optional[Format1], Field(description='Video format')] = None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class Display(BaseModel):
|
|
408
|
+
id: Optional[int] = None
|
|
409
|
+
name: Optional[str] = None
|
|
410
|
+
width: Optional[int] = None
|
|
411
|
+
height: Optional[int] = None
|
|
412
|
+
primary: Optional[bool] = None
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class DisplayInfo(BaseModel):
|
|
416
|
+
width: Annotated[
|
|
417
|
+
Optional[int], Field(description='Display width in pixels', examples=[1920])
|
|
418
|
+
] = None
|
|
419
|
+
height: Annotated[
|
|
420
|
+
Optional[int], Field(description='Display height in pixels', examples=[1080])
|
|
421
|
+
] = None
|
|
422
|
+
depth: Annotated[
|
|
423
|
+
Optional[int], Field(description='Color depth (bits per pixel)', examples=[24])
|
|
424
|
+
] = None
|
|
425
|
+
refresh_rate: Annotated[
|
|
426
|
+
Optional[int], Field(description='Refresh rate in Hz', examples=[60])
|
|
427
|
+
] = None
|
|
428
|
+
displays: Annotated[
|
|
429
|
+
Optional[List[Display]],
|
|
430
|
+
Field(description='List of available displays (multi-monitor support)'),
|
|
431
|
+
] = None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class Format2(Enum):
|
|
435
|
+
png = 'png'
|
|
436
|
+
jpeg = 'jpeg'
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class ScreenshotResponse(BaseModel):
|
|
440
|
+
image: Annotated[Optional[str], Field(description='Base64-encoded image data')] = None
|
|
441
|
+
format: Annotated[Optional[Format2], Field(description='Image format')] = None
|
|
442
|
+
width: Annotated[Optional[int], Field(description='Image width in pixels')] = None
|
|
443
|
+
height: Annotated[Optional[int], Field(description='Image height in pixels')] = None
|
|
444
|
+
size: Annotated[Optional[int], Field(description='Image size in bytes')] = None
|
|
445
|
+
timestamp: Annotated[Optional[AwareDatetime], Field(description='Screenshot timestamp')] = None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class MetricsSnapshot(BaseModel):
|
|
449
|
+
uptime_seconds: Annotated[Optional[float], Field(description='Agent uptime in seconds')] = None
|
|
450
|
+
total_requests: Annotated[Optional[int], Field(description='Total HTTP requests handled')] = (
|
|
451
|
+
None
|
|
452
|
+
)
|
|
453
|
+
total_errors: Annotated[Optional[int], Field(description='Total errors encountered')] = None
|
|
454
|
+
active_executions: Annotated[
|
|
455
|
+
Optional[int], Field(description='Current active code executions')
|
|
456
|
+
] = None
|
|
457
|
+
total_executions: Annotated[
|
|
458
|
+
Optional[int], Field(description='Total code executions completed')
|
|
459
|
+
] = None
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class Code(Enum):
|
|
463
|
+
METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED'
|
|
464
|
+
INVALID_JSON = 'INVALID_JSON'
|
|
465
|
+
MISSING_PARAMETER = 'MISSING_PARAMETER'
|
|
466
|
+
PATH_NOT_ALLOWED = 'PATH_NOT_ALLOWED'
|
|
467
|
+
FILE_NOT_FOUND = 'FILE_NOT_FOUND'
|
|
468
|
+
PERMISSION_DENIED = 'PERMISSION_DENIED'
|
|
469
|
+
COMMAND_FAILED = 'COMMAND_FAILED'
|
|
470
|
+
EXECUTION_TIMEOUT = 'EXECUTION_TIMEOUT'
|
|
471
|
+
EXECUTION_FAILED = 'EXECUTION_FAILED'
|
|
472
|
+
INTERNAL_ERROR = 'INTERNAL_ERROR'
|
|
473
|
+
INVALID_PATH = 'INVALID_PATH'
|
|
474
|
+
FILE_ALREADY_EXISTS = 'FILE_ALREADY_EXISTS'
|
|
475
|
+
DIRECTORY_NOT_FOUND = 'DIRECTORY_NOT_FOUND'
|
|
476
|
+
INVALID_REQUEST = 'INVALID_REQUEST'
|
|
477
|
+
PROCESS_NOT_FOUND = 'PROCESS_NOT_FOUND'
|
|
478
|
+
DESKTOP_NOT_AVAILABLE = 'DESKTOP_NOT_AVAILABLE'
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class ErrorResponse(BaseModel):
|
|
482
|
+
error: Annotated[
|
|
483
|
+
str, Field(description='Human-readable error message', examples=['File not found'])
|
|
484
|
+
]
|
|
485
|
+
code: Annotated[
|
|
486
|
+
Optional[Code],
|
|
487
|
+
Field(description='Machine-readable error code', examples=['FILE_NOT_FOUND']),
|
|
488
|
+
] = None
|
|
489
|
+
request_id: Annotated[
|
|
490
|
+
Optional[str],
|
|
491
|
+
Field(
|
|
492
|
+
description='Request ID for tracing (from X-Request-ID header)',
|
|
493
|
+
examples=['550e8400-e29b-41d4-a716-446655440000'],
|
|
494
|
+
),
|
|
495
|
+
] = None
|
|
496
|
+
timestamp: AwareDatetime
|
|
497
|
+
path: Annotated[
|
|
498
|
+
Optional[str], Field(description='Related file path (for file operation errors)')
|
|
499
|
+
] = None
|
|
500
|
+
details: Annotated[Optional[Dict[str, Any]], Field(description='Additional error context')] = (
|
|
501
|
+
None
|
|
502
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Token management pentru AsyncSandbox (copiat din sandbox.py)
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class TokenData:
|
|
9
|
+
"""JWT token data."""
|
|
10
|
+
token: str
|
|
11
|
+
expires_at: datetime
|
|
12
|
+
|
|
13
|
+
# Global token cache (shared between Sandbox instances)
|
|
14
|
+
_token_cache: Dict[str, TokenData] = {}
|
hopx_ai/_test_env_fix.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Quick test pentru env vars fix
|
|
2
|
+
import sys
|
|
3
|
+
sys.path.insert(0, "/var/www/sdks/python")
|
|
4
|
+
|
|
5
|
+
from hopx_ai import Sandbox
|
|
6
|
+
|
|
7
|
+
sandbox = Sandbox.create(
|
|
8
|
+
template="code-interpreter",
|
|
9
|
+
api_key="hopx_live_Lap0VJrWLii8.KSN6iLWELs13jHt960gSK9Eq63trgPApqMf7yLGVTNo"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
print("1️⃣ Set env var...")
|
|
13
|
+
sandbox.env.update({"TEST_VAR": "hello_world"})
|
|
14
|
+
all_vars = sandbox.env.get_all()
|
|
15
|
+
print(f"✅ TEST_VAR = {all_vars.get('TEST_VAR')}")
|
|
16
|
+
|
|
17
|
+
print("\n2️⃣ Update env var...")
|
|
18
|
+
sandbox.env.update({"TEST_VAR": "updated_value", "ANOTHER_VAR": "test123"})
|
|
19
|
+
all_vars = sandbox.env.get_all()
|
|
20
|
+
print(f"✅ TEST_VAR = {all_vars.get('TEST_VAR')}")
|
|
21
|
+
print(f"✅ ANOTHER_VAR = {all_vars.get('ANOTHER_VAR')}")
|
|
22
|
+
|
|
23
|
+
print("\n3️⃣ Delete env var...")
|
|
24
|
+
sandbox.env.delete("TEST_VAR")
|
|
25
|
+
all_vars = sandbox.env.get_all()
|
|
26
|
+
print(f"✅ TEST_VAR after delete = {all_vars.get('TEST_VAR')}")
|
|
27
|
+
print(f"✅ ANOTHER_VAR still = {all_vars.get('ANOTHER_VAR')}")
|
|
28
|
+
|
|
29
|
+
sandbox.kill()
|
|
30
|
+
print("\n✅ Test PASSED!")
|
hopx_ai/_utils.py
ADDED
hopx_ai/_ws_client.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""WebSocket client for real-time streaming."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional, Dict, Any, AsyncIterator, Callable
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import websockets
|
|
11
|
+
from websockets.client import WebSocketClientProtocol
|
|
12
|
+
WEBSOCKETS_AVAILABLE = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
WEBSOCKETS_AVAILABLE = False
|
|
15
|
+
WebSocketClientProtocol = Any # type: ignore
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WebSocketClient:
|
|
21
|
+
"""
|
|
22
|
+
WebSocket client for Agent API streaming.
|
|
23
|
+
|
|
24
|
+
Handles WebSocket connections with automatic reconnection,
|
|
25
|
+
message protocol, and async iteration.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, agent_url: str):
|
|
29
|
+
"""
|
|
30
|
+
Initialize WebSocket client.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
agent_url: Agent base URL (https://...)
|
|
34
|
+
"""
|
|
35
|
+
if not WEBSOCKETS_AVAILABLE:
|
|
36
|
+
raise ImportError(
|
|
37
|
+
"websockets library is required for WebSocket features. "
|
|
38
|
+
"Install with: pip install websockets"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self.agent_url = agent_url.rstrip('/')
|
|
42
|
+
# Convert https:// to wss:// for WebSocket
|
|
43
|
+
parsed = urlparse(self.agent_url)
|
|
44
|
+
ws_scheme = 'wss' if parsed.scheme == 'https' else 'ws'
|
|
45
|
+
self.ws_base_url = f"{ws_scheme}://{parsed.netloc}"
|
|
46
|
+
|
|
47
|
+
logger.debug(f"WebSocket client initialized: {self.ws_base_url}")
|
|
48
|
+
|
|
49
|
+
async def connect(
|
|
50
|
+
self,
|
|
51
|
+
endpoint: str,
|
|
52
|
+
*,
|
|
53
|
+
timeout: Optional[int] = None
|
|
54
|
+
) -> WebSocketClientProtocol:
|
|
55
|
+
"""
|
|
56
|
+
Connect to WebSocket endpoint.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
endpoint: WebSocket endpoint path (e.g., "/terminal")
|
|
60
|
+
timeout: Connection timeout in seconds
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
WebSocket connection
|
|
64
|
+
"""
|
|
65
|
+
url = f"{self.ws_base_url}{endpoint}"
|
|
66
|
+
logger.debug(f"Connecting to WebSocket: {url}")
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
ws = await asyncio.wait_for(
|
|
70
|
+
websockets.connect(url),
|
|
71
|
+
timeout=timeout
|
|
72
|
+
)
|
|
73
|
+
logger.debug(f"WebSocket connected: {endpoint}")
|
|
74
|
+
return ws
|
|
75
|
+
except asyncio.TimeoutError:
|
|
76
|
+
raise TimeoutError(f"WebSocket connection timeout: {endpoint}")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"WebSocket connection failed: {e}")
|
|
79
|
+
raise
|
|
80
|
+
|
|
81
|
+
async def send_message(
|
|
82
|
+
self,
|
|
83
|
+
ws: WebSocketClientProtocol,
|
|
84
|
+
message: Dict[str, Any]
|
|
85
|
+
) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Send JSON message over WebSocket.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
ws: WebSocket connection
|
|
91
|
+
message: Message dictionary
|
|
92
|
+
"""
|
|
93
|
+
await ws.send(json.dumps(message))
|
|
94
|
+
logger.debug(f"Sent WS message: {message.get('type', 'unknown')}")
|
|
95
|
+
|
|
96
|
+
async def receive_message(
|
|
97
|
+
self,
|
|
98
|
+
ws: WebSocketClientProtocol
|
|
99
|
+
) -> Dict[str, Any]:
|
|
100
|
+
"""
|
|
101
|
+
Receive and parse JSON message from WebSocket.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
ws: WebSocket connection
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Parsed message dictionary
|
|
108
|
+
"""
|
|
109
|
+
data = await ws.recv()
|
|
110
|
+
if isinstance(data, bytes):
|
|
111
|
+
data = data.decode('utf-8')
|
|
112
|
+
message = json.loads(data)
|
|
113
|
+
logger.debug(f"Received WS message: {message.get('type', 'unknown')}")
|
|
114
|
+
return message
|
|
115
|
+
|
|
116
|
+
async def iter_messages(
|
|
117
|
+
self,
|
|
118
|
+
ws: WebSocketClientProtocol
|
|
119
|
+
) -> AsyncIterator[Dict[str, Any]]:
|
|
120
|
+
"""
|
|
121
|
+
Iterate over incoming messages.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
ws: WebSocket connection
|
|
125
|
+
|
|
126
|
+
Yields:
|
|
127
|
+
Parsed message dictionaries
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
async for data in ws:
|
|
131
|
+
if isinstance(data, bytes):
|
|
132
|
+
data = data.decode('utf-8')
|
|
133
|
+
message = json.loads(data)
|
|
134
|
+
logger.debug(f"Yielding WS message: {message.get('type', 'unknown')}")
|
|
135
|
+
yield message
|
|
136
|
+
except websockets.exceptions.ConnectionClosed:
|
|
137
|
+
logger.debug("WebSocket connection closed")
|
|
138
|
+
|
|
139
|
+
def __repr__(self) -> str:
|
|
140
|
+
return f"<WebSocketClient url={self.ws_base_url}>"
|
|
141
|
+
|