rl-rockcli 0.0.6 → 0.0.8
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.
- package/index.js +51 -20
- package/package.json +2 -2
- package/commands/log/core/constants.js +0 -237
- package/commands/log/core/display.js +0 -370
- package/commands/log/core/search.js +0 -330
- package/commands/log/core/tail.js +0 -216
- package/commands/log/core/utils.js +0 -424
- package/commands/log.js +0 -298
- package/commands/sandbox/core/log-bridge.js +0 -119
- package/commands/sandbox/core/replay/analyzer.js +0 -311
- package/commands/sandbox/core/replay/batch-orchestrator.js +0 -536
- package/commands/sandbox/core/replay/batch-task.js +0 -369
- package/commands/sandbox/core/replay/concurrent-display.js +0 -70
- package/commands/sandbox/core/replay/concurrent-orchestrator.js +0 -170
- package/commands/sandbox/core/replay/data-source.js +0 -86
- package/commands/sandbox/core/replay/display.js +0 -231
- package/commands/sandbox/core/replay/executor.js +0 -634
- package/commands/sandbox/core/replay/history-fetcher.js +0 -124
- package/commands/sandbox/core/replay/index.js +0 -338
- package/commands/sandbox/core/replay/loghouse-data-source.js +0 -177
- package/commands/sandbox/core/replay/pid-mapping.js +0 -26
- package/commands/sandbox/core/replay/request.js +0 -109
- package/commands/sandbox/core/replay/worker.js +0 -166
- package/commands/sandbox/core/session.js +0 -346
- package/commands/sandbox/log-bridge.js +0 -2
- package/commands/sandbox/ray.js +0 -2
- package/commands/sandbox/replay/analyzer.js +0 -311
- package/commands/sandbox/replay/batch-orchestrator.js +0 -536
- package/commands/sandbox/replay/batch-task.js +0 -369
- package/commands/sandbox/replay/concurrent-display.js +0 -70
- package/commands/sandbox/replay/concurrent-orchestrator.js +0 -170
- package/commands/sandbox/replay/display.js +0 -231
- package/commands/sandbox/replay/executor.js +0 -634
- package/commands/sandbox/replay/history-fetcher.js +0 -118
- package/commands/sandbox/replay/index.js +0 -338
- package/commands/sandbox/replay/pid-mapping.js +0 -26
- package/commands/sandbox/replay/request.js +0 -109
- package/commands/sandbox/replay/worker.js +0 -166
- package/commands/sandbox/replay.js +0 -2
- package/commands/sandbox/session.js +0 -2
- package/commands/sandbox-original.js +0 -1393
- package/commands/sandbox.js +0 -499
- package/help/help.json +0 -1071
- package/help/middleware.js +0 -71
- package/help/renderer.js +0 -800
- package/lib/plugin-context.js +0 -40
- package/sdks/sandbox/core/client.js +0 -845
- package/sdks/sandbox/core/config.js +0 -70
- package/sdks/sandbox/core/types.js +0 -74
- package/sdks/sandbox/httpLogger.js +0 -251
- package/sdks/sandbox/index.js +0 -9
- package/utils/asciiArt.js +0 -138
- package/utils/bun-compat.js +0 -59
- package/utils/ciPipelines.js +0 -138
- package/utils/cli.js +0 -17
- package/utils/command-router.js +0 -79
- package/utils/configManager.js +0 -503
- package/utils/dependency-resolver.js +0 -135
- package/utils/eagleeye_traceid.js +0 -151
- package/utils/envDetector.js +0 -78
- package/utils/execution_logger.js +0 -415
- package/utils/featureManager.js +0 -68
- package/utils/firstTimeTip.js +0 -44
- package/utils/hook-manager.js +0 -125
- package/utils/http-logger.js +0 -264
- package/utils/i18n.js +0 -139
- package/utils/image-progress.js +0 -159
- package/utils/logger.js +0 -154
- package/utils/plugin-loader.js +0 -124
- package/utils/plugin-manager.js +0 -348
- package/utils/ray_cli_wrapper.js +0 -746
- package/utils/sandbox-client.js +0 -419
- package/utils/terminal.js +0 -32
- package/utils/tips.js +0 -106
|
@@ -1,845 +0,0 @@
|
|
|
1
|
-
const axios = require('axios');
|
|
2
|
-
const { SandboxConfig } = require('./config');
|
|
3
|
-
const logger = require('../../../utils/logger');
|
|
4
|
-
const { httpLogger } = require('../httpLogger');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Sandbox Client for managing sandbox environments
|
|
8
|
-
*/
|
|
9
|
-
class SandboxClient {
|
|
10
|
-
constructor(config, options = {}) {
|
|
11
|
-
if (config instanceof SandboxConfig) {
|
|
12
|
-
this.config = config;
|
|
13
|
-
} else {
|
|
14
|
-
this.config = new SandboxConfig(config);
|
|
15
|
-
}
|
|
16
|
-
this.config.validate({ requireImage: options.requireImage !== false });
|
|
17
|
-
|
|
18
|
-
this._sandboxId = null;
|
|
19
|
-
this._hostName = null;
|
|
20
|
-
this._hostIp = null;
|
|
21
|
-
this._url = `${this.config.baseUrl}/apis/envs/sandbox/v1`;
|
|
22
|
-
this._onLog = options.onLog || null;
|
|
23
|
-
|
|
24
|
-
// Allow injection of custom headers builder for internal/external environments
|
|
25
|
-
// Core version only builds basic headers; internal version can inject additional headers
|
|
26
|
-
this._buildHeadersFn = options.buildHeaders || null;
|
|
27
|
-
|
|
28
|
-
// Create axios instance with interceptors for HTTP logging
|
|
29
|
-
this._axios = axios.create();
|
|
30
|
-
this._setupInterceptors();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Log a message, either through callback or directly
|
|
35
|
-
* @private
|
|
36
|
-
* @param {string} level - Log level ('info', 'warn', 'debug')
|
|
37
|
-
* @param {string} message - Log message
|
|
38
|
-
*/
|
|
39
|
-
_log(level, message) {
|
|
40
|
-
if (this._onLog) {
|
|
41
|
-
this._onLog(level, message);
|
|
42
|
-
} else {
|
|
43
|
-
logger[level](message);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Setup axios interceptors for HTTP logging
|
|
49
|
-
*/
|
|
50
|
-
_setupInterceptors() {
|
|
51
|
-
// Request interceptor
|
|
52
|
-
this._axios.interceptors.request.use((config) => {
|
|
53
|
-
config._startTime = Date.now();
|
|
54
|
-
httpLogger.logRequest(
|
|
55
|
-
config.method?.toUpperCase() || 'GET',
|
|
56
|
-
config.url,
|
|
57
|
-
config.data
|
|
58
|
-
);
|
|
59
|
-
return config;
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Response interceptor
|
|
63
|
-
this._axios.interceptors.response.use(
|
|
64
|
-
(response) => {
|
|
65
|
-
const duration = Date.now() - (response.config._startTime || Date.now());
|
|
66
|
-
// Extract trace ID from response headers
|
|
67
|
-
const traceId = this._extractTraceId(response.headers);
|
|
68
|
-
httpLogger.logResponse(
|
|
69
|
-
response.config.method?.toUpperCase() || 'GET',
|
|
70
|
-
response.config.url,
|
|
71
|
-
response.status,
|
|
72
|
-
duration,
|
|
73
|
-
response.data,
|
|
74
|
-
traceId
|
|
75
|
-
);
|
|
76
|
-
return response;
|
|
77
|
-
},
|
|
78
|
-
(error) => {
|
|
79
|
-
const duration = Date.now() - (error.config?._startTime || Date.now());
|
|
80
|
-
// Try to extract trace ID from error response
|
|
81
|
-
const traceId = error.response ? this._extractTraceId(error.response.headers) : null;
|
|
82
|
-
httpLogger.logError(
|
|
83
|
-
error.config?.method?.toUpperCase() || 'GET',
|
|
84
|
-
error.config?.url || 'unknown',
|
|
85
|
-
error,
|
|
86
|
-
duration,
|
|
87
|
-
traceId
|
|
88
|
-
);
|
|
89
|
-
return Promise.reject(error);
|
|
90
|
-
}
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Extract trace ID from response headers
|
|
96
|
-
* Checks common trace ID header names
|
|
97
|
-
*/
|
|
98
|
-
_extractTraceId(headers) {
|
|
99
|
-
if (!headers) return null;
|
|
100
|
-
|
|
101
|
-
// Common trace ID header names (case-insensitive in axios)
|
|
102
|
-
const traceIdHeaders = [
|
|
103
|
-
'x-trace-id',
|
|
104
|
-
'x-request-id',
|
|
105
|
-
'traceid',
|
|
106
|
-
'trace-id',
|
|
107
|
-
'request-id',
|
|
108
|
-
'x-b3-traceid',
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
for (const headerName of traceIdHeaders) {
|
|
112
|
-
const value = headers[headerName];
|
|
113
|
-
if (value) return value;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
get sandboxId() {
|
|
120
|
-
return this._sandboxId;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
get hostName() {
|
|
124
|
-
return this._hostName;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
get hostIp() {
|
|
128
|
-
return this._hostIp;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
_buildHeaders() {
|
|
132
|
-
// Core headers - common to all environments
|
|
133
|
-
const headers = {
|
|
134
|
-
'Content-Type': 'application/json',
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// XRL-Authorization is a common authentication mechanism
|
|
138
|
-
if (this.config.xrlAuthorization) {
|
|
139
|
-
headers['XRL-Authorization'] = `Bearer ${this.config.xrlAuthorization}`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Allow extra headers to be passed in config
|
|
143
|
-
if (this.config.extraHeaders) {
|
|
144
|
-
Object.assign(headers, this.config.extraHeaders);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Inject additional headers from custom builder (e.g., internal headers)
|
|
148
|
-
if (this._buildHeadersFn) {
|
|
149
|
-
const additionalHeaders = this._buildHeadersFn(this.config);
|
|
150
|
-
Object.assign(headers, additionalHeaders);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return headers;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Start a new sandbox instance
|
|
158
|
-
* @returns {Promise<Object>} Sandbox instance info
|
|
159
|
-
*/
|
|
160
|
-
async start() {
|
|
161
|
-
const url = `${this._url}/start_async`;
|
|
162
|
-
const headers = this._buildHeaders();
|
|
163
|
-
const data = {
|
|
164
|
-
image: this.config.image,
|
|
165
|
-
auto_clear_time: Math.floor(this.config.autoClearSeconds / 60),
|
|
166
|
-
auto_clear_time_minutes: Math.floor(this.config.autoClearSeconds / 60),
|
|
167
|
-
startup_timeout: this.config.startupTimeout,
|
|
168
|
-
memory: this.config.memory,
|
|
169
|
-
cpus: this.config.cpus,
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
logger.debug(`[Sandbox SDK] POST ${url}`);
|
|
173
|
-
logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
|
|
174
|
-
logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
|
|
175
|
-
|
|
176
|
-
const response = await this._axios.post(url, data, { headers });
|
|
177
|
-
logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
|
|
178
|
-
|
|
179
|
-
if (response.data.status !== 'Success') {
|
|
180
|
-
const error = response.data.result || response.data;
|
|
181
|
-
throw new Error(`Failed to start sandbox: ${JSON.stringify(error)}`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const result = response.data.result;
|
|
185
|
-
this._sandboxId = result.sandbox_id;
|
|
186
|
-
this._hostName = result.host_name;
|
|
187
|
-
this._hostIp = result.host_ip;
|
|
188
|
-
|
|
189
|
-
let isAlive = false;
|
|
190
|
-
if (this.config.waitForAlive) {
|
|
191
|
-
// Wait for sandbox to be alive
|
|
192
|
-
await this._waitForAlive();
|
|
193
|
-
isAlive = true;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
sandboxId: this._sandboxId,
|
|
198
|
-
hostName: this._hostName,
|
|
199
|
-
hostIp: this._hostIp,
|
|
200
|
-
isAlive: isAlive,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async _waitForAlive() {
|
|
205
|
-
const startTime = Date.now();
|
|
206
|
-
const timeout = this.config.startupTimeout * 1000;
|
|
207
|
-
let lastStatus = null;
|
|
208
|
-
let lastLogTime = 0;
|
|
209
|
-
const loadingChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
210
|
-
let loadingIndex = 0;
|
|
211
|
-
|
|
212
|
-
this._log('info', 'Waiting for sandbox to be ready...');
|
|
213
|
-
|
|
214
|
-
while (Date.now() - startTime < timeout) {
|
|
215
|
-
const status = await this.getStatus();
|
|
216
|
-
|
|
217
|
-
if (status.is_alive) {
|
|
218
|
-
this._log('info', `Sandbox is ready (ID: ${this._sandboxId})`);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Show progress for status changes
|
|
223
|
-
if (status.status && JSON.stringify(status.status) !== JSON.stringify(lastStatus)) {
|
|
224
|
-
lastStatus = status.status;
|
|
225
|
-
const stageInfo = Object.entries(status.status)
|
|
226
|
-
.map(([stage, details]) => {
|
|
227
|
-
const icon = details.status === 'success' ? '✓' :
|
|
228
|
-
details.status === 'failed' ? '✗' :
|
|
229
|
-
details.status === 'running' ? '⟳' : '○';
|
|
230
|
-
return `${stage}: ${icon}`;
|
|
231
|
-
})
|
|
232
|
-
.join(', ');
|
|
233
|
-
this._log('info', `Sandbox startup progress: ${stageInfo}`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Show loading animation every 3 seconds if no status change
|
|
237
|
-
const now = Date.now();
|
|
238
|
-
if (now - lastLogTime > 3000) {
|
|
239
|
-
loadingIndex = (loadingIndex + 1) % loadingChars.length;
|
|
240
|
-
const elapsed = Math.floor((now - startTime) / 1000);
|
|
241
|
-
this._log('info', `${loadingChars[loadingIndex]} Starting sandbox... (${elapsed}s elapsed)`);
|
|
242
|
-
lastLogTime = now;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Check for failures
|
|
246
|
-
if (status.status) {
|
|
247
|
-
for (const [stage, details] of Object.entries(status.status)) {
|
|
248
|
-
if (details.status === 'failed' || details.status === 'timeout') {
|
|
249
|
-
// Ignore ray_schedule failure during wait for alive
|
|
250
|
-
if (stage === 'ray_schedule') {
|
|
251
|
-
this._log('debug', `Ignoring ray_schedule failure during wait for alive: ${details.message || 'Unknown error'}`);
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
throw new Error(`Sandbox failed at ${stage}: ${details.message || 'Unknown error'}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
throw new Error(`Sandbox did not become alive within ${this.config.startupTimeout}s`);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Get sandbox status
|
|
267
|
-
* @returns {Promise<Object>} Sandbox status
|
|
268
|
-
*/
|
|
269
|
-
async getStatus() {
|
|
270
|
-
const url = `${this._url}/get_status?sandbox_id=${this._sandboxId}`;
|
|
271
|
-
const headers = this._buildHeaders();
|
|
272
|
-
|
|
273
|
-
logger.debug(`[Sandbox SDK] GET ${url}`);
|
|
274
|
-
logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
|
|
275
|
-
|
|
276
|
-
const response = await this._axios.get(url, { headers });
|
|
277
|
-
logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
|
|
278
|
-
logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
|
|
279
|
-
|
|
280
|
-
if (response.data.status !== 'Success') {
|
|
281
|
-
throw new Error(`Failed to get status: ${JSON.stringify(response.data)}`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const result = response.data.result;
|
|
285
|
-
// 从响应头中提取 cluster 和其他有用信息
|
|
286
|
-
result.cluster = response.headers['x-rock-gateway-target-cluster'] || 'N/A';
|
|
287
|
-
result.request_id = response.headers['x-request-id'] || response.headers['request-id'] || 'N/A';
|
|
288
|
-
result.eagleeye_traceid = response.headers['eagleeye-traceid'] || 'N/A';
|
|
289
|
-
|
|
290
|
-
return result;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Check if sandbox is alive
|
|
295
|
-
* @returns {Promise<Object>} IsAlive response
|
|
296
|
-
*/
|
|
297
|
-
async isAlive() {
|
|
298
|
-
try {
|
|
299
|
-
const status = await this.getStatus();
|
|
300
|
-
return {
|
|
301
|
-
isAlive: status.is_alive,
|
|
302
|
-
message: status.host_name || '',
|
|
303
|
-
};
|
|
304
|
-
} catch (error) {
|
|
305
|
-
logger.warn(`Failed to check isAlive: ${error.message}`);
|
|
306
|
-
throw new Error(`Failed to check isAlive: ${error.message}`);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Execute a command in the sandbox
|
|
312
|
-
* @param {string|Array} command - Command to execute
|
|
313
|
-
* @returns {Promise<Object>} Command response
|
|
314
|
-
*/
|
|
315
|
-
async execute(command) {
|
|
316
|
-
const url = `${this._url}/execute`;
|
|
317
|
-
const headers = this._buildHeaders();
|
|
318
|
-
|
|
319
|
-
let commandArray;
|
|
320
|
-
if (Array.isArray(command)) {
|
|
321
|
-
// Array command: use as-is
|
|
322
|
-
commandArray = command;
|
|
323
|
-
} else {
|
|
324
|
-
// String command: wrap with sh -c to support shell operators (&&, ||, ;, |, etc.)
|
|
325
|
-
commandArray = ['sh', '-c', command];
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const data = {
|
|
329
|
-
command: commandArray,
|
|
330
|
-
sandbox_id: this._sandboxId,
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
logger.debug(`[Sandbox SDK] POST ${url}`);
|
|
334
|
-
logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
|
|
335
|
-
|
|
336
|
-
const response = await this._axios.post(url, data, { headers });
|
|
337
|
-
logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
|
|
338
|
-
logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
|
|
339
|
-
|
|
340
|
-
if (response.data.status !== 'Success') {
|
|
341
|
-
throw new Error(`Failed to execute command: ${JSON.stringify(response.data)}`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return response.data.result;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Stop the sandbox
|
|
349
|
-
*/
|
|
350
|
-
async stop() {
|
|
351
|
-
if (!this._sandboxId) {
|
|
352
|
-
logger.warn('No sandbox ID to stop');
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
try {
|
|
357
|
-
const url = `${this._url}/stop`;
|
|
358
|
-
const headers = this._buildHeaders();
|
|
359
|
-
const data = {
|
|
360
|
-
sandbox_id: this._sandboxId,
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
logger.debug(`[Sandbox SDK] POST ${url}`);
|
|
364
|
-
logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
|
|
365
|
-
logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
|
|
366
|
-
|
|
367
|
-
const response = await this._axios.post(url, data, { headers });
|
|
368
|
-
logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
|
|
369
|
-
logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
|
|
370
|
-
logger.info(`Sandbox ${this._sandboxId} stopped successfully`);
|
|
371
|
-
} catch (error) {
|
|
372
|
-
logger.warn(`Failed to stop sandbox: ${error.message}`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Create a bash session
|
|
378
|
-
* @param {Object} options - Session options
|
|
379
|
-
* @returns {Promise<Object>} Create session response
|
|
380
|
-
*/
|
|
381
|
-
async createSession(options = {}) {
|
|
382
|
-
const url = `${this._url}/create_session`;
|
|
383
|
-
const headers = this._buildHeaders();
|
|
384
|
-
|
|
385
|
-
const data = {
|
|
386
|
-
sandbox_id: this._sandboxId,
|
|
387
|
-
session: options.session || 'default',
|
|
388
|
-
session_type: options.sessionType || 'bash',
|
|
389
|
-
startup_source: options.startupSource || [],
|
|
390
|
-
env_enable: options.envEnable || false,
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
if (options.env) {
|
|
394
|
-
data.env = options.env;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (options.startupTimeout !== undefined) {
|
|
398
|
-
data.startup_timeout = options.startupTimeout;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
logger.debug(`[Sandbox SDK] POST ${url}`);
|
|
402
|
-
logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
|
|
403
|
-
logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
|
|
404
|
-
|
|
405
|
-
const response = await this._axios.post(url, data, { headers });
|
|
406
|
-
logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
|
|
407
|
-
logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
|
|
408
|
-
|
|
409
|
-
if (response.data.status !== 'Success') {
|
|
410
|
-
throw new Error(`Failed to create session: ${JSON.stringify(response.data)}`);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return response.data.result;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Run a command in a session
|
|
418
|
-
* @param {Object} action - Action to execute
|
|
419
|
-
* @returns {Promise<Object>} Observation result
|
|
420
|
-
*/
|
|
421
|
-
async runInSession(action) {
|
|
422
|
-
const url = `${this._url}/run_in_session`;
|
|
423
|
-
const headers = this._buildHeaders();
|
|
424
|
-
|
|
425
|
-
const data = {
|
|
426
|
-
sandbox_id: this._sandboxId,
|
|
427
|
-
command: action.command,
|
|
428
|
-
session: action.session || 'default',
|
|
429
|
-
timeout: action.timeout || null,
|
|
430
|
-
is_interactive_command: action.isInteractiveCommand || false,
|
|
431
|
-
check: action.check || 'raise',
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
if (action.expect) {
|
|
435
|
-
data.expect = action.expect;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
logger.debug(`[Sandbox SDK] POST ${url}`);
|
|
439
|
-
logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
|
|
440
|
-
logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
|
|
441
|
-
|
|
442
|
-
const axiosConfig = {
|
|
443
|
-
headers,
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
// Set axios timeout if provided (in milliseconds)
|
|
447
|
-
// axiosTimeout takes precedence over timeout for HTTP layer
|
|
448
|
-
if (action.axiosTimeout) {
|
|
449
|
-
axiosConfig.timeout = action.axiosTimeout;
|
|
450
|
-
} else if (action.timeout) {
|
|
451
|
-
// If only backend timeout is set, also set axios timeout (timeout is in seconds)
|
|
452
|
-
axiosConfig.timeout = action.timeout * 1000;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Pass AbortSignal for cancellation support
|
|
456
|
-
if (action.signal) {
|
|
457
|
-
axiosConfig.signal = action.signal;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const response = await this._axios.post(url, data, axiosConfig);
|
|
461
|
-
logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
|
|
462
|
-
logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
|
|
463
|
-
|
|
464
|
-
if (response.data.status !== 'Success') {
|
|
465
|
-
throw new Error(`Failed to run in session: ${JSON.stringify(response.data)}`);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return response.data.result;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Write content to a file in the sandbox
|
|
473
|
-
* @param {string} content - Content to write
|
|
474
|
-
* @param {string} path - Target file path
|
|
475
|
-
* @returns {Promise<Object>} Write file response
|
|
476
|
-
*/
|
|
477
|
-
async writeFile(content, path) {
|
|
478
|
-
const url = `${this._url}/write_file`;
|
|
479
|
-
const headers = this._buildHeaders();
|
|
480
|
-
|
|
481
|
-
const data = {
|
|
482
|
-
content,
|
|
483
|
-
path,
|
|
484
|
-
sandbox_id: this._sandboxId,
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
logger.debug(`[Sandbox SDK] POST ${url}`);
|
|
488
|
-
logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
|
|
489
|
-
logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify({ ...data, content: content.length > 100 ? content.substring(0, 100) + '...' : content })}`);
|
|
490
|
-
|
|
491
|
-
const response = await this._axios.post(url, data, { headers });
|
|
492
|
-
logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
|
|
493
|
-
|
|
494
|
-
if (response.data.status !== 'Success') {
|
|
495
|
-
return {
|
|
496
|
-
success: false,
|
|
497
|
-
message: `Failed to write file ${path}: ${JSON.stringify(response.data)}`,
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return {
|
|
502
|
-
success: true,
|
|
503
|
-
message: `Successfully wrote content to file ${path}`,
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Read a file from the sandbox
|
|
509
|
-
* @param {string} filePath - File path to read
|
|
510
|
-
* @param {number} startLine - Start line (1-indexed)
|
|
511
|
-
* @param {number} endLine - End line (1-indexed)
|
|
512
|
-
* @returns {Promise<Object>} Read file response
|
|
513
|
-
*/
|
|
514
|
-
async readFile(filePath, startLine, endLine) {
|
|
515
|
-
if (startLine < 1 || endLine < startLine) {
|
|
516
|
-
throw new Error(`Invalid line range: startLine=${startLine}, endLine=${endLine}`);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (endLine - startLine > 1000) {
|
|
520
|
-
throw new Error(`Line range too large: ${endLine - startLine} lines (max 1000)`);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const command = `sed -n ${startLine},${endLine}p ${filePath}`;
|
|
524
|
-
const result = await this.execute(command);
|
|
525
|
-
|
|
526
|
-
if (result.exit_code !== 0) {
|
|
527
|
-
throw new Error(`Failed to read file ${filePath}: ${result.stderr}`);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return {
|
|
531
|
-
content: result.stdout,
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Upload a file to the sandbox
|
|
537
|
-
* @param {string} localPath - Local file path
|
|
538
|
-
* @param {string} targetPath - Target path in sandbox
|
|
539
|
-
* @returns {Promise<Object>} Upload response
|
|
540
|
-
*/
|
|
541
|
-
async uploadFile(localPath, targetPath) {
|
|
542
|
-
const fs = require('fs');
|
|
543
|
-
const path = require('path');
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
if (!fs.existsSync(localPath)) {
|
|
547
|
-
return {
|
|
548
|
-
success: false,
|
|
549
|
-
message: `File not found: ${localPath}`,
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const url = `${this._url}/upload`;
|
|
554
|
-
const headers = this._buildHeaders();
|
|
555
|
-
delete headers['Content-Type'];
|
|
556
|
-
|
|
557
|
-
// Use native FormData for bun compatibility
|
|
558
|
-
const form = new FormData();
|
|
559
|
-
const fileBuffer = fs.readFileSync(localPath);
|
|
560
|
-
const blob = new Blob([fileBuffer], { type: 'application/octet-stream' });
|
|
561
|
-
|
|
562
|
-
form.append('file', blob, path.basename(localPath));
|
|
563
|
-
form.append('target_path', targetPath);
|
|
564
|
-
form.append('sandbox_id', this._sandboxId);
|
|
565
|
-
form.append('container_name', this._sandboxId);
|
|
566
|
-
|
|
567
|
-
logger.debug(`[Sandbox SDK] POST ${url}`);
|
|
568
|
-
logger.debug(`[Sandbox SDK] Request body: target_path=${targetPath}, sandbox_id=${this._sandboxId}, container_name=${this._sandboxId}, file=${path.basename(localPath)}`);
|
|
569
|
-
logger.debug(`[Sandbox SDK] Upload file: ${localPath} -> ${targetPath}`);
|
|
570
|
-
|
|
571
|
-
const response = await fetch(url, {
|
|
572
|
-
method: 'POST',
|
|
573
|
-
headers,
|
|
574
|
-
body: form,
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
const data = await response.json();
|
|
578
|
-
logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(data)}`);
|
|
579
|
-
|
|
580
|
-
if (data.status !== 'Success') {
|
|
581
|
-
return {
|
|
582
|
-
success: false,
|
|
583
|
-
message: `Failed to upload file: ${JSON.stringify(data)}`,
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
return {
|
|
588
|
-
success: true,
|
|
589
|
-
message: `Successfully uploaded ${path.basename(localPath)} to ${targetPath}`,
|
|
590
|
-
};
|
|
591
|
-
} catch (error) {
|
|
592
|
-
logger.debug(`[Sandbox SDK] Upload error: ${error}`);
|
|
593
|
-
const errorMessage = error?.message || (typeof error === 'string' ? error : 'Unknown error');
|
|
594
|
-
return {
|
|
595
|
-
success: false,
|
|
596
|
-
message: `Upload failed: ${errorMessage}`,
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* Download a file from the sandbox
|
|
603
|
-
* @param {string} filePath - File path in sandbox
|
|
604
|
-
* @returns {Promise<Object>} Download response
|
|
605
|
-
*/
|
|
606
|
-
async downloadFile(filePath) {
|
|
607
|
-
const url = `${this._url}/read_file`;
|
|
608
|
-
const headers = this._buildHeaders();
|
|
609
|
-
|
|
610
|
-
const data = {
|
|
611
|
-
path: filePath,
|
|
612
|
-
sandbox_id: this._sandboxId,
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
logger.debug(`[Sandbox SDK] POST ${url}`);
|
|
616
|
-
logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
|
|
617
|
-
|
|
618
|
-
const response = await this._axios.post(url, data, { headers });
|
|
619
|
-
logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
|
|
620
|
-
|
|
621
|
-
if (response.data.status !== 'Success') {
|
|
622
|
-
throw new Error(`Failed to download file: ${JSON.stringify(response.data)}`);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
return response.data.result;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Make a generic HTTP request to the sandbox API
|
|
630
|
-
* @param {Object} request - Request object
|
|
631
|
-
* @param {string} request.method - HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
632
|
-
* @param {string} request.uri - Request URI path
|
|
633
|
-
* @param {Object} request.requestBody - Request body data
|
|
634
|
-
* @param {Object} request.headers - Request headers
|
|
635
|
-
* @returns {Promise<Object>} Response data
|
|
636
|
-
*/
|
|
637
|
-
async makeRequest(request) {
|
|
638
|
-
const { method, uri, requestBody, headers } = request;
|
|
639
|
-
const url = `${this.config.baseUrl}${uri}`;
|
|
640
|
-
const requestHeaders = this._buildHeaders();
|
|
641
|
-
|
|
642
|
-
// Merge custom headers
|
|
643
|
-
if (headers) {
|
|
644
|
-
Object.assign(requestHeaders, headers);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
logger.debug(`[Sandbox SDK] ${method} ${url}`);
|
|
648
|
-
logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(requestHeaders)}`);
|
|
649
|
-
if (requestBody) {
|
|
650
|
-
logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(requestBody)}`);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const axiosConfig = {
|
|
654
|
-
method: method.toLowerCase(),
|
|
655
|
-
url,
|
|
656
|
-
headers: requestHeaders,
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
if (requestBody) {
|
|
660
|
-
axiosConfig.data = requestBody;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const response = await axios(axiosConfig);
|
|
664
|
-
logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
|
|
665
|
-
|
|
666
|
-
return {
|
|
667
|
-
data: response.data,
|
|
668
|
-
headers: response.headers,
|
|
669
|
-
status: response.status
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Close a session
|
|
675
|
-
* @param {string} session - Session name (default: 'default')
|
|
676
|
-
* @returns {Promise<Object>} Close session response
|
|
677
|
-
*/
|
|
678
|
-
async closeSession(session = 'default') {
|
|
679
|
-
const url = `${this._url}/close_session`;
|
|
680
|
-
const headers = this._buildHeaders();
|
|
681
|
-
|
|
682
|
-
const data = {
|
|
683
|
-
sandbox_id: this._sandboxId,
|
|
684
|
-
session: session,
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
logger.debug(`[Sandbox SDK] POST ${url}`);
|
|
688
|
-
logger.debug(`[Sandbox SDK] Request headers: ${JSON.stringify(headers)}`);
|
|
689
|
-
logger.debug(`[Sandbox SDK] Request body: ${JSON.stringify(data)}`);
|
|
690
|
-
|
|
691
|
-
const response = await this._axios.post(url, data, { headers });
|
|
692
|
-
logger.debug(`[Sandbox SDK] Response headers: ${JSON.stringify(response.headers)}`);
|
|
693
|
-
logger.debug(`[Sandbox SDK] Response: ${JSON.stringify(response.data)}`);
|
|
694
|
-
|
|
695
|
-
if (response.data.status !== 'Success') {
|
|
696
|
-
throw new Error(`Failed to close session: ${JSON.stringify(response.data)}`);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
return response.data.result;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Run a command asynchronously in a session using nohup + kill -0 polling
|
|
704
|
-
* @param {Object} action - Action configuration
|
|
705
|
-
* @param {string} action.session - Session name
|
|
706
|
-
* @param {string} action.command - Command to execute
|
|
707
|
-
* @param {number} [action.timeout] - Total timeout in milliseconds
|
|
708
|
-
* @param {number} [action.pollInterval] - Polling interval in milliseconds (default: 5000)
|
|
709
|
-
* @param {boolean} [action.killOnTimeout] - Whether to kill process on timeout (default: false)
|
|
710
|
-
* @param {boolean} [action.cleanupLog] - Whether to cleanup log file (default: false)
|
|
711
|
-
* @returns {Promise<Object>} Result with output, exit_code, and optional timeout/warning
|
|
712
|
-
*/
|
|
713
|
-
async runInSessionAsync(action) {
|
|
714
|
-
const session = action.session || 'default';
|
|
715
|
-
const timeoutMs = action.timeout || 3600000; // Default 1 hour
|
|
716
|
-
const pollInterval = action.pollInterval || 5000;
|
|
717
|
-
const killOnTimeout = action.killOnTimeout || false;
|
|
718
|
-
const cleanupLog = action.cleanupLog || false;
|
|
719
|
-
|
|
720
|
-
// Generate unique log file
|
|
721
|
-
const logFile = `/tmp/arun-${Date.now()}.log`;
|
|
722
|
-
|
|
723
|
-
// Build nohup command with PID marker
|
|
724
|
-
const nohupCommand = `nohup ${action.command} < /dev/null > ${logFile} 2>&1 & echo PIDSTART$!PIDEND;disown`;
|
|
725
|
-
|
|
726
|
-
logger.debug(`[Sandbox SDK] Starting async command: ${action.command}`);
|
|
727
|
-
|
|
728
|
-
// 1. Execute nohup command
|
|
729
|
-
const startResult = await this.runInSession({
|
|
730
|
-
session,
|
|
731
|
-
command: nohupCommand
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
// 2. Extract PID
|
|
735
|
-
const pidMatch = startResult.output.match(/PIDSTART(\d+)PIDEND/);
|
|
736
|
-
if (!pidMatch) {
|
|
737
|
-
throw new Error('Failed to extract PID from nohup output');
|
|
738
|
-
}
|
|
739
|
-
const pid = pidMatch[1];
|
|
740
|
-
logger.debug(`[Sandbox SDK] Started process with PID: ${pid}`);
|
|
741
|
-
|
|
742
|
-
// 3. Poll for process completion
|
|
743
|
-
const startTime = Date.now();
|
|
744
|
-
let pollCount = 0;
|
|
745
|
-
const maxPolls = Math.ceil(timeoutMs / pollInterval);
|
|
746
|
-
|
|
747
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
748
|
-
pollCount++;
|
|
749
|
-
logger.debug(`[Sandbox SDK] [${pollCount}/${maxPolls}] Checking process ${pid}...`);
|
|
750
|
-
console.log(`[${pollCount}/${maxPolls}] Waiting for process ${pid}...`);
|
|
751
|
-
|
|
752
|
-
// Check if process is still running (kill -0)
|
|
753
|
-
const checkResult = await this.runInSession({
|
|
754
|
-
session,
|
|
755
|
-
command: `kill -0 ${pid}`,
|
|
756
|
-
check: 'silent' // Don't throw on non-zero exit
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
if (checkResult.exit_code !== 0) {
|
|
760
|
-
// Process has finished
|
|
761
|
-
logger.debug(`[Sandbox SDK] Process ${pid} finished`);
|
|
762
|
-
console.log(`Process ${pid} finished`);
|
|
763
|
-
break;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Wait before next poll
|
|
767
|
-
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// 4. Check for timeout
|
|
771
|
-
const elapsed = Date.now() - startTime;
|
|
772
|
-
let timedOut = false;
|
|
773
|
-
|
|
774
|
-
if (elapsed >= timeoutMs) {
|
|
775
|
-
timedOut = true;
|
|
776
|
-
logger.warn(`[Sandbox SDK] Process ${pid} timed out after ${elapsed}ms`);
|
|
777
|
-
console.log(`⚠️ Process ${pid} timed out after ${elapsed}ms`);
|
|
778
|
-
|
|
779
|
-
if (killOnTimeout) {
|
|
780
|
-
logger.debug(`[Sandbox SDK] Killing process ${pid}`);
|
|
781
|
-
await this.runInSession({
|
|
782
|
-
session,
|
|
783
|
-
command: `kill ${pid}`,
|
|
784
|
-
check: 'silent'
|
|
785
|
-
});
|
|
786
|
-
// Wait a moment for process to terminate
|
|
787
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// 5. Read output from log file
|
|
792
|
-
const outputResult = await this.runInSession({
|
|
793
|
-
session,
|
|
794
|
-
command: `cat ${logFile} 2>/dev/null; echo "EXITCODE:$?"`
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
// Parse exit code from output
|
|
798
|
-
let output = outputResult.output || '';
|
|
799
|
-
let exitCode = 0;
|
|
800
|
-
|
|
801
|
-
// If process was killed due to timeout, exit code is 137 (SIGKILL)
|
|
802
|
-
if (timedOut && killOnTimeout) {
|
|
803
|
-
exitCode = 137;
|
|
804
|
-
} else {
|
|
805
|
-
const exitCodeMatch = output.match(/EXITCODE:(\d+)/);
|
|
806
|
-
if (exitCodeMatch) {
|
|
807
|
-
exitCode = parseInt(exitCodeMatch[1], 10);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
output = output.replace(/EXITCODE:\d+\n?$/, '');
|
|
812
|
-
|
|
813
|
-
// 6. Cleanup log file if requested
|
|
814
|
-
if (cleanupLog) {
|
|
815
|
-
await this.runInSession({
|
|
816
|
-
session,
|
|
817
|
-
command: `rm -f ${logFile}`,
|
|
818
|
-
check: 'silent'
|
|
819
|
-
});
|
|
820
|
-
logger.debug(`[Sandbox SDK] Cleaned up log file: ${logFile}`);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// Build result
|
|
824
|
-
const result = {
|
|
825
|
-
output: output.trim(),
|
|
826
|
-
exit_code: exitCode
|
|
827
|
-
};
|
|
828
|
-
|
|
829
|
-
if (timedOut) {
|
|
830
|
-
result.timeout = true;
|
|
831
|
-
result.warning = `Process ${pid} timed out after ${timeoutMs}ms`;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
return result;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
/**
|
|
838
|
-
* Close the sandbox
|
|
839
|
-
*/
|
|
840
|
-
async close() {
|
|
841
|
-
await this.stop();
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
module.exports = { SandboxClient };
|