diff --git a/README.md b/README.md index e309804..3cfc168 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Our goal is to bring the TypeScript ecosystem the observability tools it’s bee - [`@kubiks/otel-autumn`](./packages/otel-autumn/README.md) - [`@kubiks/otel-better-auth`](./packages/otel-better-auth/README.md) - [`@kubiks/otel-drizzle`](./packages/otel-drizzle/README.md) +- [`@kubiks/otel-e2b`](./packages/otel-e2b/README.md) - [`@kubiks/otel-inbound`](./packages/otel-inbound/README.md) - [`@kubiks/otel-mongodb`](./packages/otel-mongodb/README.md) - [`@kubiks/otel-resend`](./packages/otel-resend/README.md) diff --git a/packages/otel-e2b/CHANGELOG.md b/packages/otel-e2b/CHANGELOG.md new file mode 100644 index 0000000..36a03f1 --- /dev/null +++ b/packages/otel-e2b/CHANGELOG.md @@ -0,0 +1,28 @@ +# @kubiks/otel-e2b + +## 1.0.0 + +### Features + +- Initial release of OpenTelemetry instrumentation for E2B Sandboxes +- **Core instrumentation functions**: + - `instrumentSandbox()` - Instrument existing sandbox instances + - `instrumentSandboxClass()` - Instrument Sandbox class for automatic instrumentation +- **Sandbox lifecycle tracing**: + - `Sandbox.create()` - Sandbox creation with template information + - `sandbox.kill()` - Sandbox termination +- **Code execution tracing**: + - `sandbox.runCode()` - Code execution with language, error status, and execution count +- **File operations tracing**: + - `sandbox.files.read()` - Read files with path, size, and format + - `sandbox.files.write()` - Write single or multiple files + - `sandbox.files.list()` - List directory contents with file counts + - `sandbox.files.remove()` - Delete files or directories + - `sandbox.files.makeDir()` - Create directories +- **Command execution tracing**: + - `sandbox.commands.run()` - Execute shell commands with exit codes and output line counts +- **Security-first design**: Never captures sensitive data (code content, file contents, command arguments, etc.) +- **Comprehensive configuration options**: Control what metadata to capture +- **Idempotent instrumentation**: Safe to call multiple times +- **Full TypeScript support** with complete type definitions +- **OpenTelemetry semantic conventions** for all span attributes diff --git a/packages/otel-e2b/LICENSE b/packages/otel-e2b/LICENSE new file mode 100644 index 0000000..3162213 --- /dev/null +++ b/packages/otel-e2b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Kubiks + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/packages/otel-e2b/README.md b/packages/otel-e2b/README.md new file mode 100644 index 0000000..3130005 --- /dev/null +++ b/packages/otel-e2b/README.md @@ -0,0 +1,83 @@ +# @kubiks/otel-e2b + +OpenTelemetry instrumentation for [E2B Sandboxes](https://e2b.dev). +Capture spans for sandbox lifecycle, code execution, file operations, and command execution to monitor and debug your E2B sandbox operations. + +## Installation + +```bash +npm install @kubiks/otel-e2b +# or +pnpm add @kubiks/otel-e2b +``` + +## Quick Start + +```ts +import { Sandbox } from "@e2b/code-interpreter"; +import { instrumentSandbox } from "@kubiks/otel-e2b"; + +const sandbox = await Sandbox.create(); +instrumentSandbox(sandbox); + +// All operations are now traced +await sandbox.runCode('print("Hello from E2B")'); +await sandbox.files.write("/app/data.txt", "some data"); +await sandbox.commands.run("ls -la"); +await sandbox.kill(); +``` + +`instrumentSandbox` wraps the sandbox you already use — no configuration changes needed. Every operation creates a client span with useful attributes. + +You can also use `instrumentSandboxClass(Sandbox)` to automatically instrument all sandboxes created after setup. + +## What Gets Traced + +This instrumentation automatically traces all E2B sandbox operations including `Sandbox.create()`, `sandbox.kill()`, `sandbox.runCode()` (code execution), `sandbox.commands.run()` (shell commands), and all file operations (`files.read()`, `files.write()`, `files.list()`, `files.remove()`, `files.makeDir()`). + +## Span Attributes + +Each span includes relevant attributes for debugging and monitoring: + +| Attribute | Description | Example | +| -------------------------- | ------------------------------------- | ---------------------------- | +| `e2b.operation` | Operation type | `sandbox.create`, `code.run` | +| `e2b.sandbox.id` | Unique sandbox identifier | `sb_abc123def456` | +| `e2b.sandbox.template` | Template used for creation | `custom-template` | +| `e2b.code.language` | Programming language | `python`, `javascript` | +| `e2b.code.has_error` | Whether execution had errors | `true`, `false` | +| `e2b.code.execution_count` | Execution count from result | `1`, `2`, `3` | +| `e2b.command.exit_code` | Process exit code | `0`, `1`, `127` | +| `e2b.command.stdout_lines` | Number of stdout lines (when enabled) | `5` | +| `e2b.command.stderr_lines` | Number of stderr lines (when enabled) | `2` | +| `e2b.command.background` | Whether command ran in background | `true`, `false` | +| `e2b.file.operation` | File operation type | `read`, `write`, `list` | +| `e2b.file.path` | File or directory path | `/app/data.txt` | +| `e2b.file.size_bytes` | File size in bytes | `1024` | +| `e2b.file.format` | Read format (for read ops) | `text`, `bytes` | +| `e2b.file.count` | Number of files (list/write multiple) | `10` | + +## Configuration Options + +The instrumentation accepts optional configuration to control what metadata to capture: + +- `tracerName` - Custom tracer name (default: `"@kubiks/otel-e2b"`) +- `captureFilePaths` - Capture file paths, not content (default: `true`) +- `captureFileSize` - Capture file sizes (default: `true`) +- `captureCodeLanguage` - Capture code execution language (default: `true`) +- `captureCommandOutput` - Capture command output line counts, not content (default: `false`) + +Example: + +```ts +instrumentSandbox(sandbox, { + captureFilePaths: true, + captureCommandOutput: true, +}); +``` + +The instrumentation never captures sensitive data like code content, command arguments, file contents, or environment variables — only safe metadata like paths, sizes, exit codes, and language types. + +## License + +MIT diff --git a/packages/otel-e2b/package.json b/packages/otel-e2b/package.json new file mode 100644 index 0000000..3c385bd --- /dev/null +++ b/packages/otel-e2b/package.json @@ -0,0 +1,65 @@ +{ + "name": "@kubiks/otel-e2b", + "version": "1.0.0", + "private": false, + "publishConfig": { + "access": "public" + }, + "description": "OpenTelemetry instrumentation for E2B Sandboxes", + "keywords": [ + "opentelemetry", + "otel", + "instrumentation", + "e2b", + "sandbox", + "code-interpreter", + "observability", + "tracing", + "monitoring", + "telemetry" + ], + "author": "Kubiks", + "license": "MIT", + "repository": "kubiks-inc/otel", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "main": "./dist/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "scripts": { + "build": "pnpm clean && tsc", + "clean": "rimraf dist", + "prepublishOnly": "pnpm build", + "type-check": "tsc --noEmit", + "unit-test": "vitest --run", + "unit-test-watch": "vitest" + }, + "dependencies": {}, + "devDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-trace-base": "^2.1.0", + "@e2b/code-interpreter": "^2.0.0", + "@types/node": "18.15.11", + "rimraf": "3.0.2", + "typescript": "^5", + "vitest": "0.33.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <2.0.0", + "@e2b/code-interpreter": ">=2.0.0" + } +} \ No newline at end of file diff --git a/packages/otel-e2b/src/index.test.ts b/packages/otel-e2b/src/index.test.ts new file mode 100644 index 0000000..8f6a8e7 --- /dev/null +++ b/packages/otel-e2b/src/index.test.ts @@ -0,0 +1,505 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { SpanStatusCode, trace } from "@opentelemetry/api"; +import { + BasicTracerProvider, + InMemorySpanExporter, + SimpleSpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import type { Sandbox } from "@e2b/code-interpreter"; +import { + instrumentSandbox, + instrumentSandboxClass, + SEMATTRS_E2B_OPERATION, + SEMATTRS_E2B_SANDBOX_ID, + SEMATTRS_E2B_CODE_LANGUAGE, + SEMATTRS_E2B_CODE_HAS_ERROR, + SEMATTRS_E2B_CODE_EXECUTION_COUNT, + SEMATTRS_E2B_COMMAND_EXIT_CODE, + SEMATTRS_E2B_COMMAND_STDOUT_LINES, + SEMATTRS_E2B_COMMAND_STDERR_LINES, + SEMATTRS_E2B_COMMAND_BACKGROUND, + SEMATTRS_E2B_FILE_OPERATION, + SEMATTRS_E2B_FILE_PATH, + SEMATTRS_E2B_FILE_SIZE_BYTES, + SEMATTRS_E2B_FILE_COUNT, + SEMATTRS_E2B_FILE_FORMAT, + SEMATTRS_E2B_SANDBOX_TEMPLATE, +} from "./index"; + +describe("instrumentSandbox", () => { + let provider: BasicTracerProvider; + let exporter: InMemorySpanExporter; + + beforeEach(() => { + exporter = new InMemorySpanExporter(); + provider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(exporter)], + }); + trace.setGlobalTracerProvider(provider); + }); + + afterEach(async () => { + await provider.shutdown(); + exporter.reset(); + trace.disable(); + }); + + const createMockSandbox = (): Sandbox => { + const mockSandbox = { + sandboxId: "test-sandbox-123", + kill: vi.fn(async () => {}), + runCode: vi.fn(async () => ({ + results: [], + logs: { stdout: [], stderr: [] }, + error: undefined, + executionCount: 1, + })), + files: { + read: vi.fn(async () => "file content"), + write: vi.fn(async () => ({ + path: "/test/file.txt", + name: "file.txt", + })), + list: vi.fn(async () => [ + { name: "file1.txt", type: "file" }, + { name: "file2.txt", type: "file" }, + ]), + remove: vi.fn(async () => {}), + makeDir: vi.fn(async () => true), + }, + commands: { + run: vi.fn(async () => ({ + exitCode: 0, + stdout: ["output line 1", "output line 2"], + stderr: [], + })), + }, + } as unknown as Sandbox; + + return mockSandbox; + }; + + it("instruments sandbox.kill() and records spans", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox); + + await sandbox.kill(); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.sandbox.kill"); + expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("sandbox.kill"); + expect(span?.attributes[SEMATTRS_E2B_SANDBOX_ID]).toBe("test-sandbox-123"); + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("instruments sandbox.runCode() and captures execution details", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox); + + const result = await (sandbox as any).runCode('print("hello")', { + language: "python", + }); + + expect(result.executionCount).toBe(1); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.code.run"); + expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("code.run"); + expect(span?.attributes[SEMATTRS_E2B_SANDBOX_ID]).toBe("test-sandbox-123"); + expect(span?.attributes[SEMATTRS_E2B_CODE_LANGUAGE]).toBe("python"); + expect(span?.attributes[SEMATTRS_E2B_CODE_HAS_ERROR]).toBe(false); + expect(span?.attributes[SEMATTRS_E2B_CODE_EXECUTION_COUNT]).toBe(1); + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("instruments sandbox.files.read() and captures file details", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox); + + const content = await sandbox.files.read("/path/to/file.txt"); + + expect(content).toBe("file content"); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.files.read"); + expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("files.read"); + expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("read"); + expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/file.txt"); + expect(span?.attributes[SEMATTRS_E2B_FILE_SIZE_BYTES]).toBe(12); // "file content".length + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("instruments sandbox.files.read() with format option", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox); + + await sandbox.files.read("/path/to/file.txt", { format: "bytes" }); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.attributes[SEMATTRS_E2B_FILE_FORMAT]).toBe("bytes"); + }); + + it("instruments sandbox.files.write() for single file", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox); + + await sandbox.files.write("/path/to/file.txt", "test content"); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.files.write"); + expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("write"); + expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/file.txt"); + expect(span?.attributes[SEMATTRS_E2B_FILE_SIZE_BYTES]).toBe(12); // "test content".length + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("instruments sandbox.files.write() for multiple files", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox); + + const files = [ + { path: "/file1.txt", data: "content1" }, + { path: "/file2.txt", data: "content2" }, + { path: "/file3.txt", data: "content3" }, + ]; + + await (sandbox.files as any).write(files); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.files.write"); + expect(span?.attributes[SEMATTRS_E2B_FILE_COUNT]).toBe(3); + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("instruments sandbox.files.list() and captures file count", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox); + + const files = await sandbox.files.list("/path/to/dir"); + + expect(files).toHaveLength(2); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.files.list"); + expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("list"); + expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/dir"); + expect(span?.attributes[SEMATTRS_E2B_FILE_COUNT]).toBe(2); + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("instruments sandbox.files.remove()", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox); + + await sandbox.files.remove("/path/to/file.txt"); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.files.remove"); + expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("remove"); + expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/file.txt"); + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("instruments sandbox.files.makeDir()", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox); + + await sandbox.files.makeDir("/path/to/new/dir"); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.files.makeDir"); + expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("makeDir"); + expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/new/dir"); + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("instruments sandbox.commands.run() and captures command results", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox, { captureCommandOutput: true }); + + const result = await sandbox.commands.run("echo hello"); + + expect(result.exitCode).toBe(0); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.command.run"); + expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("command.run"); + expect(span?.attributes[SEMATTRS_E2B_COMMAND_EXIT_CODE]).toBe(0); + expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDOUT_LINES]).toBe(2); + expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDERR_LINES]).toBe(0); + expect(span?.attributes[SEMATTRS_E2B_COMMAND_BACKGROUND]).toBe(false); + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("instruments sandbox.commands.run() in background mode", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox); + + await sandbox.commands.run("long-running-command", { background: true }); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.attributes[SEMATTRS_E2B_COMMAND_BACKGROUND]).toBe(true); + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("captures errors and marks span status", async () => { + const sandbox = createMockSandbox(); + sandbox.kill = vi.fn().mockRejectedValue(new Error("Failed to kill")); + + instrumentSandbox(sandbox); + + await expect(async () => sandbox.kill()).rejects.toThrowError( + "Failed to kill" + ); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.status.code).toBe(SpanStatusCode.ERROR); + const hasException = span?.events.some( + (event) => event.name === "exception" + ); + expect(hasException).toBe(true); + }); + + it("is idempotent - calling instrument twice doesn't double-wrap", async () => { + const sandbox = createMockSandbox(); + + const first = instrumentSandbox(sandbox); + const second = instrumentSandbox(first); + + expect(first).toBe(second); + + await sandbox.kill(); + + // Should only create one span, not two + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + }); + + it("respects configuration options - captureFilePaths", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox, { captureFilePaths: false }); + + await sandbox.files.read("/secret/path.txt"); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBeUndefined(); + }); + + it("respects configuration options - captureFileSize", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox, { captureFileSize: false }); + + await sandbox.files.read("/path/file.txt"); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.attributes[SEMATTRS_E2B_FILE_SIZE_BYTES]).toBeUndefined(); + }); + + it("respects configuration options - captureCodeLanguage", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox, { captureCodeLanguage: false }); + + await (sandbox as any).runCode('print("test")', { language: "python" }); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.attributes[SEMATTRS_E2B_CODE_LANGUAGE]).toBeUndefined(); + }); + + it("respects configuration options - captureCommandOutput disabled", async () => { + const sandbox = createMockSandbox(); + instrumentSandbox(sandbox, { captureCommandOutput: false }); + + await sandbox.commands.run("echo test"); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDOUT_LINES]).toBeUndefined(); + expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDERR_LINES]).toBeUndefined(); + }); +}); + +describe("instrumentSandboxClass", () => { + let provider: BasicTracerProvider; + let exporter: InMemorySpanExporter; + + beforeEach(() => { + exporter = new InMemorySpanExporter(); + provider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(exporter)], + }); + trace.setGlobalTracerProvider(provider); + }); + + afterEach(async () => { + await provider.shutdown(); + exporter.reset(); + trace.disable(); + }); + + const createMockSandboxClass = () => { + const mockSandbox = { + sandboxId: "created-sandbox-456", + kill: vi.fn(async () => {}), + runCode: vi.fn(async () => ({ + results: [], + logs: { stdout: [], stderr: [] }, + })), + files: { + read: vi.fn(async () => "content"), + write: vi.fn(async () => ({ path: "/test.txt" })), + list: vi.fn(async () => []), + remove: vi.fn(async () => {}), + makeDir: vi.fn(async () => true), + }, + commands: { + run: vi.fn(async () => ({ exitCode: 0, stdout: [], stderr: [] })), + }, + } as unknown as Sandbox; + + return { + create: vi.fn(async () => mockSandbox), + } as unknown as typeof Sandbox; + }; + + it("instruments Sandbox.create() and records span", async () => { + const SandboxClass = createMockSandboxClass(); + instrumentSandboxClass(SandboxClass); + + const sandbox = await SandboxClass.create(); + + expect(sandbox.sandboxId).toBe("created-sandbox-456"); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.sandbox.create"); + expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("sandbox.create"); + expect(span?.attributes[SEMATTRS_E2B_SANDBOX_ID]).toBe( + "created-sandbox-456" + ); + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("captures template from options", async () => { + const SandboxClass = createMockSandboxClass(); + instrumentSandboxClass(SandboxClass); + + await SandboxClass.create({ template: "custom-template" }); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.attributes[SEMATTRS_E2B_SANDBOX_TEMPLATE]).toBe( + "custom-template" + ); + }); + + it("automatically instruments created sandbox instances", async () => { + const SandboxClass = createMockSandboxClass(); + instrumentSandboxClass(SandboxClass); + + const sandbox = await SandboxClass.create(); + + // Clear spans from create operation + exporter.reset(); + + // Use the sandbox - should be automatically instrumented + await sandbox.kill(); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.sandbox.kill"); + expect(span?.status.code).toBe(SpanStatusCode.OK); + }); + + it("is idempotent - calling instrumentSandboxClass twice doesn't double-wrap", async () => { + const SandboxClass = createMockSandboxClass(); + + const first = instrumentSandboxClass(SandboxClass); + const second = instrumentSandboxClass(first); + + expect(first).toBe(second); + + await SandboxClass.create(); + + // Should only create one span for create, not two + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + expect(spans[0]?.name).toBe("e2b.sandbox.create"); + }); + + it("handles errors during sandbox creation", async () => { + const SandboxClass = createMockSandboxClass(); + (SandboxClass as any).create = vi + .fn() + .mockRejectedValue(new Error("Failed to create sandbox")); + + instrumentSandboxClass(SandboxClass); + + await expect(async () => SandboxClass.create()).rejects.toThrowError( + "Failed to create sandbox" + ); + + const spans = exporter.getFinishedSpans(); + expect(spans).toHaveLength(1); + + const span = spans[0]; + expect(span?.name).toBe("e2b.sandbox.create"); + expect(span?.status.code).toBe(SpanStatusCode.ERROR); + const hasException = span?.events.some( + (event) => event.name === "exception" + ); + expect(hasException).toBe(true); + }); +}); diff --git a/packages/otel-e2b/src/index.ts b/packages/otel-e2b/src/index.ts new file mode 100644 index 0000000..adf8ebd --- /dev/null +++ b/packages/otel-e2b/src/index.ts @@ -0,0 +1,672 @@ +import { + context, + SpanKind, + SpanStatusCode, + trace, + type Span, +} from "@opentelemetry/api"; +import type { Sandbox } from "@e2b/code-interpreter"; + +const DEFAULT_TRACER_NAME = "@kubiks/otel-e2b"; +const INSTRUMENTED_FLAG = Symbol("kubiksOtelE2BInstrumented"); +const INSTRUMENTED_FILES_FLAG = Symbol("kubiksOtelE2BFilesInstrumented"); +const INSTRUMENTED_COMMANDS_FLAG = Symbol("kubiksOtelE2BCommandsInstrumented"); + +// Semantic attribute constants following OpenTelemetry conventions +export const SEMATTRS_E2B_OPERATION = "e2b.operation" as const; +export const SEMATTRS_E2B_SANDBOX_ID = "e2b.sandbox.id" as const; +export const SEMATTRS_E2B_SANDBOX_TEMPLATE = "e2b.sandbox.template" as const; + +// Code execution attributes +export const SEMATTRS_E2B_CODE_LANGUAGE = "e2b.code.language" as const; +export const SEMATTRS_E2B_CODE_HAS_ERROR = "e2b.code.has_error" as const; +export const SEMATTRS_E2B_CODE_EXECUTION_COUNT = + "e2b.code.execution_count" as const; + +// Command execution attributes +export const SEMATTRS_E2B_COMMAND_EXIT_CODE = "e2b.command.exit_code" as const; +export const SEMATTRS_E2B_COMMAND_STDOUT_LINES = + "e2b.command.stdout_lines" as const; +export const SEMATTRS_E2B_COMMAND_STDERR_LINES = + "e2b.command.stderr_lines" as const; +export const SEMATTRS_E2B_COMMAND_BACKGROUND = + "e2b.command.background" as const; + +// File operation attributes +export const SEMATTRS_E2B_FILE_OPERATION = "e2b.file.operation" as const; +export const SEMATTRS_E2B_FILE_PATH = "e2b.file.path" as const; +export const SEMATTRS_E2B_FILE_SIZE_BYTES = "e2b.file.size_bytes" as const; +export const SEMATTRS_E2B_FILE_FORMAT = "e2b.file.format" as const; +export const SEMATTRS_E2B_FILE_COUNT = "e2b.file.count" as const; + +/** + * Configuration options for E2B instrumentation. + */ +export interface InstrumentE2BConfig { + /** + * Custom tracer name. Defaults to "@kubiks/otel-e2b". + */ + tracerName?: string; + + /** + * Whether to capture file paths in spans. + * Paths only, not content. + * @default true + */ + captureFilePaths?: boolean; + + /** + * Whether to capture file sizes in spans. + * @default true + */ + captureFileSize?: boolean; + + /** + * Whether to capture code language in spans. + * @default true + */ + captureCodeLanguage?: boolean; + + /** + * Whether to capture command output line counts. + * Only counts, not actual content. + * @default false + */ + captureCommandOutput?: boolean; +} + +interface InstrumentedSandbox { + [INSTRUMENTED_FLAG]?: true; +} + +interface InstrumentedFilesystem { + [INSTRUMENTED_FILES_FLAG]?: true; +} + +interface InstrumentedCommands { + [INSTRUMENTED_COMMANDS_FLAG]?: true; +} + +/** + * Finalizes a span with status, timing, and optional error. + */ +function finalizeSpan(span: Span, error?: unknown): void { + if (error) { + if (error instanceof Error) { + span.recordException(error); + } else { + span.recordException(new Error(String(error))); + } + span.setStatus({ code: SpanStatusCode.ERROR }); + } else { + span.setStatus({ code: SpanStatusCode.OK }); + } + span.end(); +} + +/** + * Instruments the filesystem module of a sandbox. + */ +function instrumentFilesystem( + files: any, + sandboxId: string, + tracer: ReturnType, + config?: InstrumentE2BConfig +): any { + if (!files) { + return files; + } + + // Check if already instrumented + if ((files as InstrumentedFilesystem)[INSTRUMENTED_FILES_FLAG]) { + return files; + } + + const { captureFilePaths = true, captureFileSize = true } = config ?? {}; + + // Instrument read + const originalRead = files.read?.bind(files); + if (originalRead) { + files.read = async function instrumentedRead( + path: string, + opts?: any + ): Promise { + const span = tracer.startSpan("e2b.files.read", { + kind: SpanKind.CLIENT, + }); + + span.setAttributes({ + [SEMATTRS_E2B_OPERATION]: "files.read", + [SEMATTRS_E2B_SANDBOX_ID]: sandboxId, + [SEMATTRS_E2B_FILE_OPERATION]: "read", + }); + + if (captureFilePaths && path) { + span.setAttribute(SEMATTRS_E2B_FILE_PATH, path); + } + + if (opts?.format) { + span.setAttribute(SEMATTRS_E2B_FILE_FORMAT, opts.format); + } + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalRead(path, opts) + ); + + // Capture size if result is a string or has a length/size property + if (captureFileSize) { + if (typeof result === "string") { + span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, result.length); + } else if (result?.length !== undefined) { + span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, result.length); + } else if (result?.size !== undefined) { + span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, result.size); + } + } + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + } + + // Instrument write (handles both single and multiple file writes) + const originalWrite = files.write?.bind(files); + if (originalWrite) { + files.write = async function instrumentedWrite( + pathOrFiles: string | any[], + dataOrOpts?: any, + opts?: any + ): Promise { + const span = tracer.startSpan("e2b.files.write", { + kind: SpanKind.CLIENT, + }); + + span.setAttributes({ + [SEMATTRS_E2B_OPERATION]: "files.write", + [SEMATTRS_E2B_SANDBOX_ID]: sandboxId, + [SEMATTRS_E2B_FILE_OPERATION]: "write", + }); + + const isArray = Array.isArray(pathOrFiles); + + if (isArray) { + // Multiple files + span.setAttribute(SEMATTRS_E2B_FILE_COUNT, pathOrFiles.length); + } else if (captureFilePaths && typeof pathOrFiles === "string") { + // Single file + span.setAttribute(SEMATTRS_E2B_FILE_PATH, pathOrFiles); + + // Try to capture size + if (captureFileSize && dataOrOpts) { + if (typeof dataOrOpts === "string") { + span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, dataOrOpts.length); + } else if (dataOrOpts?.length !== undefined) { + span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, dataOrOpts.length); + } else if (dataOrOpts?.size !== undefined) { + span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, dataOrOpts.size); + } + } + } + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalWrite(pathOrFiles, dataOrOpts, opts) + ); + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + } + + // Instrument list + const originalList = files.list?.bind(files); + if (originalList) { + files.list = async function instrumentedList( + path: string, + opts?: any + ): Promise { + const span = tracer.startSpan("e2b.files.list", { + kind: SpanKind.CLIENT, + }); + + span.setAttributes({ + [SEMATTRS_E2B_OPERATION]: "files.list", + [SEMATTRS_E2B_SANDBOX_ID]: sandboxId, + [SEMATTRS_E2B_FILE_OPERATION]: "list", + }); + + if (captureFilePaths && path) { + span.setAttribute(SEMATTRS_E2B_FILE_PATH, path); + } + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalList(path, opts) + ); + + if (Array.isArray(result)) { + span.setAttribute(SEMATTRS_E2B_FILE_COUNT, result.length); + } + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + } + + // Instrument remove + const originalRemove = files.remove?.bind(files); + if (originalRemove) { + files.remove = async function instrumentedRemove( + path: string, + opts?: any + ): Promise { + const span = tracer.startSpan("e2b.files.remove", { + kind: SpanKind.CLIENT, + }); + + span.setAttributes({ + [SEMATTRS_E2B_OPERATION]: "files.remove", + [SEMATTRS_E2B_SANDBOX_ID]: sandboxId, + [SEMATTRS_E2B_FILE_OPERATION]: "remove", + }); + + if (captureFilePaths && path) { + span.setAttribute(SEMATTRS_E2B_FILE_PATH, path); + } + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalRemove(path, opts) + ); + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + } + + // Instrument makeDir + const originalMakeDir = files.makeDir?.bind(files); + if (originalMakeDir) { + files.makeDir = async function instrumentedMakeDir( + path: string, + opts?: any + ): Promise { + const span = tracer.startSpan("e2b.files.makeDir", { + kind: SpanKind.CLIENT, + }); + + span.setAttributes({ + [SEMATTRS_E2B_OPERATION]: "files.makeDir", + [SEMATTRS_E2B_SANDBOX_ID]: sandboxId, + [SEMATTRS_E2B_FILE_OPERATION]: "makeDir", + }); + + if (captureFilePaths && path) { + span.setAttribute(SEMATTRS_E2B_FILE_PATH, path); + } + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalMakeDir(path, opts) + ); + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + } + + // Mark as instrumented + (files as InstrumentedFilesystem)[INSTRUMENTED_FILES_FLAG] = true; + + return files; +} + +/** + * Instruments the commands module of a sandbox. + */ +function instrumentCommands( + commands: any, + sandboxId: string, + tracer: ReturnType, + config?: InstrumentE2BConfig +): any { + if (!commands) { + return commands; + } + + // Check if already instrumented + if ((commands as InstrumentedCommands)[INSTRUMENTED_COMMANDS_FLAG]) { + return commands; + } + + const { captureCommandOutput = false } = config ?? {}; + + // Instrument run + const originalRun = commands.run?.bind(commands); + if (originalRun) { + commands.run = async function instrumentedRun( + cmd: string, + opts?: any + ): Promise { + const span = tracer.startSpan("e2b.command.run", { + kind: SpanKind.CLIENT, + }); + + const isBackground = opts?.background === true; + + span.setAttributes({ + [SEMATTRS_E2B_OPERATION]: "command.run", + [SEMATTRS_E2B_SANDBOX_ID]: sandboxId, + [SEMATTRS_E2B_COMMAND_BACKGROUND]: isBackground, + }); + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalRun(cmd, opts) + ); + + // For non-background commands, capture result details + if (!isBackground && result) { + if (result.exitCode !== undefined) { + span.setAttribute(SEMATTRS_E2B_COMMAND_EXIT_CODE, result.exitCode); + } + + if (captureCommandOutput) { + if (result.stdout !== undefined) { + const stdoutLines = Array.isArray(result.stdout) + ? result.stdout.length + : typeof result.stdout === "string" + ? result.stdout.split("\n").length + : 0; + span.setAttribute(SEMATTRS_E2B_COMMAND_STDOUT_LINES, stdoutLines); + } + + if (result.stderr !== undefined) { + const stderrLines = Array.isArray(result.stderr) + ? result.stderr.length + : typeof result.stderr === "string" + ? result.stderr.split("\n").length + : 0; + span.setAttribute(SEMATTRS_E2B_COMMAND_STDERR_LINES, stderrLines); + } + } + } + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + } + + // Mark as instrumented + (commands as InstrumentedCommands)[INSTRUMENTED_COMMANDS_FLAG] = true; + + return commands; +} + +/** + * Instruments an E2B Sandbox instance with OpenTelemetry tracing. + * + * This function wraps sandbox methods to create spans for each operation. + * The instrumentation is idempotent - calling it multiple times on the same + * sandbox will only instrument it once. + * + * @param sandbox - The E2B Sandbox to instrument + * @param config - Optional configuration for instrumentation behavior + * @returns The instrumented sandbox (same instance, modified in place) + * + * @example + * ```typescript + * import { Sandbox } from '@e2b/code-interpreter'; + * import { instrumentSandbox } from '@kubiks/otel-e2b'; + * + * const sandbox = await Sandbox.create(); + * instrumentSandbox(sandbox, { + * captureFilePaths: true, + * captureFileSize: true, + * }); + * + * // All operations are now traced + * await sandbox.runCode('print("Hello")'); + * ``` + */ +export function instrumentSandbox( + sandbox: T, + config?: InstrumentE2BConfig +): T { + if (!sandbox) { + return sandbox; + } + + // Check if already instrumented + if ((sandbox as any)[INSTRUMENTED_FLAG]) { + return sandbox; + } + + const { tracerName = DEFAULT_TRACER_NAME, captureCodeLanguage = true } = + config ?? {}; + + const tracer = trace.getTracer(tracerName); + const sandboxId = sandbox.sandboxId || "unknown"; + + // Instrument kill method + const originalKill = sandbox.kill?.bind(sandbox); + if (originalKill) { + (sandbox as any).kill = async function instrumentedKill( + opts?: any + ): Promise { + const span = tracer.startSpan("e2b.sandbox.kill", { + kind: SpanKind.CLIENT, + }); + + span.setAttributes({ + [SEMATTRS_E2B_OPERATION]: "sandbox.kill", + [SEMATTRS_E2B_SANDBOX_ID]: sandboxId, + }); + + const activeContext = trace.setSpan(context.active(), span); + + try { + await context.with(activeContext, () => originalKill(opts)); + finalizeSpan(span); + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + } + + // Instrument runCode method (code-interpreter specific) + const originalRunCode = (sandbox as any).runCode?.bind(sandbox); + if (originalRunCode) { + (sandbox as any).runCode = async function instrumentedRunCode( + code: string, + opts?: any + ): Promise { + const span = tracer.startSpan("e2b.code.run", { + kind: SpanKind.CLIENT, + }); + + span.setAttributes({ + [SEMATTRS_E2B_OPERATION]: "code.run", + [SEMATTRS_E2B_SANDBOX_ID]: sandboxId, + }); + + // Capture language if available + if (captureCodeLanguage) { + const language = opts?.language || opts?.context?.language || "python"; + span.setAttribute(SEMATTRS_E2B_CODE_LANGUAGE, language); + } + + const activeContext = trace.setSpan(context.active(), span); + + try { + const result = await context.with(activeContext, () => + originalRunCode(code, opts) + ); + + // Capture execution details + if (result) { + // Always set has_error attribute (true if error exists, false otherwise) + span.setAttribute(SEMATTRS_E2B_CODE_HAS_ERROR, !!result.error); + + if (result.executionCount !== undefined) { + span.setAttribute( + SEMATTRS_E2B_CODE_EXECUTION_COUNT, + result.executionCount + ); + } + } + + finalizeSpan(span); + return result; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + } + + // Instrument filesystem module + if (sandbox.files) { + instrumentFilesystem(sandbox.files, sandboxId, tracer, config); + } + + // Instrument commands module + if (sandbox.commands) { + instrumentCommands(sandbox.commands, sandboxId, tracer, config); + } + + // Mark as instrumented + (sandbox as any)[INSTRUMENTED_FLAG] = true; + + return sandbox; +} + +/** + * Instruments the Sandbox class itself to automatically instrument all created sandboxes. + * + * This function wraps the static `Sandbox.create()` method to automatically + * instrument any sandbox instances it creates. + * + * @param SandboxClass - The Sandbox class to instrument + * @param config - Optional configuration for instrumentation behavior + * @returns The instrumented Sandbox class (same class, modified in place) + * + * @example + * ```typescript + * import { Sandbox } from '@e2b/code-interpreter'; + * import { instrumentSandboxClass } from '@kubiks/otel-e2b'; + * + * // Instrument the class once at startup + * instrumentSandboxClass(Sandbox, { + * captureFilePaths: true, + * captureFileSize: true, + * }); + * + * // All sandboxes created after this are automatically instrumented + * const sandbox = await Sandbox.create(); + * await sandbox.runCode('print("Hello")'); // Automatically traced + * ``` + */ +export function instrumentSandboxClass( + SandboxClass: T, + config?: InstrumentE2BConfig +): T { + if (!SandboxClass) { + return SandboxClass; + } + + // Check if already instrumented + if ((SandboxClass as any)[INSTRUMENTED_FLAG]) { + return SandboxClass; + } + + const { tracerName = DEFAULT_TRACER_NAME } = config ?? {}; + const tracer = trace.getTracer(tracerName); + + // Instrument the static create method + const originalCreate = SandboxClass.create?.bind(SandboxClass); + if (originalCreate) { + (SandboxClass as any).create = async function instrumentedCreate( + opts?: any + ): Promise { + const span = tracer.startSpan("e2b.sandbox.create", { + kind: SpanKind.CLIENT, + }); + + span.setAttributes({ + [SEMATTRS_E2B_OPERATION]: "sandbox.create", + }); + + // Capture template if available + if (opts?.template) { + span.setAttribute(SEMATTRS_E2B_SANDBOX_TEMPLATE, opts.template); + } + + const activeContext = trace.setSpan(context.active(), span); + + try { + const sandbox = await context.with(activeContext, () => + originalCreate(opts) + ); + + // Add sandbox ID to span + if (sandbox?.sandboxId) { + span.setAttribute(SEMATTRS_E2B_SANDBOX_ID, sandbox.sandboxId); + } + + finalizeSpan(span); + + // Automatically instrument the created sandbox + instrumentSandbox(sandbox as Sandbox, config); + + return sandbox as Sandbox; + } catch (error) { + finalizeSpan(span, error); + throw error; + } + }; + } + + // Mark as instrumented + (SandboxClass as any)[INSTRUMENTED_FLAG] = true; + + return SandboxClass; +} diff --git a/packages/otel-e2b/tsconfig.json b/packages/otel-e2b/tsconfig.json new file mode 100644 index 0000000..8052049 --- /dev/null +++ b/packages/otel-e2b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020"], + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declarationDir": "dist/types", + "stripInternal": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c54d9d..b4e2b6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,30 @@ importers: specifier: 0.33.0 version: 0.33.0(less@4.2.0)(sass@1.69.7)(stylus@0.59.0) + packages/otel-e2b: + devDependencies: + '@e2b/code-interpreter': + specifier: ^2.0.0 + version: 2.2.0 + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/sdk-trace-base': + specifier: ^2.1.0 + version: 2.1.0(@opentelemetry/api@1.9.0) + '@types/node': + specifier: 18.15.11 + version: 18.15.11 + rimraf: + specifier: 3.0.2 + version: 3.0.2 + typescript: + specifier: ^5 + version: 5.3.3 + vitest: + specifier: 0.33.0 + version: 0.33.0(less@4.2.0)(sass@1.69.7)(stylus@0.59.0) + packages/otel-inbound: devDependencies: '@inboundemail/sdk': @@ -284,6 +308,9 @@ packages: '@better-fetch/fetch@1.1.18': resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} + '@bufbuild/protobuf@2.10.0': + resolution: {integrity: sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==} + '@changesets/apply-release-plan@7.0.0': resolution: {integrity: sha512-vfi69JR416qC9hWmFGSxj7N6wA5J222XNBmezSVATPWDVPIF7gkd4d8CpbEbXmRWbVrkoli3oerGS6dcL/BGsQ==} @@ -345,6 +372,21 @@ packages: '@changesets/write@0.3.0': resolution: {integrity: sha512-slGLb21fxZVUYbyea+94uFiD6ntQW0M2hIKNznFizDhZPDgn2c/fv1UzzlW43RVzh1BEDuIqW6hzlJ1OflNmcw==} + '@connectrpc/connect-web@2.0.0-rc.3': + resolution: {integrity: sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw==} + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + '@connectrpc/connect': 2.0.0-rc.3 + + '@connectrpc/connect@2.0.0-rc.3': + resolution: {integrity: sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ==} + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + + '@e2b/code-interpreter@2.2.0': + resolution: {integrity: sha512-j2XIhuuV+MvjCMJbwRCL3hZjkOTv4Jk1FygI4dCwaU9PggtAW+TXhyaVzzVPZroE9fqxLfrXGqY08P06Ei7MQA==} + engines: {node: '>=20'} + '@esbuild/aix-ppc64@0.25.4': resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} engines: {node: '>=18'} @@ -637,10 +679,22 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1082,6 +1136,10 @@ packages: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -1114,6 +1172,9 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1230,6 +1291,9 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dockerfile-ast@0.7.1: + resolution: {integrity: sha512-oX/A4I0EhSkGqrFv0YuvPkBUSYp1XiY8O8zAKc8Djglx8ocz+JfOr8gP0ryRMC2myqvDLagmnZaU9ot1vG2ijw==} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -1339,6 +1403,10 @@ packages: sqlite3: optional: true + e2b@2.6.2: + resolution: {integrity: sha512-BQ2yzrBu4v48geRiTdsrHdqcWAV2zvlUq81Ont/KI7foYxk24ghdOwvOSURKJ+i1Y5vO8lDTIfsc0tWswAfOiQ==} + engines: {node: '>=20'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1497,6 +1565,11 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -1701,6 +1774,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} @@ -1792,6 +1869,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -1839,6 +1920,10 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1858,6 +1943,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mixme@0.5.10: resolution: {integrity: sha512-5H76ANWinB1H3twpJ6JY8uvAtpmFvHNArpilJAjXRKXSDDLPIMoZArw5SH0q9z+lLs8IrMw7Q2VWpWimFKFT1Q==} engines: {node: '>= 8.0.0'} @@ -1903,11 +1992,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanostores@1.0.1: resolution: {integrity: sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==} engines: {node: ^20.0.0 || >=22.0.0} @@ -1956,6 +2040,12 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + openapi-fetch@0.14.1: + resolution: {integrity: sha512-l7RarRHxlEZYjMLd/PR0slfMVse2/vvIAGm75/F7J6MlQ8/b9uUQmUF2kCPrQhJqMXSxmYWObVgeYXbFYzZR+A==} + + openapi-typescript-helpers@0.0.15: + resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} + os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -2028,6 +2118,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2070,6 +2164,9 @@ packages: pkg-types@1.0.3: resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + postcss@8.4.33: resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==} engines: {node: ^10 || ^12 || >=14} @@ -2425,6 +2522,10 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + tar@7.5.2: + resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} + engines: {node: '>=18'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -2633,6 +2734,12 @@ packages: webdriverio: optional: true + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -2710,6 +2817,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -2810,6 +2921,8 @@ snapshots: '@better-fetch/fetch@1.1.18': {} + '@bufbuild/protobuf@2.10.0': {} + '@changesets/apply-release-plan@7.0.0': dependencies: '@babel/runtime': 7.23.7 @@ -2975,6 +3088,19 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 + '@connectrpc/connect-web@2.0.0-rc.3(@bufbuild/protobuf@2.10.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.10.0))': + dependencies: + '@bufbuild/protobuf': 2.10.0 + '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.10.0) + + '@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.10.0)': + dependencies: + '@bufbuild/protobuf': 2.10.0 + + '@e2b/code-interpreter@2.2.0': + dependencies: + e2b: 2.6.2 + '@esbuild/aix-ppc64@0.25.4': optional: true @@ -3123,6 +3249,12 @@ snapshots: react: 18.2.0 react-dom: 18.3.1(react@18.2.0) + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3132,6 +3264,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 @@ -3663,6 +3799,8 @@ snapshots: fsevents: 2.3.3 optional: true + chownr@3.0.0: {} + ci-info@3.9.0: {} cliui@6.0.0: @@ -3693,6 +3831,8 @@ snapshots: commander@10.0.1: {} + compare-versions@6.1.1: {} + concat-map@0.0.1: {} config-chain@1.1.13: @@ -3793,6 +3933,11 @@ snapshots: dependencies: path-type: 4.0.0 + dockerfile-ast@0.7.1: + dependencies: + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -3822,6 +3967,19 @@ snapshots: postgres: 3.4.7 react: 18.2.0 + e2b@2.6.2: + dependencies: + '@bufbuild/protobuf': 2.10.0 + '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.10.0) + '@connectrpc/connect-web': 2.0.0-rc.3(@bufbuild/protobuf@2.10.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.10.0)) + chalk: 5.6.2 + compare-versions: 6.1.1 + dockerfile-ast: 0.7.1 + glob: 11.0.3 + openapi-fetch: 0.14.1 + platform: 1.3.6 + tar: 7.5.2 + eastasianwidth@0.2.0: {} editorconfig@1.0.4: @@ -4077,6 +4235,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.1.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -4278,6 +4445,10 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + jose@5.10.0: {} jose@6.1.0: {} @@ -4369,6 +4540,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.2: {} + lru-cache@4.1.5: dependencies: pseudomap: 1.0.2 @@ -4420,6 +4593,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.1.1: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -4440,6 +4617,10 @@ snapshots: minipass@7.1.2: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + mixme@0.5.10: {} mlly@1.4.2: @@ -4464,8 +4645,6 @@ snapshots: nanoid@3.3.11: {} - nanoid@3.3.7: {} - nanostores@1.0.1: {} needle@3.3.1: @@ -4509,6 +4688,12 @@ snapshots: dependencies: wrappy: 1.0.2 + openapi-fetch@0.14.1: + dependencies: + openapi-typescript-helpers: 0.0.15 + + openapi-typescript-helpers@0.0.15: {} + os-tmpdir@1.0.2: {} outdent@0.5.0: {} @@ -4571,6 +4756,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.0: + dependencies: + lru-cache: 11.2.2 + minipass: 7.1.2 + path-type@4.0.0: {} pathe@1.1.1: {} @@ -4607,9 +4797,11 @@ snapshots: mlly: 1.4.2 pathe: 1.1.1 + platform@1.3.6: {} + postcss@8.4.33: dependencies: - nanoid: 3.3.7 + nanoid: 3.3.11 picocolors: 1.0.0 source-map-js: 1.0.2 @@ -4974,6 +5166,14 @@ snapshots: react: 18.2.0 use-sync-external-store: 1.6.0(react@18.2.0) + tar@7.5.2: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + term-size@2.2.1: {} throttleit@2.1.0: {} @@ -5169,6 +5369,10 @@ snapshots: - supports-color - terser + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -5253,6 +5457,8 @@ snapshots: yallist@4.0.0: {} + yallist@5.0.0: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1