@cloudflare/sandbox 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,465 @@
1
+ /**
2
+ * OpenAI Agents adapters for executing shell commands and file operations
3
+ * inside a Cloudflare Sandbox.
4
+ */
5
+ import {
6
+ type ApplyPatchOperation,
7
+ type ApplyPatchResult,
8
+ applyDiff,
9
+ type Editor as OpenAIEeditor,
10
+ type Shell as OpenAIShell,
11
+ type ShellAction,
12
+ type ShellOutputResult,
13
+ type ShellResult
14
+ } from '@openai/agents';
15
+
16
+ // Command result for API responses
17
+ export interface CommandResult {
18
+ command: string;
19
+ stdout: string;
20
+ stderr: string;
21
+ exitCode: number | null;
22
+ timestamp: number;
23
+ }
24
+
25
+ // File operation result for API responses
26
+ export interface FileOperationResult {
27
+ operation: 'create' | 'update' | 'delete';
28
+ path: string;
29
+ status: 'completed' | 'failed';
30
+ output: string;
31
+ error?: string;
32
+ timestamp: number;
33
+ }
34
+
35
+ import { createLogger, type Logger } from '@repo/shared';
36
+ import type { Sandbox } from '../sandbox';
37
+
38
+ // Helper functions for error handling
39
+ function isErrorWithProperties(error: unknown): error is {
40
+ message?: string;
41
+ exitCode?: number;
42
+ stdout?: string;
43
+ stderr?: string;
44
+ status?: number;
45
+ stack?: string;
46
+ } {
47
+ return typeof error === 'object' && error !== null;
48
+ }
49
+
50
+ function getErrorMessage(error: unknown): string {
51
+ if (isErrorWithProperties(error) && typeof error.message === 'string') {
52
+ return error.message;
53
+ }
54
+ return String(error);
55
+ }
56
+
57
+ /**
58
+ * Convert unknown values to Error instances when possible so downstream
59
+ * loggers can include stack traces without losing type safety.
60
+ */
61
+ function toError(error: unknown): Error | undefined {
62
+ return error instanceof Error ? error : undefined;
63
+ }
64
+
65
+ /**
66
+ * Shell implementation that adapts Cloudflare Sandbox exec calls to the
67
+ * OpenAI Agents `Shell` contract, including structured result collection.
68
+ */
69
+ export class Shell implements OpenAIShell {
70
+ private cwd: string = '/workspace';
71
+ public results: CommandResult[] = [];
72
+ private readonly logger: Logger;
73
+
74
+ constructor(private readonly sandbox: Sandbox) {
75
+ this.logger = createLogger({
76
+ component: 'sandbox-do',
77
+ operation: 'openai-shell'
78
+ });
79
+ }
80
+
81
+ async run(action: ShellAction): Promise<ShellResult> {
82
+ this.logger.debug('SandboxShell.run called', {
83
+ commands: action.commands,
84
+ timeout: action.timeoutMs
85
+ });
86
+ const output: ShellResult['output'] = [];
87
+
88
+ for (const command of action.commands) {
89
+ this.logger.debug('Executing command', { command, cwd: this.cwd });
90
+ let stdout = '';
91
+ let stderr = '';
92
+ let exitCode: number | null = 0;
93
+ let outcome: ShellOutputResult['outcome'] = {
94
+ type: 'exit',
95
+ exitCode: 0
96
+ };
97
+ try {
98
+ const result = await this.sandbox.exec(command, {
99
+ timeout: action.timeoutMs,
100
+ cwd: this.cwd
101
+ });
102
+ stdout = result.stdout;
103
+ stderr = result.stderr;
104
+ exitCode = result.exitCode;
105
+ // exec returns a result even for failed commands, so check success field
106
+ // Timeout would be indicated by a specific error or exit code
107
+ outcome = { type: 'exit', exitCode };
108
+
109
+ this.logger.debug('Command executed successfully', {
110
+ command,
111
+ exitCode,
112
+ stdoutLength: stdout.length,
113
+ stderrLength: stderr.length
114
+ });
115
+
116
+ // Log warnings for non-zero exit codes or stderr output
117
+ if (exitCode !== 0) {
118
+ this.logger.warn(`Command failed with exit code ${exitCode}`, {
119
+ command,
120
+ stderr
121
+ });
122
+ } else if (stderr) {
123
+ this.logger.warn(`Command produced stderr output`, {
124
+ command,
125
+ stderr
126
+ });
127
+ } else {
128
+ this.logger.info(`Command completed successfully`, { command });
129
+ }
130
+ } catch (error: unknown) {
131
+ // Handle network/HTTP errors or timeout errors
132
+ const errorObj = isErrorWithProperties(error) ? error : {};
133
+ exitCode =
134
+ typeof errorObj.exitCode === 'number' ? errorObj.exitCode : null;
135
+ stdout = typeof errorObj.stdout === 'string' ? errorObj.stdout : '';
136
+ stderr = typeof errorObj.stderr === 'string' ? errorObj.stderr : '';
137
+
138
+ // Check if it's a timeout error
139
+ const errorMessage = getErrorMessage(error);
140
+ if (
141
+ errorMessage.includes('timeout') ||
142
+ errorMessage.includes('Timeout') ||
143
+ errorMessage.includes('timed out')
144
+ ) {
145
+ this.logger.error(`Command timed out`, undefined, {
146
+ command,
147
+ timeout: action.timeoutMs
148
+ });
149
+ outcome = { type: 'timeout' };
150
+ } else {
151
+ this.logger.error(`Error executing command`, toError(error), {
152
+ command,
153
+ error: errorMessage || error,
154
+ exitCode
155
+ });
156
+ outcome = { type: 'exit', exitCode: exitCode ?? 1 };
157
+ }
158
+ }
159
+ output.push({
160
+ command,
161
+ stdout,
162
+ stderr,
163
+ outcome
164
+ });
165
+
166
+ // Collect results for API responses
167
+ const collectedExitCode =
168
+ outcome.type === 'exit' ? outcome.exitCode : null;
169
+ const timestamp = Date.now();
170
+ this.results.push({
171
+ command: String(command),
172
+ stdout: String(stdout),
173
+ stderr: String(stderr),
174
+ exitCode: collectedExitCode,
175
+ timestamp
176
+ });
177
+ this.logger.debug('Result collected', {
178
+ command,
179
+ exitCode: collectedExitCode,
180
+ timestamp
181
+ });
182
+
183
+ if (outcome.type === 'timeout') {
184
+ this.logger.warn('Breaking command loop due to timeout');
185
+ break;
186
+ }
187
+ }
188
+
189
+ this.logger.debug('SandboxShell.run completed', {
190
+ totalCommands: action.commands.length,
191
+ resultsCount: this.results.length
192
+ });
193
+ return {
194
+ output,
195
+ providerData: {
196
+ working_directory: this.cwd
197
+ }
198
+ };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Editor implementation that projects applyPatch operations from Agents
204
+ * into calls against the sandbox filesystem APIs.
205
+ */
206
+ export class Editor implements OpenAIEeditor {
207
+ public results: FileOperationResult[] = [];
208
+ private readonly logger: Logger;
209
+
210
+ constructor(
211
+ private readonly sandbox: Sandbox,
212
+ private readonly root: string = '/workspace'
213
+ ) {
214
+ this.logger = createLogger({
215
+ component: 'sandbox-do',
216
+ operation: 'openai-editor'
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Create a new file inside the sandbox by applying the provided diff.
222
+ */
223
+ async createFile(
224
+ operation: Extract<ApplyPatchOperation, { type: 'create_file' }>
225
+ ): Promise<ApplyPatchResult | undefined> {
226
+ const targetPath = this.resolve(operation.path);
227
+ this.logger.debug('WorkspaceEditor.createFile called', {
228
+ path: operation.path,
229
+ targetPath
230
+ });
231
+
232
+ try {
233
+ // Create parent directory if needed
234
+ const dirPath = this.getDirname(targetPath);
235
+ if (dirPath !== this.root && dirPath !== '/') {
236
+ this.logger.debug('Creating parent directory', { dirPath });
237
+ await this.sandbox.mkdir(dirPath, { recursive: true });
238
+ }
239
+
240
+ const content = applyDiff('', operation.diff, 'create');
241
+ this.logger.debug('Writing file content', {
242
+ path: targetPath,
243
+ contentLength: content.length
244
+ });
245
+ await this.sandbox.writeFile(targetPath, content, { encoding: 'utf-8' });
246
+ const timestamp = Date.now();
247
+ const result: FileOperationResult = {
248
+ operation: 'create',
249
+ path: operation.path,
250
+ status: 'completed',
251
+ output: `Created ${operation.path}`,
252
+ timestamp
253
+ };
254
+ this.results.push(result);
255
+ this.logger.info('File created successfully', {
256
+ path: operation.path,
257
+ timestamp
258
+ });
259
+ return { status: 'completed', output: `Created ${operation.path}` };
260
+ } catch (error: unknown) {
261
+ const timestamp = Date.now();
262
+ const errorMessage = getErrorMessage(error);
263
+ const result: FileOperationResult = {
264
+ operation: 'create',
265
+ path: operation.path,
266
+ status: 'failed',
267
+ output: `Failed to create ${operation.path}`,
268
+ error: errorMessage,
269
+ timestamp
270
+ };
271
+ this.results.push(result);
272
+ this.logger.error('Failed to create file', toError(error), {
273
+ path: operation.path,
274
+ error: errorMessage
275
+ });
276
+ throw error;
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Update an existing file by reading its content, applying a diff, and
282
+ * writing the patched output back to the sandbox.
283
+ */
284
+ async updateFile(
285
+ operation: Extract<ApplyPatchOperation, { type: 'update_file' }>
286
+ ): Promise<ApplyPatchResult | undefined> {
287
+ const targetPath = this.resolve(operation.path);
288
+ this.logger.debug('WorkspaceEditor.updateFile called', {
289
+ path: operation.path,
290
+ targetPath
291
+ });
292
+
293
+ try {
294
+ let original: string;
295
+ try {
296
+ this.logger.debug('Reading original file', { path: targetPath });
297
+ const fileInfo = await this.sandbox.readFile(targetPath, {
298
+ encoding: 'utf-8'
299
+ });
300
+ original = fileInfo.content;
301
+ this.logger.debug('Original file read', {
302
+ path: targetPath,
303
+ originalLength: original.length
304
+ });
305
+ } catch (error: unknown) {
306
+ // Sandbox API may throw errors for missing files
307
+ const errorObj = isErrorWithProperties(error) ? error : {};
308
+ const errorMessage = getErrorMessage(error);
309
+ if (
310
+ errorMessage.includes('not found') ||
311
+ errorMessage.includes('ENOENT') ||
312
+ errorObj.status === 404
313
+ ) {
314
+ this.logger.error('Cannot update missing file', undefined, {
315
+ path: operation.path
316
+ });
317
+ throw new Error(`Cannot update missing file: ${operation.path}`);
318
+ }
319
+ this.logger.error('Error reading file', toError(error), {
320
+ path: operation.path,
321
+ error: errorMessage
322
+ });
323
+ throw error;
324
+ }
325
+
326
+ const patched = applyDiff(original, operation.diff);
327
+ this.logger.debug('Applied diff', {
328
+ path: targetPath,
329
+ originalLength: original.length,
330
+ patchedLength: patched.length
331
+ });
332
+ await this.sandbox.writeFile(targetPath, patched, { encoding: 'utf-8' });
333
+ const timestamp = Date.now();
334
+ const result: FileOperationResult = {
335
+ operation: 'update',
336
+ path: operation.path,
337
+ status: 'completed',
338
+ output: `Updated ${operation.path}`,
339
+ timestamp
340
+ };
341
+ this.results.push(result);
342
+ this.logger.info('File updated successfully', {
343
+ path: operation.path,
344
+ timestamp
345
+ });
346
+ return { status: 'completed', output: `Updated ${operation.path}` };
347
+ } catch (error: unknown) {
348
+ const timestamp = Date.now();
349
+ const errorMessage = getErrorMessage(error);
350
+ const result: FileOperationResult = {
351
+ operation: 'update',
352
+ path: operation.path,
353
+ status: 'failed',
354
+ output: `Failed to update ${operation.path}`,
355
+ error: errorMessage,
356
+ timestamp
357
+ };
358
+ this.results.push(result);
359
+ this.logger.error('Failed to update file', toError(error), {
360
+ path: operation.path,
361
+ error: errorMessage
362
+ });
363
+ throw error;
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Delete a file that was previously created through applyPatch calls.
369
+ */
370
+ async deleteFile(
371
+ operation: Extract<ApplyPatchOperation, { type: 'delete_file' }>
372
+ ): Promise<ApplyPatchResult | undefined> {
373
+ const targetPath = this.resolve(operation.path);
374
+ this.logger.debug('WorkspaceEditor.deleteFile called', {
375
+ path: operation.path,
376
+ targetPath
377
+ });
378
+
379
+ try {
380
+ await this.sandbox.deleteFile(targetPath);
381
+ const timestamp = Date.now();
382
+ const result: FileOperationResult = {
383
+ operation: 'delete',
384
+ path: operation.path,
385
+ status: 'completed',
386
+ output: `Deleted ${operation.path}`,
387
+ timestamp
388
+ };
389
+ this.results.push(result);
390
+ this.logger.info('File deleted successfully', {
391
+ path: operation.path,
392
+ timestamp
393
+ });
394
+ return { status: 'completed', output: `Deleted ${operation.path}` };
395
+ } catch (error: unknown) {
396
+ const timestamp = Date.now();
397
+ const errorMessage = getErrorMessage(error);
398
+ const result: FileOperationResult = {
399
+ operation: 'delete',
400
+ path: operation.path,
401
+ status: 'failed',
402
+ output: `Failed to delete ${operation.path}`,
403
+ error: errorMessage,
404
+ timestamp
405
+ };
406
+ this.results.push(result);
407
+ this.logger.error('Failed to delete file', toError(error), {
408
+ path: operation.path,
409
+ error: errorMessage
410
+ });
411
+ throw error;
412
+ }
413
+ }
414
+
415
+ private resolve(relativePath: string): string {
416
+ // If the path already starts with the root, strip it to get the relative part
417
+ let pathToProcess = relativePath;
418
+ if (relativePath.startsWith(this.root)) {
419
+ pathToProcess = relativePath.slice(this.root.length);
420
+ // Remove leading slash if present after stripping root
421
+ pathToProcess = pathToProcess.replace(/^\//, '');
422
+ }
423
+
424
+ // Remove leading ./ or / if present, then join with root
425
+ const normalized = pathToProcess.replace(/^\.\//, '').replace(/^\//, '');
426
+ const resolved = normalized ? `${this.root}/${normalized}` : this.root;
427
+
428
+ // Normalize path separators first
429
+ const pathWithNormalizedSeparators = resolved.replace(/\/+/g, '/');
430
+
431
+ // Normalize .. segments by processing path segments
432
+ const segments = pathWithNormalizedSeparators
433
+ .split('/')
434
+ .filter((s) => s && s !== '.');
435
+ const stack: string[] = [];
436
+
437
+ for (const segment of segments) {
438
+ if (segment === '..') {
439
+ if (stack.length === 0) {
440
+ throw new Error(`Operation outside workspace: ${relativePath}`);
441
+ }
442
+ stack.pop();
443
+ } else {
444
+ stack.push(segment);
445
+ }
446
+ }
447
+
448
+ const normalizedPath = `/${stack.join('/')}`;
449
+
450
+ // Ensure the resolved path is within the workspace
451
+ if (!normalizedPath.startsWith(this.root)) {
452
+ throw new Error(`Operation outside workspace: ${relativePath}`);
453
+ }
454
+
455
+ return normalizedPath;
456
+ }
457
+
458
+ private getDirname(filePath: string): string {
459
+ const lastSlash = filePath.lastIndexOf('/');
460
+ if (lastSlash === -1) {
461
+ return '/';
462
+ }
463
+ return filePath.substring(0, lastSlash) || '/';
464
+ }
465
+ }
package/src/sandbox.ts CHANGED
@@ -22,7 +22,6 @@ import type {
22
22
  import {
23
23
  createLogger,
24
24
  getEnvString,
25
- runWithLogger,
26
25
  type SessionDeleteResult,
27
26
  shellEscape,
28
27
  TraceContext
@@ -757,8 +756,20 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
757
756
  }
758
757
  }
759
758
 
760
- override onStop() {
759
+ override async onStop() {
761
760
  this.logger.debug('Sandbox stopped');
761
+
762
+ // Clear in-memory state that references the old container
763
+ // This prevents stale references after container restarts
764
+ this.portTokens.clear();
765
+ this.defaultSession = null;
766
+ this.activeMounts.clear();
767
+
768
+ // Persist cleanup to storage so state is clean on next container start
769
+ await Promise.all([
770
+ this.ctx.storage.delete('portTokens'),
771
+ this.ctx.storage.delete('defaultSession')
772
+ ]);
762
773
  }
763
774
 
764
775
  override onError(error: unknown) {
@@ -905,48 +916,46 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
905
916
  // Create request-specific logger with trace ID
906
917
  const requestLogger = this.logger.child({ traceId, operation: 'fetch' });
907
918
 
908
- return await runWithLogger(requestLogger, async () => {
909
- const url = new URL(request.url);
919
+ const url = new URL(request.url);
910
920
 
911
- // Capture and store the sandbox name from the header if present
912
- if (!this.sandboxName && request.headers.has('X-Sandbox-Name')) {
913
- const name = request.headers.get('X-Sandbox-Name')!;
914
- this.sandboxName = name;
915
- await this.ctx.storage.put('sandboxName', name);
916
- }
921
+ // Capture and store the sandbox name from the header if present
922
+ if (!this.sandboxName && request.headers.has('X-Sandbox-Name')) {
923
+ const name = request.headers.get('X-Sandbox-Name')!;
924
+ this.sandboxName = name;
925
+ await this.ctx.storage.put('sandboxName', name);
926
+ }
917
927
 
918
- // Detect WebSocket upgrade request (RFC 6455 compliant)
919
- const upgradeHeader = request.headers.get('Upgrade');
920
- const connectionHeader = request.headers.get('Connection');
921
- const isWebSocket =
922
- upgradeHeader?.toLowerCase() === 'websocket' &&
923
- connectionHeader?.toLowerCase().includes('upgrade');
928
+ // Detect WebSocket upgrade request (RFC 6455 compliant)
929
+ const upgradeHeader = request.headers.get('Upgrade');
930
+ const connectionHeader = request.headers.get('Connection');
931
+ const isWebSocket =
932
+ upgradeHeader?.toLowerCase() === 'websocket' &&
933
+ connectionHeader?.toLowerCase().includes('upgrade');
924
934
 
925
- if (isWebSocket) {
926
- // WebSocket path: Let parent Container class handle WebSocket proxying
927
- // This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
928
- try {
929
- requestLogger.debug('WebSocket upgrade requested', {
930
- path: url.pathname,
931
- port: this.determinePort(url)
932
- });
933
- return await super.fetch(request);
934
- } catch (error) {
935
- requestLogger.error(
936
- 'WebSocket connection failed',
937
- error instanceof Error ? error : new Error(String(error)),
938
- { path: url.pathname }
939
- );
940
- throw error;
941
- }
935
+ if (isWebSocket) {
936
+ // WebSocket path: Let parent Container class handle WebSocket proxying
937
+ // This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
938
+ try {
939
+ requestLogger.debug('WebSocket upgrade requested', {
940
+ path: url.pathname,
941
+ port: this.determinePort(url)
942
+ });
943
+ return await super.fetch(request);
944
+ } catch (error) {
945
+ requestLogger.error(
946
+ 'WebSocket connection failed',
947
+ error instanceof Error ? error : new Error(String(error)),
948
+ { path: url.pathname }
949
+ );
950
+ throw error;
942
951
  }
952
+ }
943
953
 
944
- // Non-WebSocket: Use existing port determination and HTTP routing logic
945
- const port = this.determinePort(url);
954
+ // Non-WebSocket: Use existing port determination and HTTP routing logic
955
+ const port = this.determinePort(url);
946
956
 
947
- // Route to the appropriate port
948
- return await this.containerFetch(request, port);
949
- });
957
+ // Route to the appropriate port
958
+ return await this.containerFetch(request, port);
950
959
  }
951
960
 
952
961
  wsConnect(request: Request, port: number): Promise<Response> {
@@ -1051,7 +1060,23 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
1051
1060
  );
1052
1061
  } else {
1053
1062
  // Regular execution with session
1054
- const response = await this.client.commands.execute(command, sessionId);
1063
+ const commandOptions =
1064
+ options &&
1065
+ (options.timeout !== undefined ||
1066
+ options.env !== undefined ||
1067
+ options.cwd !== undefined)
1068
+ ? {
1069
+ timeoutMs: options.timeout,
1070
+ env: options.env,
1071
+ cwd: options.cwd
1072
+ }
1073
+ : undefined;
1074
+
1075
+ const response = await this.client.commands.execute(
1076
+ command,
1077
+ sessionId,
1078
+ commandOptions
1079
+ );
1055
1080
 
1056
1081
  const duration = Date.now() - startTime;
1057
1082
  result = this.mapExecuteResponseToExecResult(
@@ -1092,7 +1117,12 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
1092
1117
  try {
1093
1118
  const stream = await this.client.commands.executeStream(
1094
1119
  command,
1095
- sessionId
1120
+ sessionId,
1121
+ {
1122
+ timeoutMs: options.timeout,
1123
+ env: options.env,
1124
+ cwd: options.cwd
1125
+ }
1096
1126
  );
1097
1127
 
1098
1128
  for await (const event of parseSSEStream<ExecEvent>(stream)) {
@@ -1222,12 +1252,23 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
1222
1252
  // Use the new HttpClient method to start the process
1223
1253
  try {
1224
1254
  const session = sessionId ?? (await this.ensureDefaultSession());
1255
+ const requestOptions = {
1256
+ ...(options?.processId !== undefined && {
1257
+ processId: options.processId
1258
+ }),
1259
+ ...(options?.timeout !== undefined && { timeoutMs: options.timeout }),
1260
+ ...(options?.env !== undefined && { env: options.env }),
1261
+ ...(options?.cwd !== undefined && { cwd: options.cwd }),
1262
+ ...(options?.encoding !== undefined && { encoding: options.encoding }),
1263
+ ...(options?.autoCleanup !== undefined && {
1264
+ autoCleanup: options.autoCleanup
1265
+ })
1266
+ };
1267
+
1225
1268
  const response = await this.client.processes.startProcess(
1226
1269
  command,
1227
1270
  session,
1228
- {
1229
- processId: options?.processId
1230
- }
1271
+ requestOptions
1231
1272
  );
1232
1273
 
1233
1274
  const processObj = this.createProcessFromDTO(
@@ -1347,7 +1388,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
1347
1388
 
1348
1389
  const session = await this.ensureDefaultSession();
1349
1390
  // Get the stream from CommandClient
1350
- return this.client.commands.executeStream(command, session);
1391
+ return this.client.commands.executeStream(command, session, {
1392
+ timeoutMs: options?.timeout,
1393
+ env: options?.env,
1394
+ cwd: options?.cwd
1395
+ });
1351
1396
  }
1352
1397
 
1353
1398
  /**
@@ -1363,7 +1408,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
1363
1408
  throw new Error('Operation was aborted');
1364
1409
  }
1365
1410
 
1366
- return this.client.commands.executeStream(command, sessionId);
1411
+ return this.client.commands.executeStream(command, sessionId, {
1412
+ timeoutMs: options?.timeout,
1413
+ env: options?.env,
1414
+ cwd: options?.cwd
1415
+ });
1367
1416
  }
1368
1417
 
1369
1418
  /**
@@ -1697,11 +1746,18 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
1697
1746
  async createSession(options?: SessionOptions): Promise<ExecutionSession> {
1698
1747
  const sessionId = options?.id || `session-${Date.now()}`;
1699
1748
 
1749
+ const mergedEnv = {
1750
+ ...this.envVars,
1751
+ ...(options?.env ?? {})
1752
+ };
1753
+ const envPayload =
1754
+ Object.keys(mergedEnv).length > 0 ? mergedEnv : undefined;
1755
+
1700
1756
  // Create session in container
1701
1757
  await this.client.utils.createSession({
1702
1758
  id: sessionId,
1703
- env: options?.env,
1704
- cwd: options?.cwd
1759
+ ...(envPayload && { env: envPayload }),
1760
+ ...(options?.cwd && { cwd: options.cwd })
1705
1761
  });
1706
1762
 
1707
1763
  // Return wrapper that binds sessionId to all operations
package/src/version.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  * This file is auto-updated by .github/changeset-version.ts during releases
4
4
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
5
5
  */
6
- export const SDK_VERSION = '0.5.1';
6
+ export const SDK_VERSION = '0.5.3';