diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..0a3a479 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,5 @@ +# Changesets + +Describe each change with `pnpm changeset` and commit the generated file with it. A release runs `pnpm run version` and +`pnpm run release` in CI (`.github/workflows/release.yml`); do not run `changeset version` locally, or the release +finds nothing to publish. The npm and PyPI packages share one version. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..196f43e --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [["@e2b/adk", "@e2b/adk-python"]], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "privatePackages": { + "version": true, + "tag": true + } +} diff --git a/.github/scripts/is_release.sh b/.github/scripts/is_release.sh new file mode 100755 index 0000000..3b88bd0 --- /dev/null +++ b/.github/scripts/is_release.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Prints "true" when .changeset/ holds pending changesets, which is what gates a release. +set -euo pipefail +count=$(find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md' | wc -l | tr -d ' ') +[ "$count" -gt 0 ] && echo "true" || echo "false" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e31f5eb..6fcf03a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,12 +9,16 @@ permissions: contents: read jobs: - lint-type-test: + python: + name: Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] + defaults: + run: + working-directory: packages/e2b-adk-python steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 @@ -23,7 +27,7 @@ jobs: with: python-version: ${{ matrix.python-version }} enable-cache: true - cache-dependency-glob: uv.lock + cache-dependency-glob: packages/e2b-adk-python/uv.lock - name: Install dependencies run: uv sync --locked --extra dev @@ -35,26 +39,30 @@ jobs: run: uv run mypy e2b_adk - name: Test - # Tests mock the E2B sandbox — no credentials or network access needed. + # Tests mock the E2B sandbox: no credentials or network access needed. run: uv run pytest - build: + - name: Build sdist and wheel + if: matrix.python-version == '3.12' + run: uv build + + js: + name: Node ${{ matrix.node }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [22, 24] steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - name: Install uv - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - with: - python-version: "3.12" - enable-cache: true - cache-dependency-glob: uv.lock + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 - - name: Install dependencies - run: uv sync --locked --extra dev + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: ${{ matrix.node }} + cache: pnpm - - name: Build sdist and wheel - run: uv run python -m build + - run: pnpm install --frozen-lockfile - - name: Check package metadata - run: uv run twine check dist/* + - run: pnpm --filter @e2b/adk check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3875e42..2e130fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,72 +1,160 @@ name: Release +# Releases are cut by hand: write changesets (`pnpm changeset`), merge them, then +# `gh workflow run release.yml --ref main`. One run versions and publishes both packages: +# @e2b/adk to npm and e2b-adk to PyPI, both over OIDC trusted publishing (no tokens). +# Both registries trust this file and the `deployment` environment. on: - release: - types: [published] + workflow_dispatch: {} + +concurrency: release-${{ github.ref }} permissions: contents: read jobs: - build: - name: Build and test distributions + preflight: + name: Release preflight runs-on: ubuntu-latest - + outputs: + release: ${{ steps.version.outputs.release }} steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - name: Check the ref + # `workflow_dispatch` offers every branch in the picker, and a feature branch + # carrying changesets would otherwise publish real packages from that branch. + if: github.ref != 'refs/heads/main' + env: + REF: ${{ github.ref }} + run: | + echo "::error::Releases must run on main; this run is on ${REF}." + exit 1 - - name: Set up uv - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - with: - version: "0.13.4" - python-version: "3.13" - enable-cache: true - cache-dependency-glob: uv.lock + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - name: Install dependencies - run: uv sync --locked --extra dev + - name: Check for pending changesets + id: version + run: echo "release=$(./.github/scripts/is_release.sh)" >> "$GITHUB_OUTPUT" + + test: + name: Build and test + needs: preflight + if: needs.preflight.outputs.release == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - name: Run unit tests - run: uv run pytest + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 - - name: Run lint checks - run: uv run ruff check e2b_adk tests examples + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 22 + cache: pnpm - - name: Run type checks - run: uv run mypy e2b_adk + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: '0.13.4' + python-version: '3.13' + enable-cache: true + cache-dependency-glob: packages/e2b-adk-python/uv.lock - - name: Build distributions - run: uv run python -m build + - run: pnpm install --frozen-lockfile - - name: Check package metadata - run: uv run twine check dist/* + - run: uv sync --frozen --extra dev + working-directory: packages/e2b-adk-python - - name: Upload distributions - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: python-package-distributions - path: dist/* - if-no-files-found: error - retention-days: 7 + - run: pnpm check publish: - name: Publish to PyPI - needs: build + name: Version and publish + needs: [preflight, test] + if: needs.preflight.outputs.release == 'true' runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/e2b-adk + # Both trusted publishers (npm and PyPI) are pinned to this environment, and its + # deployment branch policy only allows main. + environment: deployment permissions: - contents: read + contents: write id-token: write - steps: - - name: Download distributions - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 + - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + id: app-token + with: + app-id: ${{ vars.VERSION_BUMPER_APPID }} + private-key: ${{ secrets.VERSION_BUMPER_SECRET }} + + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + token: ${{ steps.app-token.outputs.token }} + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 22 + registry-url: 'https://registry.npmjs.org' + cache: pnpm + + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version: '0.13.4' + python-version: '3.13' + enable-cache: true + cache-dependency-glob: packages/e2b-adk-python/uv.lock + + - name: Update npm + # npm trusted publishing needs npm 11.5.1 or newer. + run: | + npm install -g npm@^11.6 + npm --version + + - run: pnpm install --frozen-lockfile + + - run: uv sync --frozen --extra dev + working-directory: packages/e2b-adk-python + + - name: No local dependencies + # A file: or link: dependency would publish a local path in package.json or the + # Python sdist, and nothing outside this repo could install it. + run: | + if grep -nE '"(file|link):' package.json packages/*/package.json || grep -nE '(file|path) *=' packages/e2b-adk-python/pyproject.toml; then + echo "::error::Local dependency paths found; point them at published versions before releasing." + exit 1 + fi + + - name: Create new versions + run: pnpm run version + + - name: Commit new versions + id: commit + # Commit before publishing, because `changeset publish` tags whatever HEAD it + # publishes from. + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + # `add -A`: `changeset version` writes each CHANGELOG.md as a new file the first time. + git add -A + if git diff --cached --quiet; then + echo "::error::'changeset version' produced no changes, so there is nothing to publish or tag." + exit 1 + fi + git commit -m "[skip ci] Release new versions" + + - name: Publish + id: release + uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 with: - name: python-package-distributions - path: dist + publish: pnpm run release + createGithubReleases: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Publish distributions to PyPI - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3 + - name: Push new versions + # Keep the version bump even after a partial publish, so a later release cannot + # reuse a version that one registry already has. + if: always() && steps.commit.outcome == 'success' && steps.release.outcome != 'skipped' + run: | + if ! git push; then + git fetch origin "${GITHUB_REF_NAME}" + git merge --no-edit FETCH_HEAD + git push + fi diff --git a/.gitignore b/.gitignore index 697bf63..2b6bf0c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,32 +2,40 @@ /.claude/ /.planning/ -# Python +# Credentials. +.env +.env.* +!.env.example +/.claude/verification/auth-state.json + +# Dependencies and tool caches. +node_modules/ .venv/ __pycache__/ *.pyc -dist/ -build/ -*.egg-info/ .pytest_cache/ .mypy_cache/ .ruff_cache/ +# Build output: regenerate, don't commit. +dist/ +build/ +coverage/ +*.egg-info/ +*.tgz +work/ +.DS_Store + # Docs staged here for separate PRs to other repos (e2b-dev/docs, google/adk-docs) /docs/ -# Features +# Personal agent notes. /features/ /CLAUDE.md /AGENTS.md /CLAUDE_ADDITIONS.md /AGENTS_ADDITIONS.md -/DECISIONS.md - -# Environment -.env -!.env.example - -# Verification credentials (never commit) -.env.verification -/.claude/verification/auth-state.json \ No newline at end of file +DECISIONS.md +EXECUTION_PLAN.md +VALIDATION.md +HANDOFF.md diff --git a/README.md b/README.md index 372fcbf..f0c6a48 100644 --- a/README.md +++ b/README.md @@ -1,151 +1,38 @@ -# e2b-adk +# E2B for Google ADK -A [Google ADK](https://google.github.io/adk-docs/) plugin backed by -[E2B](https://e2b.dev) sandboxes. Attach it to an ADK agent and the agent can -generate and run code, execute shell commands, read and write files, and start -long-running processes — all inside an isolated E2B sandbox, with no -infrastructure of your own. +Give [Google ADK](https://google.github.io/adk-docs/) agents code, shell, file and background-process tools that run in +an isolated [E2B](https://e2b.dev) sandbox. Available for Python and TypeScript. -Under the hood every tool call runs in the -[E2B Code Interpreter](https://e2b.dev/docs), so the model's code executes in a -real Python kernel instead of being trusted blind. +| | Package | Registry | Docs | +| --- | --- | --- | --- | +| Python | `e2b-adk` | [PyPI](https://pypi.org/project/e2b-adk/) | [packages/e2b-adk-python](packages/e2b-adk-python/README.md) | +| TypeScript | `@e2b/adk` | npm (with the first release) | [packages/e2b-adk-js](packages/e2b-adk-js/README.md) | -## Install +## Setup ```bash pip install e2b-adk +# or +npm install @google/adk @e2b/adk ``` -Or with [uv](https://docs.astral.sh/uv/): - -```bash -uv pip install e2b-adk -``` - -## Credentials - -You need an E2B API key for the sandbox and a model key for the agent's LLM -(Gemini is ADK's default): - -```bash -export E2B_API_KEY="..." -export GOOGLE_API_KEY="..." -``` - -Get an E2B key at [e2b.dev/dashboard](https://e2b.dev/dashboard). - -## Quickstart - -Create the plugin, hand its tools to an `Agent`, and register the plugin on the -`App`. The plugin owns the sandbox lifecycle — you never create or tear one down -yourself. - -```python -import asyncio - -from google.adk.agents import Agent -from google.adk.apps import App -from google.adk.runners import InMemoryRunner - -from e2b_adk import E2BPlugin +Set `E2B_API_KEY` ([get one](https://e2b.dev/dashboard)) and a model key such as `GOOGLE_API_KEY`. +## Configuration -async def main() -> None: - plugin = E2BPlugin() - agent = Agent( - model="gemini-2.5-flash", - name="coder", - instruction="Write and run Python to answer the user. Verify with run_code.", - tools=plugin.get_tools(), - ) - app = App(name="demo", root_agent=agent, plugins=[plugin]) +- Python: [plugin configuration](packages/e2b-adk-python/README.md#configuration) +- TypeScript: [toolset configuration](packages/e2b-adk-js/README.md#configuration) - async with InMemoryRunner(app=app) as runner: - # run_debug prints the conversation as it runs. - await runner.run_debug("Compute the 20th Fibonacci number.") - - -asyncio.run(main()) -``` - -## Tools - -`plugin.get_tools()` returns six tools. Each returns a JSON-serializable dict -with a `success` flag — tools report failures in the result rather than raising, -so a bad call never aborts the agent run. `success` means the tool *ran*: code -that raised or a command that exited non-zero still returns `success: True` with -the failure captured in `error` / `exit_code`. Only a call that could not run at -all (sandbox unavailable, timeout) returns `success: False`. - -| Tool | Does | Key result fields | -|------|------|-------------------| -| `run_code` | Run code in a stateful kernel (variables persist across calls) | `stdout`, `stderr`, `text`, `error` | -| `run_command` | Run a shell command (each call is independent) | `stdout`, `stderr`, `exit_code` | -| `write_file` | Write a file in the sandbox | `path` | -| `read_file` | Read a file back | `path`, `content` | -| `list_files` | List a directory | `entries` (`[{name, type}]`) | -| `start_background_command` | Start a long-running process; optional preview URL for a port | `pid`, `preview_url` (+ `readiness` when `port` is set) | - -### Configuration - -`E2BPlugin` takes keyword-only options, all optional. Anything you don't set -falls back to the E2B SDK's own default — the plugin overrides none of them. - -```python -E2BPlugin( - # Common options - api_key=None, # defaults to the E2B_API_KEY env var - template=None, # E2B sandbox template - metadata=None, # dict[str, str] attached to the sandbox - envs=None, # dict[str, str] environment variables - timeout=None, # sandbox timeout in seconds (re-applied on every tool call) - plugin_name="e2b_plugin", - # Passed straight through to AsyncSandbox.create() — see the E2B SDK docs - secure=None, - allow_internet_access=None, - mcp=None, - network=None, - lifecycle=None, # e.g. pause/auto-resume; defaults to E2B's behavior - volume_mounts=None, - # **opts: any other AsyncSandbox.create() / connection param (proxy, request_timeout, …) -) -``` - -## Examples - -- [`examples/data_analysis.py`](examples/data_analysis.py) — a data-analysis - agent that saves a dataset and computes real answers with pandas instead of - guessing. -- [`examples/code_generator.py`](examples/code_generator.py) — a code generator - that executes every snippet before returning it. - -Run one with the credentials above: +## Development ```bash -uv run --extra examples python examples/data_analysis.py +pnpm install +pnpm check # both packages: lint, types, tests, build ``` -## v1 notes - -- **Sequential tool calls.** One sandbox is shared per plugin; calls are meant - to run one at a time, not concurrently. Concurrent calls are safe for the - sandbox lifecycle (creation is locked) but share one filesystem and kernel. -- **Text-only output.** Tools return text — `stdout`/`stderr`/file contents. - Rich results (charts, dataframes, images) are not surfaced yet. -- **Lazy, shared sandbox lifecycle.** The sandbox is created on the first tool - call and reused for the agent's lifetime, then killed when the runner exits. -- **Keep-alive while active, expiry when idle.** While a sandbox SDK operation - is active, a best-effort heartbeat refreshes its `timeout` (E2B's default is - 300s), including when a foreground call runs longer than that timeout. A - final refresh starts the full idle window when the operation ends. An idle - gap longer than `timeout` still expires the sandbox under E2B's default - lifecycle (`on_timeout: kill`), and later tool calls return failure results — - pass e.g. - `lifecycle={"on_timeout": {"action": "pause"}, "auto_resume": True}` to pause - and auto-resume across idle gaps instead. -- **Bounded output.** Output larger than 10 KB per field is truncated with a - `…[truncated N bytes]` marker so a single call can't flood the model's context. +The Python package uses [uv](https://docs.astral.sh/uv/); run `uv sync --extra dev` in `packages/e2b-adk-python` first. +Releases go through [changesets](.changeset/README.md): both packages share one version. ## License -MIT — see [LICENSE](LICENSE). +[MIT](LICENSE) diff --git a/package.json b/package.json new file mode 100644 index 0000000..227c797 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "e2b-adk-plugin", + "private": true, + "type": "module", + "packageManager": "pnpm@11.19.0", + "scripts": { + "build": "pnpm --filter @e2b/adk build", + "test": "pnpm --filter @e2b/adk test", + "test:py": "pnpm --filter @e2b/adk-python test", + "test:all": "pnpm test && pnpm test:py", + "check": "pnpm -r run check", + "changeset": "changeset", + "version": "changeset version && pnpm -r run postVersion && pnpm -r run lock", + "release": "pnpm build && pnpm --filter @e2b/adk-python run release && changeset publish" + }, + "devDependencies": { + "@changesets/cli": "2.31.1" + } +} diff --git a/packages/e2b-adk-js/.env.example b/packages/e2b-adk-js/.env.example new file mode 100644 index 0000000..28ef179 --- /dev/null +++ b/packages/e2b-adk-js/.env.example @@ -0,0 +1,3 @@ +E2B_API_KEY= +GOOGLE_API_KEY= +OPENAI_API_KEY= diff --git a/packages/e2b-adk-js/.prettierignore b/packages/e2b-adk-js/.prettierignore new file mode 100644 index 0000000..032d76f --- /dev/null +++ b/packages/e2b-adk-js/.prettierignore @@ -0,0 +1,7 @@ +pnpm-lock.yaml +dist +work +DECISIONS.md +EXECUTION_PLAN.md +VALIDATION.md +HANDOFF.md diff --git a/packages/e2b-adk-js/.prettierrc.json b/packages/e2b-adk-js/.prettierrc.json new file mode 100644 index 0000000..4c29eaa --- /dev/null +++ b/packages/e2b-adk-js/.prettierrc.json @@ -0,0 +1 @@ +{ "semi": false, "singleQuote": true, "trailingComma": "all" } diff --git a/packages/e2b-adk-js/LICENSE b/packages/e2b-adk-js/LICENSE new file mode 100644 index 0000000..d4b8c19 --- /dev/null +++ b/packages/e2b-adk-js/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 FoundryLabs, Inc. + +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/e2b-adk-js/README.md b/packages/e2b-adk-js/README.md new file mode 100644 index 0000000..6254300 --- /dev/null +++ b/packages/e2b-adk-js/README.md @@ -0,0 +1,124 @@ +# E2B for Google ADK (TypeScript) + +Give [Google ADK](https://github.com/google/adk-js) agents code, shell, file and background-process tools that run in an isolated [E2B](https://e2b.dev) sandbox. Each ADK session gets its own sandbox, which keeps files and kernel state across the messages of that conversation. + +## Install + +Requires Node.js 22 or later and an ESM project (`"type": "module"`). Tested with Google ADK 2.1.0. + +```bash +npm install @google/adk @e2b/adk +npm install --save-dev tsx typescript @types/node +``` + +```bash +export E2B_API_KEY="..." +export GOOGLE_API_KEY="..." +``` + +## Quick start + +```typescript +import { E2BToolset } from '@e2b/adk' +import { InMemoryRunner, LlmAgent } from '@google/adk' + +// One toolset per application, like the root agent. +const toolset = new E2BToolset() +const runner = new InMemoryRunner({ + agent: new LlmAgent({ + name: 'calculator', + model: 'gemini-2.5-flash', + instruction: 'Use run_code to verify calculations. Check its error field.', + tools: [toolset], + }), +}) + +const session = await runner.sessionService.createSession({ + appName: runner.appName, + userId: 'user', +}) +try { + for await (const event of runner.runAsync({ + userId: session.userId, + sessionId: session.id, + newMessage: { role: 'user', parts: [{ text: 'Compute 1234567 * 89.' }] }, + })) { + if (event.errorCode) throw new Error(`Agent failed: ${event.errorCode}`) + for (const part of event.content?.parts ?? []) + if (part.text && !part.thought) process.stdout.write(part.text) + } +} finally { + // Release the sandbox when the conversation is over. + await toolset.destroySession(session) +} +``` + +## Tools + +| Tool | Arguments | Returns | +| -------------------------- | --------------------------------------------- | ----------------------------------- | +| `run_code` | `code`, `language`, `envs`, `timeoutMs` | `stdout`, `stderr`, `text`, `error` | +| `run_command` | `command`, `cwd`, `envs`, `timeoutMs` | `stdout`, `stderr`, `exit_code` | +| `write_file` | `path`, `content` | `path` | +| `read_file` | `path` | `path`, `content` | +| `list_files` | `path` (default `.`) | `entries`, `truncated` | +| `start_background_command` | `command`, `cwd`, `envs`, `timeoutMs`, `port` | `pid`, `preview_url`, `readiness` | + +`run_code` takes `python` (default), `javascript`, `typescript`, `r`, `java` or `bash`, with one kernel per language per sandbox. + +- `success: true` means the operation ran. Code that raises returns an `error`; a nonzero exit returns `exit_code`. +- `success: false` means it could not run, for example invalid arguments or a transport failure. The agent loop keeps going. +- The call that creates a workspace returns `workspace: "new"`. If the sandbox expired, the next call returns `workspace_expired: true`, and the call after it starts an empty workspace. + +Tell the model to check these fields. + +## Sessions and cleanup + +Workspaces are keyed by ADK session (`appName`, `userId` and session `id`). The key is stored in the sandbox metadata, so any server instance, including a restarted one, finds the conversation's sandbox and connects to it. Several instances behind a load balancer can serve one conversation without session affinity. + +- **ADK's `close()` does not release a workspace.** ADK calls it after every run without saying which one ended, so the toolset treats it as a no-op. +- **Release sessions yourself.** Call `destroySession(session)` when a conversation ends, and `destroyAll()` on shutdown. +- **Billing.** A sandbox is billed from its first tool call until it is destroyed or times out. The default idle timeout is five minutes. + +Set `namespace` when staging and production share one E2B API key and app name, so each deployment finds only its own sandboxes. + +## Configuration + +```typescript +const toolset = new E2BToolset({ + template: 'your-template', + namespace: 'production', + maxOperationMs: 600_000, + sandboxOptions: { + timeoutMs: 300_000, + lifecycle: { onTimeout: 'pause', autoResume: true }, + metadata: { application: 'analyst' }, + allowInternetAccess: false, + }, +}) +``` + +- `template`: E2B template. It must include the interpreters you use and util-linux `setsid`. +- `sandboxOptions`: the E2B SDK creation options, except `template`. Unset options keep the SDK defaults. +- `maxOperationMs`: upper bound for one `run_code` or `run_command`, default 10 minutes. The model's `timeoutMs` cannot exceed it. +- `namespace`: separates deployments that share an API key and app name. + +With `lifecycle: { onTimeout: 'pause' }`, an idle sandbox is paused and resumed by the next call in its session, with files and kernel state intact. Paused sandboxes are not billed, but they stay until destroyed, so use pause only if your application calls `destroySession()`. + +## Direct code executor + +`E2BCodeExecutor` implements ADK's `BaseCodeExecutor` for direct `executeCode()` calls. It keeps kernel state until `close()`. See the [direct executor example](examples/code-executor.ts). + +Setting `LlmAgent({ codeExecutor })` alone does not run model-generated code in ADK 2.1.0. Use `E2BToolset` for an autonomous agent. + +## Examples and development + +- [Data analysis agent](examples/data-analysis.ts): two messages in one conversation; the CSV saved by the first is reused by the second. +- [Direct code executor](examples/code-executor.ts) + +```bash +pnpm install --frozen-lockfile +pnpm check +``` + +Unit tests use real ADK objects with mocked models and E2B SDK calls, so CI needs no credentials. Live checks use real E2B sandboxes and a model. Copy `.env.example` to `.env`, then run `pnpm test:live`. They are never part of `pnpm check` or CI. diff --git a/packages/e2b-adk-js/eslint.config.js b/packages/e2b-adk-js/eslint.config.js new file mode 100644 index 0000000..f5ba68f --- /dev/null +++ b/packages/e2b-adk-js/eslint.config.js @@ -0,0 +1,8 @@ +import js from '@eslint/js' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist/**', 'node_modules/**', 'work/**'] }, + js.configs.recommended, + ...tseslint.configs.recommended, +) diff --git a/packages/e2b-adk-js/examples/code-executor.ts b/packages/e2b-adk-js/examples/code-executor.ts new file mode 100644 index 0000000..618e506 --- /dev/null +++ b/packages/e2b-adk-js/examples/code-executor.ts @@ -0,0 +1,39 @@ +import { E2BCodeExecutor } from '@e2b/adk' +import { + CodeExecutionLanguage, + InvocationContext, + PluginManager, + createSession, +} from '@google/adk' + +// Direct calls: ADK 2.1.0 does not automatically execute model code blocks +// when only LlmAgent.codeExecutor is configured. Use E2BToolset for agents. +const invocationContext = new InvocationContext({ + invocationId: 'calculation', + pluginManager: new PluginManager(), + session: createSession({ + id: 'calculation', + appName: 'direct_executor', + userId: 'user', + }), +}) +const executor = new E2BCodeExecutor({ + sandboxOptions: { metadata: { example: 'adk-typescript-direct-executor' } }, +}) + +try { + for (const code of ['answer = 6 * 7', 'print(answer)']) { + const result = await executor.executeCode({ + invocationContext, + codeExecutionInput: { + code, + language: CodeExecutionLanguage.PYTHON, + inputFiles: [], + }, + }) + if (result.stderr) throw new Error(result.stderr) + process.stdout.write(result.stdout) + } +} finally { + await executor.close() +} diff --git a/packages/e2b-adk-js/examples/data-analysis.ts b/packages/e2b-adk-js/examples/data-analysis.ts new file mode 100644 index 0000000..46d1017 --- /dev/null +++ b/packages/e2b-adk-js/examples/data-analysis.ts @@ -0,0 +1,68 @@ +import { E2BToolset } from '@e2b/adk' +import { InMemoryRunner, LlmAgent, type Session } from '@google/adk' + +const dataset = `month,region,marketing_spend,revenue +2024-01,North,12000,48000 +2024-02,North,15000,60000 +2024-01,South,8000,24000 +2024-02,South,11000,33000` + +// One toolset and agent for the whole application; each session gets its own sandbox. +const toolset = new E2BToolset({ + sandboxOptions: { metadata: { example: 'adk-typescript-data-analysis' } }, +}) +const rootAgent = new LlmAgent({ + name: 'data_analyst', + model: 'gemini-2.5-flash', + instruction: + 'Files and Python variables persist for the whole conversation. ' + + 'Use write_file to save supplied data and run_code with Python to calculate answers. ' + + 'Check error and exit_code fields, fix failures and rerun. ' + + 'If a result has workspace_expired, earlier files are gone: ask for the data again. ' + + 'Never report a number you have not verified in the sandbox.', + tools: [toolset], + generateContentConfig: { + temperature: 0, + thinkingConfig: { thinkingBudget: 0 }, + }, +}) +const runner = new InMemoryRunner({ agent: rootAgent }) + +async function ask(session: Session, text: string): Promise { + let answer = '' + for await (const event of runner.runAsync({ + userId: session.userId, + sessionId: session.id, + newMessage: { role: 'user', parts: [{ text }] }, + runConfig: { maxLlmCalls: 8 }, + })) { + if (event.errorCode) throw new Error(`Agent failed: ${event.errorCode}`) + for (const part of event.content?.parts ?? []) { + if (part.text && !part.thought) answer += part.text + } + } + return answer +} + +const session = await runner.sessionService.createSession({ + appName: runner.appName, + userId: 'analyst', +}) +try { + console.log( + await ask( + session, + `${dataset}\nSave this CSV to /home/user/sales.csv. Calculate total revenue divided by total marketing spend for each region. Which region performs better?`, + ), + ) + // The follow-up reuses the saved file in the same sandbox. + console.log( + await ask( + session, + 'Using the same file, which month had the highest total revenue?', + ), + ) +} finally { + // ADK does not release session workspaces; the application does. + await toolset.destroySession(session) +} diff --git a/packages/e2b-adk-js/package.json b/packages/e2b-adk-js/package.json new file mode 100644 index 0000000..5ac1c9a --- /dev/null +++ b/packages/e2b-adk-js/package.json @@ -0,0 +1,61 @@ +{ + "name": "@e2b/adk", + "version": "0.1.1", + "description": "E2B sandbox tools and code execution for Google ADK.", + "type": "module", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/e2b-dev/e2b-adk-plugin.git", + "directory": "packages/e2b-adk-js" + }, + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=22" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "types": "./dist/index.d.ts", + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:live": "node --env-file=.env --import tsx scripts/live.ts", + "example:analysis": "node --env-file=.env --import tsx examples/data-analysis.ts", + "example:executor": "node --env-file=.env --import tsx examples/code-executor.ts", + "lint": "eslint .", + "format:check": "prettier --check .", + "format": "prettier --write .", + "check": "pnpm typecheck && pnpm lint && pnpm format:check && pnpm test && pnpm build", + "prepack": "pnpm build" + }, + "peerDependencies": { + "@google/adk": "^2.1.0" + }, + "dependencies": { + "@e2b/code-interpreter": "^2.8.0", + "zod": "^4.2.1" + }, + "devDependencies": { + "@eslint/js": "^9.0.0", + "@google/adk": "2.1.0", + "@types/node": "^22.0.0", + "eslint": "^9.0.0", + "prettier": "^3.0.0", + "tsx": "^4.20.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.0.0", + "vitest": "^3.2.4" + } +} diff --git a/packages/e2b-adk-js/scripts/live-processes.ts b/packages/e2b-adk-js/scripts/live-processes.ts new file mode 100644 index 0000000..b05a467 --- /dev/null +++ b/packages/e2b-adk-js/scripts/live-processes.ts @@ -0,0 +1,142 @@ +// Two separate Node processes serve one ADK session: the second must find the +// first one's sandbox by metadata, see its files and kernel state, and a destroy +// from either side must be visible to the other. +// +// node --env-file=.env --import tsx scripts/live-processes.ts +// +// The parent spawns itself twice as "processes" A and B, passing the session. +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { randomUUID } from 'node:crypto' +import { Context, InvocationContext, PluginManager } from '@google/adk' +import { Sandbox } from '@e2b/code-interpreter' +import { E2BToolset } from '../src/index.js' + +const [role, sessionId, runId] = process.argv.slice(2) + +async function tool( + toolset: E2BToolset, + name: string, + args: Record, +) { + const tools = await toolset.getTools() + const ctx = new InvocationContext({ + invocationId: randomUUID(), + pluginManager: new PluginManager(), + session: { + appName: 'live-processes', + userId: 'alice', + id: sessionId!, + state: {}, + events: [], + lastUpdateTime: 0, + }, + }) + return (await tools + .find((t) => t.name === name)! + .runAsync({ + args, + toolContext: new Context({ invocationContext: ctx }), + })) as Record +} + +const metadata = { adkLiveRun: runId! } +async function remaining() { + const paginator = Sandbox.list({ query: { metadata } }) + const ids: string[] = [] + while (paginator.hasNext) + ids.push(...(await paginator.nextItems()).map((b) => b.sandboxId)) + return ids +} + +if (role === 'A') { + // Process A: first message of the conversation. + const toolset = new E2BToolset({ + sandboxOptions: { timeoutMs: 180_000, metadata }, + }) + const w = await tool(toolset, 'write_file', { + path: '/home/user/from-a.txt', + content: 'written by A', + }) + assert.equal(w.workspace, 'new', JSON.stringify(w)) + const c = await tool(toolset, 'run_code', { code: 'from_a = 41' }) + assert.equal(c.success, true, JSON.stringify(c)) + const bg = await tool(toolset, 'start_background_command', { + command: 'sleep 600', + }) + assert.equal(bg.success, true) + console.log(`A done: ${(await remaining()).join(',')}`) +} else if (role === 'B') { + // Process B: the next message lands on another instance. + const toolset = new E2BToolset({ + sandboxOptions: { timeoutMs: 180_000, metadata }, + }) + const r = await tool(toolset, 'read_file', { path: '/home/user/from-a.txt' }) + assert.equal(r.content, 'written by A', JSON.stringify(r)) + assert.equal(r.workspace, undefined, 'B must reuse, not create') + const c = await tool(toolset, 'run_code', { code: 'print(from_a + 1)' }) + assert.equal(String(c.stdout).trim(), '42', JSON.stringify(c)) + const p = await tool(toolset, 'run_command', { + command: 'pgrep -xc sleep || true', + }) + assert.equal( + String(p.stdout).trim(), + '1', + "A's background process is visible", + ) + assert.equal((await remaining()).length, 1, 'still one sandbox') + await toolset.destroySession({ + appName: 'live-processes', + userId: 'alice', + id: sessionId!, + }) + assert.deepEqual(await remaining(), [], 'B destroyed the shared sandbox') + console.log('B done') +} else if (role === 'A2') { + // Process A again, after B destroyed the sandbox: its cached handle is stale. + const toolset = new E2BToolset({ + sandboxOptions: { timeoutMs: 180_000, metadata }, + }) + // No cache in a fresh process: the next message simply starts a new workspace. + const r = await tool(toolset, 'read_file', { path: '/home/user/from-a.txt' }) + assert.equal(r.workspace, 'new', JSON.stringify(r)) + assert.equal(r.success, false) + await toolset.destroySession({ + appName: 'live-processes', + userId: 'alice', + id: sessionId!, + }) + assert.deepEqual(await remaining(), []) + console.log('A2 done') +} else { + const session = randomUUID() + const run = randomUUID() + const step = (r: string) => { + const out = spawnSync( + process.execPath, + [ + '--env-file=.env', + '--import', + 'tsx', + 'scripts/live-processes.ts', + r, + session, + run, + ], + { encoding: 'utf8' }, + ) + process.stdout.write(out.stdout) + if (out.status !== 0) { + process.stderr.write(out.stderr) + throw new Error(`process ${r} failed`) + } + } + try { + step('A') + step('B') + step('A2') + console.log('PASS two processes shared one session sandbox and cleaned up') + } finally { + for (const id of await remaining()) await Sandbox.kill(id) + } +} diff --git a/packages/e2b-adk-js/scripts/live.ts b/packages/e2b-adk-js/scripts/live.ts new file mode 100644 index 0000000..0f920b0 --- /dev/null +++ b/packages/e2b-adk-js/scripts/live.ts @@ -0,0 +1,597 @@ +import assert from 'node:assert/strict' +import { randomUUID } from 'node:crypto' +import { setTimeout as sleep } from 'node:timers/promises' +import { + CodeExecutionLanguage, + Context, + FunctionTool, + InMemoryRunner, + InvocationContext, + LlmAgent, + PluginManager, + type Event, +} from '@google/adk' +import { Sandbox } from '@e2b/code-interpreter' +import { z } from 'zod' +import { E2BCodeExecutor, E2BToolset } from '../src/index.js' +import { OpenAIModel } from './openai-model.js' + +assert(process.env.E2B_API_KEY, 'E2B_API_KEY is required') +const runId = randomUUID() +const metadata = { adkLiveRun: runId } +const options = (extra: Record = {}) => ({ + sandboxOptions: { timeoutMs: 120_000, metadata, ...extra }, +}) +type Result = Record + +function invocation(id: string, userId = 'live', abortSignal?: AbortSignal) { + return new InvocationContext({ + invocationId: randomUUID(), + pluginManager: new PluginManager(), + abortSignal, + session: { + appName: 'live', + userId, + id, + state: {}, + events: [], + lastUpdateTime: 0, + }, + }) +} + +async function remaining() { + const paginator = Sandbox.list({ query: { metadata } }) + const ids: string[] = [] + while (paginator.hasNext) + ids.push(...(await paginator.nextItems()).map((box) => box.sandboxId)) + return ids +} + +function client(toolset: E2BToolset) { + const tools = toolset.getTools() + const raw = async ( + name: string, + args: Result, + ctx: InvocationContext, + ): Promise => + (await ( + await tools + ) + .find((tool) => tool.name === name)! + .runAsync({ + args, + toolContext: new Context({ invocationContext: ctx }), + })) as Result + const call = async (name: string, args: Result, ctx: InvocationContext) => { + const result = await raw(name, args, ctx) + assert.equal(result.success, true, `${name}: ${JSON.stringify(result)}`) + return result + } + /** Marker-file line count and matching processes, from inside the sandbox. */ + const probe = async (ctx: InvocationContext, file: string, pattern: string) => + String( + ( + await call( + 'run_command', + { + command: `wc -l < ${file} 2>/dev/null || echo 0; pgrep -fc '${pattern}' || true`, + }, + ctx, + ) + ).stdout, + ) + .trim() + .split('\n') + .map(Number) + /** Assert that writes to `file` stopped and no process matches `pattern`. */ + const stopped = async ( + ctx: InvocationContext, + file: string, + pattern: string, + ) => { + const [before] = await probe(ctx, file, pattern) + await sleep(2000) + const [after, processes] = await probe(ctx, file, pattern) + assert.equal(after, before, `${file} still growing`) + // pgrep -f also matches the probing shell itself. + assert(processes! <= 1, `${processes} processes match ${pattern}`) + } + return { raw, call, stopped } +} + +async function directTools() { + const toolset = new E2BToolset(options()) + const { call } = client(toolset) + const ctx = invocation(runId) + const first = await call( + 'write_file', + { path: '/home/user/adk-smoke.txt', content: 'smoke-content' }, + ctx, + ) + assert.equal(first.workspace, 'new') + assert.equal( + (await call('read_file', { path: '/home/user/adk-smoke.txt' }, ctx)) + .content, + 'smoke-content', + ) + assert.match( + JSON.stringify(await call('list_files', { path: '/home/user' }, ctx)), + /adk-smoke.txt/, + ) + const exit = await call( + 'run_command', + { command: "printf 'shell ok'; echo err >&2; exit 7", cwd: '/tmp' }, + ctx, + ) + assert.deepEqual( + [exit.stdout, exit.stderr, exit.exit_code], + ['shell ok', 'err\n', 7], + ) + // A start the sandbox refuses is an ordinary, correctable error. + for (const name of ['run_command', 'start_background_command']) { + const refused = await client(toolset).raw( + name, + { command: 'pwd', cwd: '/does/not/exist' }, + ctx, + ) + assert.equal( + refused.error, + 'Sandbox operation failed (InvalidArgumentError).', + JSON.stringify(refused), + ) + } + assert.equal( + (await call('run_command', { command: 'pwd', cwd: '/tmp' }, ctx)).stdout, + '/tmp\n', + ) + console.log('PASS a refused start (missing cwd) does not block the workspace') + const samples: Record = { + python: ['v = 6 * 7', 'print(v)'], + javascript: ['var v = 6 * 7', 'console.log(v)'], + typescript: ['var v: number = 6 * 7', 'console.log(v)'], + r: ['v <- 6 * 7', 'cat(v)'], + java: ['int v = 6 * 7;', 'System.out.println(v);'], + bash: ['v=$((6 * 7))', 'echo $v'], + } + for (const [language, [set, get]] of Object.entries(samples)) { + await call('run_code', { code: set, language, timeoutMs: 30_000 }, ctx) + const result = await call('run_code', { code: get, language }, ctx) + assert.equal(result.error, '', `${language} execution error`) + assert.match(`${result.stdout}${result.text}`, /42/) + console.log(`PASS run_code ${language} keeps kernel state`) + } + const runtimeError = await call( + 'run_code', + { code: 'raise ValueError("expected")' }, + ctx, + ) + assert.match(String(runtimeError.error), /ValueError/) + const bg = await call( + 'start_background_command', + { command: 'python3 -m http.server 3000', port: 3000 }, + ctx, + ) + assert.equal(typeof bg.pid, 'number') + assert.match(String(bg.preview_url), /^https:\/\//) + await sleep(2000) + const served = await fetch(String(bg.preview_url)) + assert.equal(served.status, 200, 'preview URL serves the background process') + console.log('PASS preview URL reaches the background server') + assert.equal((await remaining()).length, 1) + await toolset.destroySession(ctx.session) + assert.deepEqual(await remaining(), []) + console.log('PASS six tools, state, errors, background and destroySession') +} + +async function stopMechanisms() { + const toolset = new E2BToolset(options()) + const { raw, call, stopped } = client(toolset) + const ctx = invocation(runId) + const loop = (file: string) => + `(while :; do echo x >> ${file}; sleep 0.2; done) & sleep 600` + + let result = await raw( + 'run_command', + { command: loop('/tmp/deadline'), timeoutMs: 3000 }, + ctx, + ) + assert.equal(result.timed_out, true, JSON.stringify(result)) + await stopped(ctx, '/tmp/deadline', 'sleep 600') + console.log('PASS command deadline kills the command and its children') + + for (const delay of [20, 1500]) { + const abort = new AbortController() + setTimeout(() => abort.abort(), delay) + const started = Date.now() + result = await raw( + 'run_command', + { command: loop(`/tmp/abort${delay}`) }, + invocation(runId, 'live', abort.signal), + ) + assert.equal(result.aborted, true, JSON.stringify(result)) + assert(Date.now() - started < 10_000, 'abort returns promptly') + await stopped(ctx, `/tmp/abort${delay}`, 'sleep 600') + console.log(`PASS command abort after ${delay} ms leaves no process`) + } + + // The deadline covers the start request: a 1 ms limit kills the late handle. + result = await raw( + 'run_command', + { command: loop('/tmp/start'), timeoutMs: 1 }, + ctx, + ) + assert.equal(result.timed_out, true, JSON.stringify(result)) + await stopped(ctx, '/tmp/start', 'sleep 600') + console.log('PASS a deadline during command start kills the late process') + + // ...and kernel start-up: the request was already sent. Either the kernel + // that comes up is restarted, or the code finished before it was listed + // and its result is returned; "did not run" is never claimed. + result = await raw( + 'run_code', + { code: 'System.out.println(1);', language: 'java', timeoutMs: 1 }, + ctx, + ) + assert.ok( + (result.timed_out === true && result.kernel_restarted === true) || + (result.success === true && result.stdout === '1\n'), + JSON.stringify(result), + ) + const afterStart = await call( + 'run_code', + { code: 'System.out.println(2);', language: 'java' }, + ctx, + ) + assert.match(String(afterStart.stdout), /2/) + console.log( + 'PASS a deadline during kernel start-up never claims the code did not run', + ) + + const loops: Record = { + python: ['v = 1', 'while True:\n pass', 'print(v)'], + javascript: ['var v = 1', 'while (true) {}', 'console.log(v)'], + typescript: ['var v: number = 1', 'while (true) {}', 'console.log(v)'], + r: ['v <- 1', 'repeat {}', 'print(v)'], + java: ['int v = 1;', 'while (true) {}', 'System.out.println(v);'], + bash: ['v=1', 'while :; do :; done', 'echo "v=[$v]"'], + } + for (const [language, [set, spin, get]] of Object.entries(loops)) { + await call('run_code', { code: set, language, timeoutMs: 30_000 }, ctx) + result = await raw( + 'run_code', + { code: spin, language, timeoutMs: 3000 }, + ctx, + ) + assert.equal( + result.timed_out, + true, + `${language}: ${JSON.stringify(result)}`, + ) + assert.equal(result.kernel_restarted, true) + const started = Date.now() + const after = await call('run_code', { code: get, language }, ctx) + assert( + Date.now() - started < 15_000, + `${language} kernel responds promptly`, + ) + assert.doesNotMatch( + `${after.stdout}${after.text}`, + /\b1\b/, + `${language} state was reset`, + ) + console.log( + `PASS ${language} deadline restarts the kernel (${Date.now() - started} ms)`, + ) + } + + const abort = new AbortController() + setTimeout(() => abort.abort(), 1500) + result = await raw( + 'run_code', + { + code: "import subprocess\nsubprocess.Popen(['bash', '-c', 'while :; do echo x >> /tmp/child; sleep 0.2; done'])\nwhile True:\n pass", + }, + invocation(runId, 'live', abort.signal), + ) + assert.equal(result.aborted, true, JSON.stringify(result)) + await stopped(ctx, '/tmp/child', 'tmp/child') + console.log( + 'PASS code abort restarts the kernel and stops its child processes', + ) + + await toolset.destroySession(ctx.session) + assert.deepEqual(await remaining(), []) +} + +async function lifecycle() { + // L3: a disconnected background process outlives the SDK's 60 s default. + { + const toolset = new E2BToolset(options()) + const { call } = client(toolset) + const ctx = invocation(randomUUID()) + await call( + 'start_background_command', + { command: 'while :; do echo x >> /tmp/bg; sleep 1; done' }, + ctx, + ) + await sleep(70_000) + const count = Number( + (await call('run_command', { command: 'wc -l < /tmp/bg' }, ctx)).stdout, + ) + assert(count >= 65, `background process stopped after ${count} s`) + console.log(`PASS background process alive after 70 s (${count} lines)`) + await toolset.destroyAll() + } + + // L1/L2: a paused autoResume sandbox keeps files and kernel state. + { + const toolset = new E2BToolset( + options({ lifecycle: { onTimeout: 'pause', autoResume: true } }), + ) + const { raw, call } = client(toolset) + const ctx = invocation(randomUUID()) + await call( + 'write_file', + { path: '/home/user/keep.txt', content: 'kept' }, + ctx, + ) + await call('run_code', { code: 'kept = 41' }, ctx) + const [id] = await remaining() + assert(await Sandbox.pause(id!), 'pause') + console.log(`INFO paused ${id}: ${(await Sandbox.getInfo(id!)).state}`) + const read = await raw('read_file', { path: '/home/user/keep.txt' }, ctx) + console.log( + `INFO first call on the paused sandbox: ${JSON.stringify(read)}`, + ) + assert.equal(read.content, 'kept', 'files survive pause/resume') + assert.equal( + (await call('run_code', { code: 'kept + 1' }, ctx)).text, + '42', + 'kernel survives pause/resume', + ) + console.log('PASS pause/autoResume keeps files and kernel state') + await Sandbox.pause(id!) + await toolset.destroyAll() + assert.deepEqual(await remaining(), [], 'destroyAll kills a paused sandbox') + console.log('PASS destroyAll removes a paused sandbox') + } + + // L5: TTL expiry is explicit, then a new workspace. + { + const toolset = new E2BToolset(options({ timeoutMs: 10_000 })) + const { raw, call } = client(toolset) + const ctx = invocation(randomUUID()) + await call('write_file', { path: '/home/user/x.txt', content: 'x' }, ctx) + await sleep(25_000) + const expired = await raw('read_file', { path: '/home/user/x.txt' }, ctx) + assert.equal(expired.workspace_expired, true, JSON.stringify(expired)) + const fresh = await raw('read_file', { path: '/home/user/x.txt' }, ctx) + assert.equal(fresh.workspace, 'new', JSON.stringify(fresh)) + assert.equal(fresh.success, false, 'the new workspace is empty') + console.log('PASS TTL expiry reported once, then a new empty workspace') + await toolset.destroyAll() + } + assert.deepEqual(await remaining(), []) +} + +async function directExecutor() { + const executor = new E2BCodeExecutor(options()) + const ctx = invocation(runId) + try { + for (const [language, code] of [ + [CodeExecutionLanguage.PYTHON, 'print(21 * 2)'], + [CodeExecutionLanguage.JAVASCRIPT, 'console.log(21 * 2)'], + [ + CodeExecutionLanguage.TYPESCRIPT, + 'const v: number = 21 * 2; console.log(v)', + ], + [CodeExecutionLanguage.SHELL, 'echo $((21 * 2))'], + ] as const) { + const result = await executor.executeCode({ + invocationContext: ctx, + codeExecutionInput: { code, language, inputFiles: [] }, + }) + assert.equal(result.stderr, '', `${language}: ${result.stderr}`) + assert.match(result.stdout, /42/) + console.log(`PASS executor ${language}`) + } + } finally { + await executor.close() + } + assert.deepEqual(await remaining(), []) +} + +/** L6: the adk web / api_server shape, driven by Gemini as real users would. */ +async function agent() { + const toolset = new E2BToolset(options()) + let lastRequest = 0 + const deployed: string[] = [] + const deploy = new FunctionTool({ + name: 'deploy_report', + description: 'Publish a report file. Requires human approval.', + parameters: z.object({ path: z.string() }), + requireConfirmation: true, + execute: async ({ path }) => { + deployed.push(path) + return { published: path } + }, + }) + const rootAgent = new LlmAgent({ + name: 'analyst', + // LIVE_MODEL=openai uses a small OpenAI model when Gemini is rate limited. + model: + process.env.LIVE_MODEL === 'openai' + ? new OpenAIModel() + : 'gemini-2.5-flash', + instruction: + 'You work in a sandbox that persists for the whole conversation. Always use the tools; never guess file contents. Report tool results exactly.', + tools: [toolset, deploy], + generateContentConfig: { + temperature: 0, + thinkingConfig: { thinkingBudget: 0 }, + }, + // Pace requests below free-tier rate limits; this is not a retry. + beforeModelCallback: async () => { + if (process.env.LIVE_MODEL === 'openai') return undefined + const wait = lastRequest + 13_000 - Date.now() + lastRequest = Math.max(Date.now(), lastRequest + 13_000) + if (wait > 0) await sleep(wait) + return undefined + }, + }) + const runner = new InMemoryRunner({ agent: rootAgent, appName: 'live' }) + const say = async ( + userId: string, + sessionId: string, + text: string | Event['content'], + abortSignal?: AbortSignal, + ) => { + const events: Event[] = [] + for await (const event of runner.runAsync({ + userId, + sessionId, + newMessage: + typeof text === 'string' ? { role: 'user', parts: [{ text }] } : text!, + runConfig: { maxLlmCalls: 10 }, + abortSignal, + })) { + if (event.errorCode) + throw new Error(`Model error ${event.errorCode}: ${event.errorMessage}`) + events.push(event) + } + const parts = events.flatMap((e) => e.content?.parts ?? []) + return { + parts, + text: parts + .filter((p) => p.text && !p.thought) + .map((p) => p.text) + .join(''), + tools: parts + .filter((p) => p.functionResponse) + .map((p) => p.functionResponse!), + } + } + const users = await Promise.all( + ['alice', 'bob'].map((userId) => + runner.sessionService.createSession({ appName: 'live', userId }), + ), + ) + + const firstTurn = await Promise.all( + users.map((s) => + say( + s.userId, + s.id, + `Write the text "${s.userId}-secret-${runId.slice(0, 6)}" to /home/user/note.txt, and store the number ${s.userId.length * 7} in a Python variable named n.`, + ), + ), + ) + for (const turn of firstTurn) + assert( + turn.tools.some((t) => t.name === 'write_file' && t.response?.success), + ) + assert.equal((await remaining()).length, 2, 'one sandbox per session') + console.log('PASS two concurrent users got separate workspaces') + + for (const s of users) { + const turn = await say( + s.userId, + s.id, + 'Read /home/user/note.txt and print the Python variable n. Reply with both values.', + ) + const other = s.userId === 'alice' ? 'bob' : 'alice' + assert.match(turn.text, new RegExp(`${s.userId}-secret`)) + assert.doesNotMatch(turn.text, new RegExp(`${other}-secret`)) + assert.match(turn.text, new RegExp(String(s.userId.length * 7))) + console.log( + `PASS ${s.userId}: files and kernel persisted into the next message`, + ) + } + + const alice = users[0]! + const paused = await say( + alice.userId, + alice.id, + 'Write "quarterly: 42" to /home/user/report.txt, then publish it with deploy_report, then read it back and tell me its content.', + ) + const request = paused.parts.find( + (p) => p.functionCall?.name === 'adk_request_confirmation', + )?.functionCall + assert(request, 'deploy_report asked for confirmation') + const resumed = await say(alice.userId, alice.id, { + role: 'user', + parts: [ + { + functionResponse: { + id: request.id, + name: request.name, + response: { confirmed: true }, + }, + }, + ], + }) + assert.deepEqual(deployed, ['/home/user/report.txt']) + assert.match(resumed.text + JSON.stringify(resumed.tools), /quarterly: 42/) + console.log('PASS workspace survived the confirmation pause') + + const bob = users[1]! + const abort = new AbortController() + const started = Date.now() + const cancelled = say( + bob.userId, + bob.id, + 'Run exactly this shell command with run_command and timeoutMs 300000: touch /tmp/started && sleep 240', + abort.signal, + ) + setTimeout(() => abort.abort(), 8000) + await cancelled + assert(Date.now() - started < 30_000, 'cancelled run returned promptly') + const { call } = client(toolset) + const left = await call( + 'run_command', + { + command: 'test -f /tmp/started && echo started; pgrep -xc sleep || true', + }, + invocation(bob.id, bob.userId), + ) + assert.equal( + String(left.stdout).trim(), + 'started\n0', + 'the command ran and no sleep process is left', + ) + console.log('PASS runner abort stopped the command in the sandbox') + + await toolset.destroyAll() + assert.deepEqual(await remaining(), []) + console.log('PASS destroyAll released every session workspace') +} + +const modes: Record Promise> = { + direct: directTools, + stop: stopMechanisms, + executor: directExecutor, + lifecycle, + agent, +} +let failed = false +try { + const selected = process.argv[2] ?? 'all' + for (const [name, run] of Object.entries(modes)) + if (selected === 'all' || selected.split(',').includes(name)) await run() +} catch (error) { + failed = true + if (/429|RESOURCE_EXHAUSTED/.test(String(error))) + console.log('INCONCLUSIVE: model rate limit (HTTP 429)') + throw error +} finally { + // Only clean up this run's unique metadata-tagged workspaces. + const leaked = await remaining() + for (const id of leaked) await Sandbox.kill(id) + if (!failed) + assert.equal( + leaked.length, + 0, + 'Fallback cleanup was needed: investigate lifecycle', + ) +} diff --git a/packages/e2b-adk-js/scripts/openai-model.ts b/packages/e2b-adk-js/scripts/openai-model.ts new file mode 100644 index 0000000..84d47b1 --- /dev/null +++ b/packages/e2b-adk-js/scripts/openai-model.ts @@ -0,0 +1,148 @@ +// Test-only: ADK 2.1.0 has no OpenAI provider. Used by the live harness when +// Gemini is rate limited. Not part of the published package. +import { + BaseLlm, + type BaseLlmConnection, + type LlmRequest, + type LlmResponse, +} from '@google/adk' + +type Json = Record + +/** Gemini Schema (uppercase types, string lengths) to JSON Schema. */ +function jsonSchema(schema: unknown): unknown { + if (Array.isArray(schema)) return schema.map(jsonSchema) + if (!schema || typeof schema !== 'object') return schema + const out: Json = {} + for (const [key, value] of Object.entries(schema)) { + if (key === 'type' && typeof value === 'string') + out[key] = value.toLowerCase() + else if (/^(min|max)(Length|Items)$/.test(key)) out[key] = Number(value) + else out[key] = jsonSchema(value) + } + return out +} + +export class OpenAIModel extends BaseLlm { + constructor(model = 'gpt-4.1-mini') { + super({ model }) + } + + override async *generateContentAsync( + request: LlmRequest, + ): AsyncGenerator { + const messages: Json[] = [] + const system = request.config?.systemInstruction as + string | { parts?: { text?: string }[] } | undefined + const instruction = + typeof system === 'string' + ? system + : system?.parts?.map((p) => p.text ?? '').join('') + if (instruction) messages.push({ role: 'system', content: instruction }) + const pending = new Set() + for (const content of request.contents) { + const parts = content.parts ?? [] + const text = parts + .filter((p) => p.text && !p.thought) + .map((p) => p.text) + .join('') + if (content.role === 'model') { + const calls = parts.filter((p) => p.functionCall) + for (const p of calls) pending.add(p.functionCall!.id!) + if (text || calls.length) + messages.push({ + role: 'assistant', + content: text || null, + ...(calls.length && { + tool_calls: calls.map((p) => ({ + id: p.functionCall!.id, + type: 'function', + function: { + name: p.functionCall!.name, + arguments: JSON.stringify(p.functionCall!.args ?? {}), + }, + })), + }), + }) + } else { + for (const p of parts.filter((p) => p.functionResponse)) { + const id = p.functionResponse!.id! + // Framework-internal responses (e.g. confirmations) have no model call. + if (!pending.delete(id)) continue + messages.push({ + role: 'tool', + tool_call_id: id, + content: JSON.stringify(p.functionResponse!.response), + }) + } + if (text) messages.push({ role: 'user', content: text }) + } + } + const tools = (request.config?.tools ?? []) + .flatMap((t) => + 'functionDeclarations' in t ? (t.functionDeclarations ?? []) : [], + ) + .map((d) => ({ + type: 'function', + function: { + name: d.name, + description: d.description, + parameters: jsonSchema( + d.parameters ?? + d.parametersJsonSchema ?? { type: 'object', properties: {} }, + ), + }, + })) + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + authorization: `Bearer ${process.env.OPENAI_API_KEY}`, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model: this.model, + messages, + temperature: 0, + ...(tools.length && { tools, parallel_tool_calls: false }), + }), + }) + if (!response.ok) { + const body = (await response.json().catch(() => ({}))) as { + error?: { message?: string } + } + throw new Error(`OpenAI ${response.status}: ${body.error?.message ?? ''}`) + } + const message = ( + (await response.json()) as { + choices: { + message: { + content: string | null + tool_calls?: { + id: string + function: { name: string; arguments: string } + }[] + } + }[] + } + ).choices[0]!.message + yield { + content: { + role: 'model', + parts: [ + ...(message.content ? [{ text: message.content }] : []), + ...(message.tool_calls ?? []).map((call) => ({ + functionCall: { + id: call.id, + name: call.function.name, + args: JSON.parse(call.function.arguments) as Json, + }, + })), + ], + }, + } + } + + override async connect(): Promise { + throw new Error('Live streaming is not supported by this test adapter') + } +} diff --git a/packages/e2b-adk-js/src/code-executor.ts b/packages/e2b-adk-js/src/code-executor.ts new file mode 100644 index 0000000..8252c71 --- /dev/null +++ b/packages/e2b-adk-js/src/code-executor.ts @@ -0,0 +1,113 @@ +import { + BaseCodeExecutor, + CodeExecutionLanguage, + type CodeExecutionResult, + type InvocationContext, +} from '@google/adk' +import type { E2BCodeExecutorOptions } from './options.js' +import { kernelLost, operationError, truncate } from './results.js' +import { sessionTag } from './toolset.js' +import { operationLimit, Workspace } from './workspace.js' + +type ExecuteParams = Parameters[0] + +/** Stateful code execution in one E2B workspace, owned by one ADK session. */ +export class E2BCodeExecutor extends BaseCodeExecutor { + private workspace?: Workspace + private readonly timeoutMs: number + private sessionKey?: string + + constructor(private readonly options: E2BCodeExecutorOptions = {}) { + super() + // Executor calls carry no limit: keep the SDK's 60 s default, capped by maxOperationMs. + this.timeoutMs = Math.min(60_000, operationLimit(options)) + this.stateful = true + // ADK's response processor currently labels extracted blocks as Python. + this.codeBlockDelimiters = [ + ['```tool_code\n', '\n```'], + ['```python\n', '\n```'], + ] + } + + override async executeCode({ + invocationContext, + codeExecutionInput: input, + }: ExecuteParams): Promise { + const fail = (stderr: string): CodeExecutionResult => ({ + stdout: '', + stderr, + outputFiles: [], + }) + if ( + input.inputFiles?.length || + (input.args && Object.keys(input.args).length) + ) { + return fail( + 'inputFiles and args are not supported. Use E2BToolset file and command tools.', + ) + } + const language = { + [CodeExecutionLanguage.PYTHON]: 'python', + [CodeExecutionLanguage.JAVASCRIPT]: 'javascript', + [CodeExecutionLanguage.TYPESCRIPT]: 'typescript', + [CodeExecutionLanguage.SHELL]: 'bash', + [CodeExecutionLanguage.UNSPECIFIED]: undefined, + [CodeExecutionLanguage.POWERSHELL]: undefined, + [CodeExecutionLanguage.WINDOWS_CMD]: undefined, + }[input.language] + if (typeof language !== 'string') + return fail( + 'Unsupported language. Use python, javascript, typescript or shell.', + ) + if (typeof input.code !== 'string' || !input.code.trim()) + return fail('Code must be a non-empty string.') + const key = this.keyFor(invocationContext) + if (this.sessionKey && this.sessionKey !== key) { + return fail( + 'This executor belongs to another ADK session. Create a separate executor for each session.', + ) + } + this.sessionKey = key + this.workspace ??= new Workspace( + this.options, + 'Retry close() before using this executor.', + sessionTag(invocationContext.session, this.options.namespace), + ) + try { + const outcome = await this.workspace.runCode( + input.code, + language, + undefined, + { created: false }, + { signal: invocationContext.abortSignal, timeoutMs: this.timeoutMs }, + ) + if ('stopped' in outcome) + return fail( + `Execution ${outcome.stopped === 'timed_out' ? `exceeded ${this.timeoutMs} ms` : 'was cancelled'}. The kernel was restarted; its variables and imports were lost.`, + ) + if ('kernelLost' in outcome) return fail(kernelLost(language, outcome)) + const { execution } = outcome + const text = execution.text ? `\n${execution.text}` : '' + const error = execution.error + ? `\n${execution.error.name}: ${execution.error.value}\n${execution.error.traceback}` + : '' + return { + stdout: truncate(execution.logs.stdout.join('') + text), + stderr: truncate(execution.logs.stderr.join('') + error), + outputFiles: [], + } + } catch (error) { + return fail(operationError(error)) + } + } + + private keyFor(context: InvocationContext): string { + const { appName, userId, id } = context.session + return JSON.stringify([appName, userId, id]) + } + + /** Releases the workspace; a later call starts a new one. The instance stays bound to its original session. */ + async close(): Promise { + await this.workspace?.destroy() + } +} diff --git a/packages/e2b-adk-js/src/index.ts b/packages/e2b-adk-js/src/index.ts new file mode 100644 index 0000000..7aa11f2 --- /dev/null +++ b/packages/e2b-adk-js/src/index.ts @@ -0,0 +1,7 @@ +export { E2BCodeExecutor } from './code-executor.js' +export { E2BToolset } from './toolset.js' +export type { + E2BCodeExecutorOptions, + E2BToolsetOptions, + E2BSandboxOptions, +} from './options.js' diff --git a/packages/e2b-adk-js/src/options.ts b/packages/e2b-adk-js/src/options.ts new file mode 100644 index 0000000..380c6d2 --- /dev/null +++ b/packages/e2b-adk-js/src/options.ts @@ -0,0 +1,21 @@ +import type { SandboxOpts } from '@e2b/code-interpreter' + +/** E2B creation options. Unspecified settings retain the SDK defaults. */ +export interface E2BSandboxOptions { + template?: string + sandboxOptions?: Omit +} + +export interface E2BToolsetOptions extends E2BSandboxOptions { + /** Longest a single code or command execution may run, in milliseconds. Default 600_000. */ + maxOperationMs?: number + /** + * Separates the sandboxes of deployments that share one E2B API key and use + * the same ADK app name, such as staging and production. Part of the sandbox + * metadata used to find, reuse and destroy a session's sandbox; 'default' + * when unset. + */ + namespace?: string +} + +export type E2BCodeExecutorOptions = E2BToolsetOptions diff --git a/packages/e2b-adk-js/src/results.ts b/packages/e2b-adk-js/src/results.ts new file mode 100644 index 0000000..180820b --- /dev/null +++ b/packages/e2b-adk-js/src/results.ts @@ -0,0 +1,75 @@ +const OUTPUT_LIMIT = 10_000 +const TRUNCATED = '\n[truncated]' + +export class SandboxCleanupError extends Error { + constructor(hint = '') { + super(`Sandbox cleanup failed. ${hint}`.trim()) + } +} + +/** The sandbox disappeared (TTL expiry or external kill); its state is gone. */ +export class WorkspaceExpiredError extends Error { + constructor() { + super( + 'The sandbox for this conversation expired or was deleted; its files, kernels and processes are gone. The next call starts a new, empty workspace.', + ) + } +} + +export class WorkspaceDestroyedError extends Error { + constructor() { + super( + 'The application released this workspace while the operation was pending.', + ) + } +} + +export class WorkspaceBlockedError extends Error {} + +export class OperationAbortedError extends Error { + constructor() { + super('The operation was cancelled before it started.') + } +} + +export function truncate(value: string): string { + return value.length <= OUTPUT_LIMIT + ? value + : value.slice(0, OUTPUT_LIMIT - TRUNCATED.length) + TRUNCATED +} + +export function failure(error: string, fields: Record = {}) { + return { success: false as const, error, ...fields } +} + +/** The error's class name if it looks like one, never its message. */ +export function errorName(error: unknown): string { + const name = error instanceof Error ? error.name : '' + return /^[A-Za-z]{1,60}(Error|Exception)$/.test(name) ? name : '' +} + +/** Adapter errors carry safe text; SDK messages may contain bodies or credentials. */ +export function operationError(error: unknown): string { + if ( + error instanceof SandboxCleanupError || + error instanceof WorkspaceExpiredError || + error instanceof WorkspaceDestroyedError || + error instanceof WorkspaceBlockedError || + error instanceof OperationAbortedError + ) + return error.message + const name = errorName(error) + return `Sandbox operation failed${name ? ` (${name})` : ''}.` +} + +/** Tell the caller its kernel state is gone; the sandbox's files are not. */ +export function kernelLost( + language: string, + outcome: { kernelLost: 'restarted' | 'missing'; cause: unknown }, +): string { + const name = errorName(outcome.cause) + const cause = name ? ` (${name})` : '' + return outcome.kernelLost === 'restarted' + ? `The connection to the ${language} kernel was lost${cause}, so it was restarted: its variables and imports were lost. Files in the sandbox are kept.` + : `The ${language} kernel state is no longer available${cause}; the next call starts an empty kernel. Files in the sandbox are kept.` +} diff --git a/packages/e2b-adk-js/src/toolset.ts b/packages/e2b-adk-js/src/toolset.ts new file mode 100644 index 0000000..31f104a --- /dev/null +++ b/packages/e2b-adk-js/src/toolset.ts @@ -0,0 +1,354 @@ +import { + BaseToolset, + FunctionTool, + type BaseTool, + type Session, + type ToolOptions, +} from '@google/adk' +import { z } from 'zod' +import type { E2BToolsetOptions } from './options.js' +import { + failure, + kernelLost, + OperationAbortedError, + operationError, + SandboxCleanupError, + truncate, + WorkspaceExpiredError, +} from './results.js' +import { operationLimit, Workspace, type RunInfo } from './workspace.js' + +/** SDK default for code and command execution when the model sets no limit. */ +const DEFAULT_TIMEOUT_MS = 60_000 +/** Always tagged: E2B's list matches any sandbox that has the given pairs. */ +const DEFAULT_NAMESPACE = 'default' +const CLEANUP_HINT = + 'The application must retry destroySession() before this workspace can be used.' + +type SessionRef = Pick +type Execute = ( + input: z.infer, + workspace: Workspace, + info: RunInfo, + signal?: AbortSignal, +) => Promise> + +const path = z.string().min(1).describe('Path inside the sandbox.') +const envs = z + .record(z.string(), z.string()) + .optional() + .describe('Environment variables for this operation only.') +const command = { + command: z + .string() + .min(1) + .describe('Shell command to execute in the sandbox.'), + cwd: z + .string() + .min(1) + .optional() + .describe('Working directory inside the sandbox.'), + envs, +} + +/** Convert validation failures as well as execution failures into tool responses. */ +class SafeFunctionTool extends FunctionTool { + override async runAsync(request: Parameters[0]) { + try { + return await super.runAsync(request) + } catch { + return failure( + 'Invalid tool arguments. Check the declared argument names, types and limits.', + ) + } + } +} + +const key = ({ appName, userId, id }: SessionRef) => + JSON.stringify([appName, userId, id]) + +/** Sandbox metadata that lets any process find a session's sandbox again. */ +export const sessionTag = ( + { appName, userId, id }: SessionRef, + namespace?: string, +) => ({ + adkNamespace: namespace ?? DEFAULT_NAMESPACE, + adkAppName: appName, + adkUserId: userId, + adkSessionId: id, +}) + +/** + * Six agent-callable tools. Each ADK session gets its own lazily created + * sandbox, tagged with the session so any process finds it again, and kept + * until the application calls destroySession()/destroyAll() or the sandbox + * timeout releases it. ADK's automatic close() leaves it running. + */ +export class E2BToolset extends BaseToolset { + private readonly workspaces = new Map() + private readonly tools: BaseTool[] + + constructor(private readonly options: E2BToolsetOptions = {}) { + super([]) + const max = operationLimit(options) + const limit = z.number().int().min(1).max(max).optional() + const timeoutMs = limit.describe( + `Execution limit in milliseconds, 1 to ${max}. Defaults to ${Math.min(DEFAULT_TIMEOUT_MS, max)}.`, + ) + const deadline = (value?: number) => + value ?? Math.min(DEFAULT_TIMEOUT_MS, max) + this.tools = [ + this.tool( + 'run_code', + 'Execute code in a stateful kernel, one per language, shared by the whole conversation while its workspace lives. Returns stdout, stderr, the last expression text and runtime errors. Defaults to Python.', + z.object({ + code: z.string().min(1), + language: z + .enum(['python', 'javascript', 'typescript', 'r', 'java', 'bash']) + .default('python'), + envs, + timeoutMs, + }), + async (input, workspace, info, signal) => { + const ms = deadline(input.timeoutMs) + const outcome = await workspace.runCode( + input.code, + input.language, + input.envs, + info, + { signal, timeoutMs: ms }, + ) + if ('stopped' in outcome) { + const why = + outcome.stopped === 'timed_out' + ? `exceeded ${ms} ms` + : 'was cancelled' + return failure( + `Execution ${why}. The ${input.language} kernel was restarted; its variables and imports were lost.`, + { [outcome.stopped]: true, kernel_restarted: true }, + ) + } + if ('kernelLost' in outcome) + return failure( + kernelLost(input.language, outcome), + outcome.kernelLost === 'restarted' + ? { kernel_restarted: true } + : { kernel_reset: true }, + ) + const { execution } = outcome + return { + success: true, + stdout: truncate(execution.logs.stdout.join('')), + stderr: truncate(execution.logs.stderr.join('')), + text: truncate(execution.text ?? ''), + error: execution.error + ? truncate( + `${execution.error.name}: ${execution.error.value}\n${execution.error.traceback}`, + ) + : '', + } + }, + ), + this.tool( + 'run_command', + 'Run a shell command and return stdout, stderr and exit_code. Each command starts a new process; cwd and envs do not persist, files do.', + z.object({ ...command, timeoutMs }), + async (input, workspace, info, signal) => { + const ms = deadline(input.timeoutMs) + const outcome = await workspace.runCommand( + input.command, + { cwd: input.cwd, envs: input.envs }, + info, + { signal, timeoutMs: ms }, + ) + if ('stopped' in outcome) { + const why = + outcome.stopped === 'timed_out' + ? `timed out after ${ms} ms` + : 'was cancelled' + return failure( + `Command ${why}; it and its child processes were killed.`, + { + [outcome.stopped]: true, + stdout: truncate(outcome.stdout), + stderr: truncate(outcome.stderr), + }, + ) + } + return { + success: true, + stdout: truncate(outcome.result.stdout), + stderr: truncate(outcome.result.stderr), + exit_code: outcome.result.exitCode, + } + }, + ), + this.tool( + 'write_file', + 'Write a UTF-8 text file inside the sandbox. Creates parent directories.', + z.object({ path, content: z.string() }), + (input, workspace, info, signal) => + workspace.run( + async (sandbox) => { + await sandbox.files.write(input.path, input.content) + return { success: true, path: input.path } + }, + info, + signal, + ), + ), + this.tool( + 'read_file', + 'Read a UTF-8 text file from the sandbox. Large content is truncated.', + z.object({ path }), + (input, workspace, info, signal) => + workspace.run( + async (sandbox) => ({ + success: true, + path: input.path, + content: truncate(await sandbox.files.read(input.path)), + }), + info, + signal, + ), + ), + this.tool( + 'list_files', + 'List entries in a sandbox directory.', + z.object({ path: path.default('.') }), + (input, workspace, info, signal) => + workspace.run( + async (sandbox) => { + const entries = await sandbox.files.list(input.path) + return { + success: true, + entries: entries + .slice(0, 100) + .map(({ name, type }) => ({ name: truncate(name), type })), + truncated: entries.length > 100, + } + }, + info, + signal, + ), + ), + this.tool( + 'start_background_command', + 'Start a long-running sandbox process and return its PID immediately. It runs until it exits, timeoutMs elapses, or the workspace ends. An optional preview URL is not checked for readiness.', + z.object({ + ...command, + timeoutMs: limit.describe( + `Optional process lifetime in milliseconds, 1 to ${max}. Omit to keep it running.`, + ), + port: z.number().int().min(1).max(65535).optional(), + }), + async (input, workspace, info, signal) => { + const started = await workspace.startBackground( + input.command, + { cwd: input.cwd, envs: input.envs }, + input.timeoutMs, + info, + signal, + ) + let previewUrl: string | null = null + if (input.port !== undefined) { + try { + previewUrl = `https://${started.host(input.port)}` + } catch { + /* Preserve the started process PID. */ + } + } + return { + success: true, + pid: started.pid, + preview_url: previewUrl, + ...(input.port === undefined ? {} : { readiness: 'unknown' }), + } + }, + ), + ] + } + + private workspace(session: SessionRef): Workspace { + let workspace = this.workspaces.get(key(session)) + if (!workspace) { + workspace = new Workspace( + this.options, + CLEANUP_HINT, + sessionTag(session, this.options.namespace), + ) + this.workspaces.set(key(session), workspace) + } + return workspace + } + + private tool( + name: string, + description: string, + parameters: T, + execute: Execute, + ): BaseTool { + return new SafeFunctionTool({ + name, + description, + parameters, + execute: (async (input: z.infer, context) => { + const invocation = context?.invocationContext + if (!invocation) + return failure('An ADK invocation context is required.') + const info: RunInfo = { created: false } + const result = await execute( + input, + this.workspace(invocation.session), + info, + invocation.abortSignal, + ).catch((error: unknown) => + failure(operationError(error), { + ...(error instanceof WorkspaceExpiredError && { + workspace_expired: true, + }), + ...(error instanceof OperationAbortedError && { aborted: true }), + }), + ) + return info.created ? { ...result, workspace: 'new' } : result + }) as ToolOptions['execute'], + }) + } + + override async getTools(): Promise { + return [...this.tools] + } + + /** + * ADK calls this after every run without saying which one ended, so it + * releases nothing. Use destroySession() or destroyAll(). + */ + override async close(): Promise {} + + /** + * Kill the session's sandbox, wherever it was created. Later calls in that + * session start a new workspace. + */ + async destroySession(session: SessionRef): Promise { + const id = key(session) + const workspace = this.workspace(session) + await workspace.destroy() + // Forget it only if no newer call has started using it meanwhile. + if (workspace.idle && this.workspaces.get(id) === workspace) + this.workspaces.delete(id) + } + + /** Destroy every workspace this process knows, continuing past failures; failed ones stay retryable. */ + async destroyAll(): Promise { + const results = await Promise.allSettled( + [...this.workspaces.values()].map((workspace) => workspace.destroy()), + ) + for (const [id, workspace] of this.workspaces) + if (workspace.idle) this.workspaces.delete(id) + const failed = results.filter((r) => r.status === 'rejected').length + if (failed) + throw new SandboxCleanupError( + `${failed} workspace(s) could not be destroyed; retry destroyAll().`, + ) + } +} diff --git a/packages/e2b-adk-js/src/workspace.ts b/packages/e2b-adk-js/src/workspace.ts new file mode 100644 index 0000000..1a96278 --- /dev/null +++ b/packages/e2b-adk-js/src/workspace.ts @@ -0,0 +1,637 @@ +import { + CommandExitError, + InvalidArgumentError, + NotFoundError, + Sandbox, + SandboxNotFoundError, + TimeoutError, + type CommandHandle, + type CommandResult, + type Execution, + type SandboxInfo, +} from '@e2b/code-interpreter' +import type { E2BSandboxOptions, E2BToolsetOptions } from './options.js' +import { + OperationAbortedError, + SandboxCleanupError, + WorkspaceBlockedError, + WorkspaceDestroyedError, + WorkspaceExpiredError, +} from './results.js' + +/** Upper bound for cleanup and recovery calls, independent of operation limits. */ +const RECOVERY_WAIT_MS = 30_000 +/** Lets our own deadline fire before the SDK's, so one path handles both. */ +const SDK_GRACE_MS = 5_000 +/** Longest delay setTimeout honours; larger values fire immediately. */ +const MAX_TIMER_MS = 2_147_483_647 + +export type Stop = 'timed_out' | 'aborted' +export type CodeOutcome = + | { execution: Execution } + /** Stopped by restarting its kernel, whose variables and imports are gone. */ + | { stopped: Stop } + /** The kernel's state is gone: restarted after a lost stream, or unknown to the server. */ + | { kernelLost: 'restarted' | 'missing'; cause: unknown } +export type CommandOutcome = + { result: CommandResult } | { stopped: Stop; stdout: string; stderr: string } + +interface Current { + sandbox: Sandbox +} + +/** Set when an operation created the sandbox, so its result can say so. */ +export interface RunInfo { + created: boolean +} + +interface RunOptions { + signal?: AbortSignal + timeoutMs: number +} + +/** Validate creation options and return the per-operation execution limit. */ +export function operationLimit(options: E2BToolsetOptions): number { + const ttl = options.sandboxOptions?.timeoutMs ?? 300_000 + if (!Number.isFinite(ttl) || ttl <= 0) + throw new Error( + 'sandboxOptions.timeoutMs must be a positive finite number.', + ) + const max = options.maxOperationMs ?? 600_000 + if (!Number.isSafeInteger(max) || max <= 0 || max > MAX_TIMER_MS) + throw new Error( + `maxOperationMs must be an integer from 1 to ${MAX_TIMER_MS}.`, + ) + return max +} + +/** The server refused before creating a process (verified live for a bad cwd). */ +const refusedToStart = (error: unknown) => error instanceof InvalidArgumentError + +const quote = (value: string) => `'${value.replaceAll("'", `'\\''`)}'` + +function withTimeout(promise: Promise, ms: number): Promise { + let timer: ReturnType | undefined + return Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout( + () => reject(new TimeoutError('Recovery timed out')), + ms, + ) + }), + ]).finally(() => clearTimeout(timer)) +} + +/** Wait for `previous`, unless the caller cancels first. */ +function turn(previous: Promise, signal?: AbortSignal) { + return new Promise((resolve, reject) => { + if (signal?.aborted) return reject(new OperationAbortedError()) + const abort = () => reject(new OperationAbortedError()) + signal?.addEventListener('abort', abort, { once: true }) + void previous.then(() => { + signal?.removeEventListener('abort', abort) + resolve() + }) + }) +} + +type Settled = { value: T } | { error: unknown } | undefined + +/** How `promise` settled within `ms`; undefined if it did not. */ +function settleWithin(promise: Promise, ms: number): Promise> { + return withTimeout( + promise.then( + (value) => ({ value }), + (error: unknown) => ({ error }), + ), + ms, + ).catch(() => undefined) +} + +/** Aborts on the caller's signal or at the deadline, whichever comes first. */ +function stopper(signal: AbortSignal | undefined, timeoutMs: number) { + let reason: Stop | undefined + let reject!: (error: Error) => void + const stopped = new Promise((_, fail) => (reject = fail)) + stopped.catch(() => undefined) + const stop = (why: Stop) => { + reason ??= why + reject(new OperationAbortedError()) + } + const abort = () => stop('aborted') + const timer = setTimeout(() => stop('timed_out'), timeoutMs) + if (signal?.aborted) abort() + else signal?.addEventListener('abort', abort, { once: true }) + return { + /** Resolves with `promise`, or rejects once stopped. */ + race: (promise: Promise) => Promise.race([promise, stopped]), + get reason() { + return reason + }, + dispose() { + clearTimeout(timer) + signal?.removeEventListener('abort', abort) + }, + } +} + +type Operation = ( + sandbox: Sandbox, + current: Current, + /** Throws if the workspace was destroyed or blocked since the operation started. */ + check: () => void, +) => Promise + +/** + * One serialized sandbox for one ADK session, found again from any process + * by its metadata tag. An expired or destroyed sandbox is never silently + * replaced inside a running operation. + */ +export class Workspace { + private current?: Current + /** An in-flight create; destroy() owns its result. */ + private creating?: Promise + private queue: Promise = Promise.resolve() + private epoch = 0 + private pending = 0 + private destroying?: Promise + private blocked?: Error + private readonly ttl: number + + constructor( + private readonly options: E2BSandboxOptions, + private readonly cleanupHint: string, + /** Metadata that identifies this session's sandbox across processes. */ + private readonly tag: Record, + ) { + // The cloud default is five minutes. Do not override creation defaults. + this.ttl = options.sandboxOptions?.timeoutMs ?? 300_000 + } + + /** No sandbox, queued work or cleanup: safe to forget. */ + get idle(): boolean { + return this.pending === 0 && !this.current && !this.destroying + } + + run( + operation: Operation, + info: RunInfo, + signal?: AbortSignal, + ): Promise { + if (this.destroying) return Promise.reject(new WorkspaceDestroyedError()) + const epoch = this.epoch + const previous = this.queue + this.pending++ + const result = turn(previous, signal).then(() => + this.serve(operation, info, epoch, signal), + ) + // A cancelled waiter must not let the next operation overtake the active one. + this.queue = Promise.allSettled([previous, result]) + return result.finally(() => this.pending--) + } + + private async serve( + operation: Operation, + info: RunInfo, + epoch: number, + signal?: AbortSignal, + ): Promise { + const check = () => { + if (epoch !== this.epoch) throw new WorkspaceDestroyedError() + if (this.blocked) throw this.blocked + } + check() + if (signal?.aborted) throw new OperationAbortedError() + let current = this.current + if (current) { + // connect() resumes a paused sandbox and only ever extends its lifetime, + // so it never shortens what another process granted. + try { + await current.sandbox.connect({ timeoutMs: this.ttl }) + } catch (error) { + check() + // Only a sandbox-level 404 is conclusive; anything else is best effort. + if (error instanceof SandboxNotFoundError) + throw this.gone(current, error) + } + } else { + current = await this.acquire(epoch, info) + } + // Setup awaited the SDK: recheck before any side effect. + check() + if (signal?.aborted) throw new OperationAbortedError() + const { sandbox } = current + const refresh = () => + sandbox.connect({ timeoutMs: this.ttl }).then( + () => undefined, + () => undefined, // Best-effort maintenance must not replace an operation's result. + ) + let stopped = false + let timer: ReturnType | undefined + let pending: Promise = Promise.resolve() + const schedule = () => { + timer = setTimeout( + () => { + pending = refresh().then(() => { + if (!stopped) schedule() + }) + }, + Math.max(100, this.ttl / 2), + ) + timer.unref() + } + schedule() + try { + const value = await operation(sandbox, current, check) + if (epoch !== this.epoch) throw new WorkspaceDestroyedError() + return value + } catch (error) { + if (epoch !== this.epoch) throw new WorkspaceDestroyedError() + throw this.gone(current, error) + } finally { + stopped = true + clearTimeout(timer) + await pending + if (epoch === this.epoch) await refresh() + } + } + + /** The session's sandboxes, oldest first, as any process would see them. */ + private async find(): Promise { + const paginator = Sandbox.list({ + query: { metadata: this.tag, state: ['running', 'paused'] }, + }) + const infos: SandboxInfo[] = [] + while (paginator.hasNext) infos.push(...(await paginator.nextItems())) + return infos.sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime()) + } + + /** Connect to a listed sandbox; undefined if it vanished meanwhile. */ + private async connect(info: SandboxInfo): Promise { + try { + return await Sandbox.connect(info.sandboxId, { timeoutMs: this.ttl }) + } catch (error) { + if (error instanceof SandboxNotFoundError) return undefined + throw error + } + } + + /** Reuse the session's existing sandbox from any process, or create one. */ + private async acquire(epoch: number, info: RunInfo): Promise { + const [existing] = await this.find() + const found = existing && (await this.connect(existing)) + if (epoch !== this.epoch) throw new WorkspaceDestroyedError() + if (found) return (this.current = { sandbox: found }) + const sandbox = await this.create(epoch) + // Two processes may have created at once; the oldest wins everywhere. + // Until that check succeeds, our sandbox is not the session's sandbox. + let oldest: SandboxInfo | undefined + try { + ;[oldest] = await this.find() + } catch (error) { + await this.discard(sandbox) + throw error + } + if (epoch !== this.epoch) throw new WorkspaceDestroyedError() + if (oldest && oldest.sandboxId !== sandbox.sandboxId) { + const winner = await this.connect(oldest) + if (epoch !== this.epoch) throw new WorkspaceDestroyedError() + if (winner) { + await this.discard(sandbox) + return (this.current = { sandbox: winner }) + } + } + info.created = true + return (this.current = { sandbox }) + } + + /** Give up a sandbox we created but must not use; keep it if the kill fails. */ + private async discard(sandbox: Sandbox) { + try { + await withTimeout(sandbox.kill(), RECOVERY_WAIT_MS) + } catch { + // Not confirmed dead: keep the handle so destroy() retries, block reuse. + this.blocked = new SandboxCleanupError(this.cleanupHint) + throw this.blocked + } + this.current = undefined + } + + private async create(epoch: number): Promise { + const { metadata, ...rest } = this.options.sandboxOptions ?? {} + const opts = { ...rest, metadata: { ...metadata, ...this.tag } } + const creating = this.options.template + ? Sandbox.create(this.options.template, opts) + : Sandbox.create(opts) + this.creating = creating.catch(() => undefined) + let sandbox: Sandbox + try { + sandbox = await creating + } finally { + this.creating = undefined + } + // Destroyed meanwhile: destroy() kills this sandbox and keeps it if that fails. + if (epoch !== this.epoch) throw new WorkspaceDestroyedError() + this.current = { sandbox } + return sandbox + } + + /** A sandbox-level 404 means the workspace is gone (expired, or destroyed elsewhere). */ + private gone(current: Current, error: unknown): unknown { + if (!(error instanceof SandboxNotFoundError)) return error + if (this.current === current) this.current = undefined + return new WorkspaceExpiredError() + } + + /** + * Block reuse until destroy() succeeds. If the sandbox itself is gone + * (destroyed from another process, or expired), report that instead. + */ + private async block(current: Current, message: string): Promise { + const alive = await Sandbox.getInfo(current.sandbox.sandboxId).then( + () => true, + (error: unknown) => !(error instanceof SandboxNotFoundError), + ) + if (!alive) return this.gone(current, new SandboxNotFoundError('')) as Error + const error = new WorkspaceBlockedError(`${message} ${this.cleanupHint}`) + if (this.current === current) this.blocked = error + return error + } + + runCode( + code: string, + language: string, + envs: Record | undefined, + info: RunInfo, + { signal, timeoutMs }: RunOptions, + ): Promise { + return this.run( + async (sandbox, current) => { + // The deadline covers kernel start-up too. The sandbox keeps one + // kernel per language, shared by every process of this session. + const stop = stopper(signal, timeoutMs) + const execution = sandbox.runCode(code, { + language, + envs, + timeoutMs: timeoutMs + SDK_GRACE_MS, + }) + execution.catch(() => undefined) + try { + return { execution: await stop.race(execution) } + } catch (error) { + const reason = + stop.reason ?? + (error instanceof TimeoutError ? 'timed_out' : undefined) + if (!reason) { + // Refused before the code ran: nothing to stop. + if (error instanceof InvalidArgumentError) throw error + if (error instanceof SandboxNotFoundError) throw error + if (error instanceof NotFoundError) + return { kernelLost: 'missing', cause: error } + } + // The request went out, whether the stream broke or the deadline + // passed: only a kernel restart proves the code stopped. + const finished = await this.stopCode( + execution, + sandbox, + current, + language, + ) + if (finished) return { execution: finished } + return reason + ? { stopped: reason } + : { kernelLost: 'restarted', cause: error } + } finally { + stop.dispose() + } + }, + info, + signal, + ) + } + + runCommand( + command: string, + options: { cwd?: string; envs?: Record }, + info: RunInfo, + { signal, timeoutMs }: RunOptions, + ): Promise { + return this.run( + async (sandbox, current) => { + // The deadline covers the start request too. + const stop = stopper(signal, timeoutMs) + try { + // Own process group, so stopping the command also stops its children. + // No abort signal here: aborting the start request leaves the process running. + const starting = this.start( + sandbox, + current, + `exec setsid -w /bin/bash -c ${quote(command)}`, + { ...options, timeoutMs: 0 }, + ) + let handle: CommandHandle + try { + handle = await stop.race(starting) + } catch (error) { + if (!stop.reason) throw error + // Stopped while starting: the process may still start, so own it. + const late = await settleWithin(starting, RECOVERY_WAIT_MS) + if (!late) { + void starting.then( + (handle) => this.stopGroup(sandbox, handle), + () => undefined, + ) + throw await this.block( + current, + 'The command was stopped while starting and could not be confirmed stopped.', + ) + } + if ('error' in late) throw late.error + handle = late.value + } + if (!stop.reason) { + try { + return { result: await stop.race(handle.wait()) } + } catch (error) { + if (!stop.reason) { + if (error instanceof CommandExitError) return { result: error } + if (error instanceof SandboxNotFoundError) throw error + // The stream broke, not necessarily the command: stop it first. + if (!(await this.stopGroup(sandbox, handle))) + throw await this.block( + current, + 'The connection to the command was lost and it could not be confirmed stopped.', + ) + throw error + } + } + } + if (!(await this.stopGroup(sandbox, handle))) + throw await this.block( + current, + 'The command could not be confirmed stopped.', + ) + return { + stopped: stop.reason!, + stdout: handle.stdout, + stderr: handle.stderr, + } + } finally { + stop.dispose() + } + }, + info, + signal, + ) + } + + startBackground( + command: string, + options: { cwd?: string; envs?: Record }, + timeoutMs: number | undefined, + info: RunInfo, + signal?: AbortSignal, + ) { + return this.run( + async (sandbox, current) => { + // The SDK default (60 s) would kill a disconnected process too. + const handle = await this.start(sandbox, current, command, { + ...options, + timeoutMs: timeoutMs ?? 0, + }) + // Stop consuming the SDK event stream without killing the process. + await handle.disconnect() + return { + pid: handle.pid, + host: (port: number) => sandbox.getHost(port), + } + }, + info, + signal, + ) + } + + /** Start in the background; an ambiguous failure may leave it running. */ + private async start( + sandbox: Sandbox, + current: Current, + command: string, + options: { cwd?: string; envs?: Record; timeoutMs: number }, + ): Promise { + try { + return await sandbox.commands.run(command, { + ...options, + background: true, + }) + } catch (error) { + if (refusedToStart(error) || error instanceof SandboxNotFoundError) + throw error + const name = error instanceof Error ? error.name : 'unknown error' + throw await this.block( + current, + `The command start failed (${name}); it may still be running.`, + ) + } + } + + /** Restart the language's kernel; false if none exists yet, so nothing ran. */ + /** + * Stop code whose request already went out. Restart its kernel; if none is + * listed yet the request may still start one, so let it settle and look + * again. Returns the result if the code finished meanwhile; blocks the + * workspace when the code cannot be confirmed stopped. + */ + private async stopCode( + execution: Promise, + sandbox: Sandbox, + current: Current, + language: string, + ): Promise { + if (await this.restart(sandbox, current, language)) { + // The restart closes the open stream; don't let it outlive this turn. + await withTimeout(execution, RECOVERY_WAIT_MS).catch(() => undefined) + return undefined + } + const settled = await settleWithin(execution, RECOVERY_WAIT_MS) + if (settled && 'value' in settled) return settled.value + if (settled && (await this.restart(sandbox, current, language))) + return undefined + throw await this.block(current, 'The code could not be confirmed stopped.') + } + + private async restart( + sandbox: Sandbox, + current: Current, + language: string, + ): Promise { + try { + const contexts = await withTimeout( + sandbox.listCodeContexts(), + RECOVERY_WAIT_MS, + ) + const context = contexts.find((c) => c.language === language) + if (!context) return false + await withTimeout(sandbox.restartCodeContext(context), RECOVERY_WAIT_MS) + return true + } catch (error) { + if (error instanceof SandboxNotFoundError) throw error + throw await this.block( + current, + `The ${language} kernel could not be restarted, so the code may still be running.`, + ) + } + } + + /** SIGKILL the command's process group; true only if it is confirmed gone. */ + private async stopGroup(sandbox: Sandbox, handle: CommandHandle) { + try { + await withTimeout( + sandbox.commands.run(`kill -9 -- -${handle.pid}`), + RECOVERY_WAIT_MS, + ) + } catch (error) { + if (error instanceof SandboxNotFoundError) throw error + // kill reports "No such process" once the whole group has exited. + // Any other failure leaves the group's state unknown. + if (!( + error instanceof CommandExitError && + /No such process/.test(error.stderr) + )) + return false + } + const exited = await settleWithin(handle.wait(), RECOVERY_WAIT_MS) + return exited !== undefined + } + + /** + * Kill the session's sandbox, including one created by another process or + * before a restart; queued and later-started work never recreates it. + */ + destroy(): Promise { + this.destroying ??= this.kill().finally(() => { + this.destroying = undefined + }) + return this.destroying + } + + private async kill() { + this.epoch++ + // A create still in flight belongs to this destroy, even if it lands later. + const late = await this.creating + if (late) this.current = { sandbox: late } + const current = this.current + try { + if (current) await withTimeout(current.sandbox.kill(), RECOVERY_WAIT_MS) + // Sandboxes of this session that this process never held. + for (const info of await this.find()) + await withTimeout(Sandbox.kill(info.sandboxId), RECOVERY_WAIT_MS) + } catch { + // Retain the handle so a later destroy can retry; block reuse meanwhile. + this.blocked = new SandboxCleanupError(this.cleanupHint) + throw this.blocked + } + if (this.current === current) this.current = undefined + this.blocked = undefined + } +} diff --git a/packages/e2b-adk-js/tests/executor.test.ts b/packages/e2b-adk-js/tests/executor.test.ts new file mode 100644 index 0000000..61a6700 --- /dev/null +++ b/packages/e2b-adk-js/tests/executor.test.ts @@ -0,0 +1,218 @@ +import { describe, expect, it } from 'vitest' +import { CodeExecutionLanguage as Language } from '@google/adk' +import { TimeoutError } from '@e2b/code-interpreter' +import { E2BCodeExecutor } from '../src/index.js' +import { + context, + execution, + mockSandbox, + statefulSandboxes, +} from './helpers.js' + +describe('code executor', () => { + it.each([ + [Language.PYTHON, 'python'], + [Language.JAVASCRIPT, 'javascript'], + [Language.TYPESCRIPT, 'typescript'], + [Language.SHELL, 'bash'], + ])('maps %s to %s', async (language, expected) => { + const { sandbox } = mockSandbox() + const executor = new E2BCodeExecutor() + expect( + await executor.executeCode({ + invocationContext: context(), + codeExecutionInput: { code: 'code', language, inputFiles: [] }, + }), + ).toEqual({ stdout: 'hello\n\n42', stderr: '', outputFiles: [] }) + expect(sandbox.runCode).toHaveBeenCalledWith('code', { + language: expected, + envs: undefined, + timeoutMs: 65_000, + }) + await executor.close() + }) + + it.each([ + Language.UNSPECIFIED, + Language.WINDOWS_CMD, + Language.POWERSHELL, + '__proto__' as Language, + 'constructor' as Language, + ])( + 'rejects unsupported %s without creating a workspace', + async (language) => { + const { create } = mockSandbox() + const result = await new E2BCodeExecutor().executeCode({ + invocationContext: context(), + codeExecutionInput: { code: 'x', language, inputFiles: [] }, + }) + expect(result.stderr).toContain('Unsupported language') + expect(create).not.toHaveBeenCalled() + }, + ) + + it('rejects input files and nonempty args before side effects', async () => { + const { create } = mockSandbox() + const executor = new E2BCodeExecutor() + for (const input of [ + { inputFiles: [{ name: 'a', content: 'x', mimeType: 'text/plain' }] }, + { inputFiles: [], args: ['--flag'] }, + { inputFiles: [], args: { flag: true } }, + ]) { + const result = await executor.executeCode({ + invocationContext: context(), + codeExecutionInput: { code: 'x', language: Language.PYTHON, ...input }, + }) + expect(result.stderr).toContain('inputFiles and args are not supported') + } + expect(create).not.toHaveBeenCalled() + expect(executor.optimizeDataFile).toBe(false) + expect(executor.codeBlockDelimiters).toEqual([ + ['```tool_code\n', '\n```'], + ['```python\n', '\n```'], + ]) + }) + + it('retains one workspace and refuses a different session, including another user with the same id', async () => { + const { create, sandbox } = mockSandbox() + const executor = new E2BCodeExecutor() + const input = { code: 'x', language: Language.PYTHON, inputFiles: [] } + await executor.executeCode({ + invocationContext: context(), + codeExecutionInput: input, + }) + await executor.executeCode({ + invocationContext: context(), + codeExecutionInput: { ...input, code: 'x + 1' }, + }) + expect(create).toHaveBeenCalledTimes(1) + for (const ctx of [ + context('different'), + context('session', 'other-user'), + ]) { + expect( + ( + await executor.executeCode({ + invocationContext: ctx, + codeExecutionInput: input, + }) + ).stderr, + ).toContain('another ADK session') + } + expect(sandbox.runCode).toHaveBeenCalledTimes(2) + await executor.close() + await executor.close() + expect(sandbox.kill).toHaveBeenCalledTimes(1) + // close() is not terminal: a later call starts a new workspace. + await executor.executeCode({ + invocationContext: context(), + codeExecutionInput: input, + }) + expect(create).toHaveBeenCalledTimes(2) + }) + + it('reports kernel state lost by stream recovery in stderr', async () => { + const { sandbox } = mockSandbox() + sandbox.runCode.mockRejectedValueOnce(new TypeError('terminated')) + const result = await new E2BCodeExecutor().executeCode({ + invocationContext: context(), + codeExecutionInput: { + code: 'x', + language: Language.PYTHON, + inputFiles: [], + }, + }) + expect(result.stderr).toBe( + 'The connection to the python kernel was lost (TypeError), so it was restarted: its variables and imports were lost. Files in the sandbox are kept.', + ) + expect(sandbox.restartCodeContext).toHaveBeenCalledTimes(1) + }) + + it('tags its sandbox with the session so close() also finds one created elsewhere', async () => { + const { create, registry } = mockSandbox() + const boxes = statefulSandboxes(create) + const input = { code: 'x', language: Language.PYTHON, inputFiles: [] } + await new E2BCodeExecutor().executeCode({ + invocationContext: context(), + codeExecutionInput: input, + }) + expect(registry.entries.get(boxes[0]!.sandboxId)?.metadata).toEqual({ + adkNamespace: 'default', + adkAppName: 'test', + adkUserId: 'user', + adkSessionId: 'session', + }) + // A second executor (another process) reuses it, and its close() kills it. + const other = new E2BCodeExecutor() + await other.executeCode({ + invocationContext: context(), + codeExecutionInput: input, + }) + expect(create).toHaveBeenCalledTimes(1) + await other.close() + expect(registry.running()).toHaveLength(0) + }) + + it('bounds execution and restarts the kernel after a timeout', async () => { + const { sandbox } = mockSandbox() + sandbox.runCode.mockRejectedValueOnce(new TimeoutError('timed out')) + const executor = new E2BCodeExecutor({ maxOperationMs: 5000 }) + const result = await executor.executeCode({ + invocationContext: context(), + codeExecutionInput: { + code: 'x', + language: Language.PYTHON, + inputFiles: [], + }, + }) + expect(result.stderr).toBe( + 'Execution exceeded 5000 ms. The kernel was restarted; its variables and imports were lost.', + ) + expect(sandbox.runCode.mock.calls[0]![1]).toMatchObject({ + timeoutMs: 10_000, + }) + expect(sandbox.restartCodeContext).toHaveBeenCalledTimes(1) + }) + + it('normalizes runtime errors and truncates all output fields', async () => { + const { sandbox } = mockSandbox() + sandbox.runCode.mockResolvedValue( + execution({ + logs: { stdout: ['x'.repeat(20000)], stderr: [] }, + error: { + name: 'ValueError', + value: 'bad', + traceback: 't'.repeat(20000), + }, + }), + ) + const executor = new E2BCodeExecutor() + const result = await executor.executeCode({ + invocationContext: context(), + codeExecutionInput: { + code: 'x', + language: Language.PYTHON, + inputFiles: [], + }, + }) + expect(result.stdout).toHaveLength(10000) + expect(result.stderr).toHaveLength(10000) + expect(result.stderr).toContain('ValueError: bad') + await executor.close() + }) + + it('does not expose SDK request secrets on transport failures', async () => { + const { create } = mockSandbox() + create.mockRejectedValue(new Error('secret-api-key')) + const result = await new E2BCodeExecutor().executeCode({ + invocationContext: context(), + codeExecutionInput: { + code: 'x', + language: Language.PYTHON, + inputFiles: [], + }, + }) + expect(result.stderr).toContain('Sandbox operation failed') + expect(result.stderr).not.toContain('secret-api-key') + }) +}) diff --git a/packages/e2b-adk-js/tests/helpers.ts b/packages/e2b-adk-js/tests/helpers.ts new file mode 100644 index 0000000..b8cc116 --- /dev/null +++ b/packages/e2b-adk-js/tests/helpers.ts @@ -0,0 +1,339 @@ +import { vi } from 'vitest' +import { + Context, + InvocationContext, + PluginManager, + BaseLlm, + type Event, + type LlmRequest, + type LlmResponse, + type BaseLlmConnection, +} from '@google/adk' +import { + FileNotFoundError, + Sandbox, + SandboxNotFoundError, + type Execution, +} from '@e2b/code-interpreter' + +export function context( + id = 'session', + userId = 'user', + abortSignal?: AbortSignal, +) { + return new InvocationContext({ + invocationId: 'invocation', + pluginManager: new PluginManager(), + abortSignal, + session: { + id, + userId, + appName: 'test', + state: {}, + events: [], + lastUpdateTime: 0, + }, + }) +} + +export function toolContext(invocation = context()) { + return new Context({ invocationContext: invocation }) +} + +export function execution(overrides: Partial = {}): Execution { + return { + logs: { stdout: ['hello\n'], stderr: [] }, + text: '42', + results: [], + ...overrides, + } as Execution +} + +/** A background CommandHandle double. */ +export function commandHandle( + result = { stdout: 'ok', stderr: '', exitCode: 0 }, +) { + return { + pid: 101, + stdout: '', + stderr: '', + wait: vi.fn().mockResolvedValue(result), + kill: vi.fn().mockResolvedValue(true), + disconnect: vi.fn().mockResolvedValue(undefined), + } +} + +let nextId = 0 + +export function fakeSandbox() { + const handle = commandHandle() + return { + sandboxId: `fake-sandbox-${++nextId}`, + handle, + runCode: vi.fn().mockResolvedValue(execution()), + // The sandbox keeps one default kernel per language (verified live). + listCodeContexts: vi.fn().mockImplementation(async () => + ['python', 'javascript', 'typescript', 'r', 'java', 'bash'].map( + (language) => ({ + id: `ctx-${language}`, + language, + cwd: '/home/user', + }), + ), + ), + restartCodeContext: vi.fn().mockResolvedValue(undefined), + commands: { + // Background starts return the handle; foreground calls are kill commands. + run: vi.fn(async (_command: string, opts?: { background?: boolean }) => + opts?.background ? handle : { stdout: '', stderr: '', exitCode: 0 }, + ), + }, + files: { + write: vi.fn().mockResolvedValue({ path: '/home/user/test.txt' }), + read: vi.fn().mockResolvedValue('contents'), + list: vi.fn().mockResolvedValue([{ name: 'test.txt', type: 'file' }]), + }, + getHost: vi.fn().mockReturnValue('3000-fake.e2b.app'), + connect: vi.fn(async function (this: unknown) { + return this + }), + kill: vi.fn().mockResolvedValue(true), + } +} + +export type FakeSandbox = ReturnType + +interface Entry { + box: FakeSandbox + metadata: Record + startedAt: Date + state: 'running' | 'paused' | 'killed' +} + +/** + * The E2B control plane as every "process" in a test sees it: sandboxes are + * listed by metadata, connected by id (resuming a paused one), and killed by + * id. A second toolset or workspace over the same registry is another process. + */ +export function registry() { + const entries = new Map() + const info = (e: Entry) => ({ + sandboxId: e.box.sandboxId, + metadata: e.metadata, + startedAt: e.startedAt, + state: e.state, + }) + const alive = (id: string) => { + const e = entries.get(id) + return e && e.state !== 'killed' ? e : undefined + } + const reg = { + entries, + add(box: FakeSandbox, metadata: Record) { + const entry: Entry = { + box, + metadata, + startedAt: new Date(1_700_000_000_000 + entries.size * 1000), + state: 'running', + } + entries.set(box.sandboxId, entry) + box.kill.mockImplementation(async () => { + const was = entry.state !== 'killed' + entry.state = 'killed' + return was + }) + box.connect.mockImplementation(async () => { + if (entry.state === 'killed') throw new SandboxNotFoundError('gone') + entry.state = 'running' + return box + }) + return entry + }, + pause(id: string) { + alive(id)!.state = 'paused' + }, + kill(id: string) { + const e = alive(id) + if (e) e.state = 'killed' + }, + running: () => [...entries.values()].filter((e) => e.state === 'running'), + list(query?: { metadata?: Record; state?: string[] }) { + const states = query?.state ?? ['running', 'paused'] + return ( + [...entries.values()] + .filter( + (e) => + states.includes(e.state) && + Object.entries(query?.metadata ?? {}).every( + ([k, v]) => e.metadata[k] === v, + ), + ) + // Newest first, as the real API answers. + .sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime()) + .map(info) + ) + }, + alive, + info, + } + return reg +} + +export type Registry = ReturnType + +export function mockSandbox() { + const sandbox = fakeSandbox() + const reg = registry() + /** Configure this to change what a create returns; the registry wraps it. */ + const create = vi.fn().mockResolvedValue(sandbox as unknown as Sandbox) + vi.spyOn(Sandbox, 'create').mockImplementation(async (...args: unknown[]) => { + const opts = (typeof args[0] === 'string' ? args[1] : args[0]) as + { metadata?: Record } | undefined + const box = (await create(...args)) as FakeSandbox + reg.add(box, opts?.metadata ?? {}) + return box as unknown as Sandbox + }) + const list = vi.spyOn(Sandbox, 'list').mockImplementation(((opts?: { + query?: Parameters[0] + }) => { + let done = false + return { + get hasNext() { + return !done + }, + nextItems: async () => { + done = true + return reg.list(opts?.query) + }, + } + }) as unknown as typeof Sandbox.list) + const connect = vi.spyOn(Sandbox, 'connect').mockImplementation((async ( + id: string, + ) => { + const e = reg.alive(id) + if (!e) throw new SandboxNotFoundError(`${id} not found`) + e.state = 'running' + return e.box + }) as unknown as typeof Sandbox.connect) + const kill = vi.spyOn(Sandbox, 'kill').mockImplementation(async (id) => { + const e = reg.alive(id) + if (e) e.state = 'killed' + return Boolean(e) + }) + vi.spyOn(Sandbox, 'getInfo').mockImplementation((async (id: string) => { + const e = reg.alive(id) + if (!e) throw new SandboxNotFoundError(`${id} not found`) + return reg.info(e) + }) as unknown as typeof Sandbox.getInfo) + // Tests must never contact a model or sandbox endpoint. + vi.spyOn(globalThis, 'fetch').mockRejectedValue( + new Error('Network disabled in tests'), + ) + return { sandbox, create, registry: reg, list, connect, kill } +} + +/** Every create returns a new sandbox with its own files, lost on kill. */ +export function statefulSandboxes( + create: ReturnType['create'], +) { + const boxes: Array }> = [] + create.mockImplementation(async () => { + const box = Object.assign(fakeSandbox(), { + data: new Map(), + }) + box.files.write.mockImplementation(async (path: string, value: string) => { + box.data.set(path, value) + return { path } + }) + box.files.read.mockImplementation(async (path: string) => { + if (!box.data.has(path)) throw new FileNotFoundError(`${path} missing`) + return box.data.get(path) + }) + boxes.push(box) + return box as unknown as Sandbox + }) + return boxes +} + +export function deferred() { + let resolve!: (value: T) => void + let reject!: (reason: unknown) => void + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve, reject } +} + +export const tick = () => new Promise((resolve) => setImmediate(resolve)) + +export class ScriptedModel extends BaseLlm { + readonly requests: LlmRequest[] = [] + private index = 0 + constructor(private readonly responses: Array) { + super({ model: 'scripted' }) + } + override async *generateContentAsync(request: LlmRequest) { + this.requests.push({ + ...request, + contents: structuredClone(request.contents), + }) + const response = this.responses[this.index++] + if (response instanceof Error) throw response + if (!response) throw new Error('Unexpected model call') + yield response + } + override async connect(): Promise { + throw new Error('Live streaming is not used') + } +} + +export type PlannedCall = { name: string; args: Record } + +/** + * Deterministic under concurrency: the latest user text is a JSON list of tool + * calls, issued one per model turn, then "done". + */ +export class PlanModel extends BaseLlm { + constructor() { + super({ model: 'plan' }) + } + override async *generateContentAsync(request: LlmRequest) { + const parts = request.contents.flatMap((c) => + (c.parts ?? []).map((p) => ({ role: c.role, p })), + ) + let start = parts.length - 1 + while (!(parts[start]!.role === 'user' && parts[start]!.p.text)) start-- + const plan = JSON.parse(parts[start]!.p.text!) as PlannedCall[] + const done = parts.slice(start).filter((x) => x.p.functionResponse).length + const next = plan[done] + yield next + ? { + content: { + role: 'model', + parts: [{ functionCall: { id: `call-${done}`, ...next } }], + }, + } + : { content: { role: 'model', parts: [{ text: 'done' }] } } + } + override async connect(): Promise { + throw new Error('Live streaming is not used') + } +} + +export const plan = (...calls: PlannedCall[]) => ({ + role: 'user', + parts: [{ text: JSON.stringify(calls) }], +}) + +export async function drain(stream: AsyncGenerator) { + const events: Event[] = [] + for await (const event of stream) events.push(event) + return events +} + +export const toolResponses = (events: Event[]) => + events + .flatMap((e) => e.content?.parts ?? []) + .filter((p) => p.functionResponse) + .map((p) => p.functionResponse!.response as Record) diff --git a/packages/e2b-adk-js/tests/isolation.test.ts b/packages/e2b-adk-js/tests/isolation.test.ts new file mode 100644 index 0000000..14ed2c5 --- /dev/null +++ b/packages/e2b-adk-js/tests/isolation.test.ts @@ -0,0 +1,369 @@ +import { describe, expect, it } from 'vitest' +import { + FunctionTool, + InMemoryRunner, + InvocationContext, + LlmAgent, + PluginManager, + type LlmResponse, +} from '@google/adk' +import { z } from 'zod' +import { E2BToolset } from '../src/index.js' +import { + deferred, + drain, + mockSandbox, + plan, + PlanModel, + ScriptedModel, + statefulSandboxes, + tick, + toolContext, + toolResponses, +} from './helpers.js' + +const write = (content: string) => ({ + name: 'write_file', + args: { path: '/home/user/data.txt', content }, +}) +const read = { name: 'read_file', args: { path: '/home/user/data.txt' } } + +/** The adk web / api_server pattern: one module-level agent for every user. */ +function app(toolset = new E2BToolset()) { + const runner = new InMemoryRunner({ + agent: new LlmAgent({ + name: 'agent', + model: new PlanModel(), + tools: [toolset], + }), + }) + const session = (userId: string) => + runner.sessionService.createSession({ appName: runner.appName, userId }) + const ask = async ( + s: { id: string; userId: string }, + message: ReturnType, + ) => + toolResponses( + await drain( + runner.runAsync({ + userId: s.userId, + sessionId: s.id, + newMessage: message, + }), + ), + ) + return { runner, toolset, session, ask } +} + +function invocation(sessionId: string, invocationId: string) { + return toolContext( + new InvocationContext({ + invocationId, + pluginManager: new PluginManager(), + session: { + id: sessionId, + userId: 'user', + appName: 'test', + state: {}, + events: [], + lastUpdateTime: 0, + }, + }), + ) +} + +describe('session-scoped workspaces', () => { + it('gives concurrent sessions of a module-level agent separate sandboxes, and later sessions still work', async () => { + const { create } = mockSandbox() + const boxes = statefulSandboxes(create) + const { session, ask } = app() + const [alice, bob] = await Promise.all([session('alice'), session('bob')]) + const [a, b] = await Promise.all([ + ask(alice, plan(write('alice-data'), read)), + ask(bob, plan(write('bob-data'), read)), + ]) + expect(a[1]).toMatchObject({ success: true, content: 'alice-data' }) + expect(b[1]).toMatchObject({ success: true, content: 'bob-data' }) + expect(create).toHaveBeenCalledTimes(2) + const carol = await session('carol') + expect((await ask(carol, plan(read)))[0]).toEqual({ + success: false, + error: 'Sandbox operation failed (FileNotFoundError).', + workspace: 'new', + }) + expect(create).toHaveBeenCalledTimes(3) + // ADK closed the toolset after each run; nothing was killed. + expect(boxes.map((box) => box.kill.mock.calls.length)).toEqual([0, 0, 0]) + }) + + it('keeps files and kernels across messages of one session', async () => { + const { create, sandbox } = mockSandbox() + const { session, ask } = app() + const s = await session('alice') + const first = await ask( + s, + plan(write('x'), { name: 'run_code', args: { code: 'n = 1' } }), + ) + expect(first[0]).toMatchObject({ workspace: 'new' }) + const second = await ask( + s, + plan(read, { name: 'run_code', args: { code: 'n' } }), + ) + expect(second.every((r) => r.success && !('workspace' in r))).toBe(true) + expect(create).toHaveBeenCalledTimes(1) + expect(sandbox.runCode).toHaveBeenCalledTimes(2) + expect(sandbox.kill).not.toHaveBeenCalled() + }) + + it('keeps the workspace through a tool-confirmation pause', async () => { + const { create } = mockSandbox() + statefulSandboxes(create) + const deploy = new FunctionTool({ + name: 'deploy', + description: 'deploy', + parameters: z.object({}), + requireConfirmation: true, + execute: async () => ({ ok: true }), + }) + const call = ( + name: string, + args: Record, + id: string, + ): LlmResponse => ({ + content: { role: 'model', parts: [{ functionCall: { name, args, id } }] }, + }) + const done: LlmResponse = { + content: { role: 'model', parts: [{ text: 'done' }] }, + } + const runner = new InMemoryRunner({ + agent: new LlmAgent({ + name: 'agent', + model: new ScriptedModel([ + call('write_file', write('artifact').args, 'w1'), + call('deploy', {}, 'd1'), + call('read_file', read.args, 'r1'), + done, + ]), + tools: [new E2BToolset(), deploy], + }), + }) + const s = await runner.sessionService.createSession({ + appName: runner.appName, + userId: 'u', + }) + const first = await drain( + runner.runAsync({ userId: 'u', sessionId: s.id, newMessage: plan() }), + ) + const confirm = first + .flatMap((e) => e.content?.parts ?? []) + .find( + (p) => p.functionCall?.name === 'adk_request_confirmation', + )?.functionCall + expect(confirm).toBeDefined() + const second = await drain( + runner.runAsync({ + userId: 'u', + sessionId: s.id, + newMessage: { + role: 'user', + parts: [ + { + functionResponse: { + id: confirm!.id, + name: confirm!.name, + response: { confirmed: true }, + }, + }, + ], + }, + }), + ) + expect(toolResponses(second)).toContainEqual({ + success: true, + path: '/home/user/data.txt', + content: 'artifact', + }) + expect(create).toHaveBeenCalledTimes(1) + }) + + it('serializes overlapping invocations of one session in one shared sandbox', async () => { + const { create, sandbox } = mockSandbox() + let active = 0 + let overlap = false + sandbox.files.write.mockImplementation(async () => { + overlap ||= ++active > 1 + await tick() + active-- + return { path: 'x' } + }) + const toolset = new E2BToolset() + const tool = (await toolset.getTools())[2]! + const results = await Promise.all( + ['inv-1', 'inv-2', 'inv-1', 'inv-2'].map((id, i) => + tool.runAsync({ + args: { path: `/f${i}`, content: 'x' }, + toolContext: invocation('shared', id), + }), + ), + ) + expect(results.every((r) => (r as { success: boolean }).success)).toBe(true) + expect(overlap).toBe(false) + expect(create).toHaveBeenCalledTimes(1) + }) + + it('a late ADK close() from another run does not disturb active work', async () => { + const { sandbox } = mockSandbox() + const slow = deferred() + sandbox.files.read.mockReturnValue(slow.promise) + const toolset = new E2BToolset() + const tool = (await toolset.getTools())[3]! + const active = tool.runAsync({ + args: { path: '/a' }, + toolContext: invocation('s', 'new'), + }) + await tick() + await toolset.close() + slow.resolve('still here') + expect(await active).toMatchObject({ success: true, content: 'still here' }) + expect(sandbox.kill).not.toHaveBeenCalled() + }) + + it('destroySession releases only that session; later messages start a new workspace', async () => { + const { create } = mockSandbox() + const boxes = statefulSandboxes(create) + const { session, ask, toolset } = app() + const [alice, bob] = await Promise.all([session('alice'), session('bob')]) + await ask(alice, plan(write('a'))) + await ask(bob, plan(write('b'))) + await toolset.destroySession(alice) + expect(boxes[0]!.kill).toHaveBeenCalledTimes(1) + expect(boxes[1]!.kill).not.toHaveBeenCalled() + await toolset.destroySession(alice) + expect(boxes[0]!.kill).toHaveBeenCalledTimes(1) + expect((await ask(alice, plan(read)))[0]).toMatchObject({ + success: false, + workspace: 'new', + }) + expect((await ask(bob, plan(read)))[0]).toMatchObject({ content: 'b' }) + }) + + it('destroySession fails queued work without recreating, and blocks until a failed kill is retried', async () => { + const { create, sandbox } = mockSandbox() + const toolset = new E2BToolset() + const [, , writeTool, readTool] = await toolset.getTools() + const ref = { appName: 'test', userId: 'user', id: 's' } + await writeTool!.runAsync({ + args: { path: '/a', content: 'x' }, + toolContext: invocation('s', 'i'), + }) + const slow = deferred() + sandbox.files.read.mockReturnValueOnce(slow.promise) + const running = readTool!.runAsync({ + args: { path: '/a' }, + toolContext: invocation('s', 'i'), + }) + const queued = readTool!.runAsync({ + args: { path: '/a' }, + toolContext: invocation('s', 'j'), + }) + await tick() + sandbox.kill.mockRejectedValueOnce(new Error('down')) + await expect(toolset.destroySession(ref)).rejects.toThrow( + 'retry destroySession()', + ) + slow.reject(new Error('sandbox killed')) + expect(await running).toMatchObject({ + success: false, + error: expect.stringContaining('released'), + }) + expect(await queued).toMatchObject({ + success: false, + error: expect.stringContaining('released'), + }) + expect( + await readTool!.runAsync({ + args: { path: '/a' }, + toolContext: invocation('s', 'k'), + }), + ).toMatchObject({ + success: false, + error: expect.stringContaining('cleanup failed'), + }) + expect(create).toHaveBeenCalledTimes(1) + await toolset.destroySession(ref) + expect( + await readTool!.runAsync({ + args: { path: '/a' }, + toolContext: invocation('s', 'k'), + }), + ).toMatchObject({ success: true, workspace: 'new' }) + }) + + it('destroyAll handles concurrent calls and partial failures', async () => { + const { create } = mockSandbox() + const boxes = statefulSandboxes(create) + const toolset = new E2BToolset() + const writeTool = (await toolset.getTools())[2]! + for (const id of ['a', 'b', 'c']) + await writeTool.runAsync({ + args: { path: '/x', content: id }, + toolContext: invocation(id, 'i'), + }) + boxes[1]!.kill.mockRejectedValueOnce(new Error('down')) + const [first, second] = await Promise.allSettled([ + toolset.destroyAll(), + toolset.destroyAll(), + ]) + expect(first.status).toBe('rejected') + expect(second.status).toBe('rejected') + expect(boxes.map((box) => box.kill.mock.calls.length)).toEqual([1, 1, 1]) + await toolset.destroyAll() + expect(boxes.map((box) => box.kill.mock.calls.length)).toEqual([1, 2, 1]) + await toolset.destroyAll() + expect(boxes.map((box) => box.kill.mock.calls.length)).toEqual([1, 2, 1]) + }) + + it('a second server instance continues the conversation in the same sandbox', async () => { + const { create } = mockSandbox() + const boxes = statefulSandboxes(create) + // Two E2BToolset instances are two processes sharing the E2B account. + const instanceA = app() + const instanceB = app() + const s = await instanceA.session('alice') + // The shared session store (a database in production) knows the session on B too. + await instanceB.runner.sessionService.createSession({ + appName: instanceB.runner.appName, + userId: 'alice', + sessionId: s.id, + }) + await instanceA.ask(s, plan(write('from-a'))) + expect((await instanceB.ask(s, plan(read)))[0]).toMatchObject({ + success: true, + content: 'from-a', + }) + expect(create).toHaveBeenCalledTimes(1) + await instanceB.toolset.destroySession(s) + expect(boxes[0]!.kill).toHaveBeenCalledTimes(1) + expect((await instanceA.ask(s, plan(read)))[0]).toMatchObject({ + success: false, + workspace_expired: true, + }) + expect((await instanceA.ask(s, plan(read)))[0]).toMatchObject({ + success: false, + workspace: 'new', + }) + expect(create).toHaveBeenCalledTimes(2) + }) + + it('destroySession after a restart still finds and kills the session sandbox', async () => { + const { create, registry } = mockSandbox() + statefulSandboxes(create) + const before = app() + const s = await before.session('alice') + await before.ask(s, plan(write('x'))) + expect(registry.running()).toHaveLength(1) + // A fresh process that never served this session. + await new E2BToolset().destroySession(s) + expect(registry.running()).toHaveLength(0) + }) +}) diff --git a/packages/e2b-adk-js/tests/runner.test.ts b/packages/e2b-adk-js/tests/runner.test.ts new file mode 100644 index 0000000..024dc40 --- /dev/null +++ b/packages/e2b-adk-js/tests/runner.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, it } from 'vitest' +import { + InMemoryRunner, + LlmAgent, + type Event, + type LlmResponse, +} from '@google/adk' +import { E2BCodeExecutor, E2BToolset } from '../src/index.js' +import { mockSandbox, ScriptedModel } from './helpers.js' + +const toolCall: LlmResponse = { + content: { + role: 'model', + parts: [ + { + functionCall: { + id: 'call-1', + name: 'run_code', + args: { code: '6 * 7' }, + }, + }, + ], + }, +} +const done: LlmResponse = { + content: { role: 'model', parts: [{ text: '42' }] }, +} +const message = { role: 'user', parts: [{ text: 'Compute the answer.' }] } +async function collect(stream: AsyncGenerator) { + const events: Event[] = [] + for await (const event of stream) events.push(event) + return events +} + +describe('real ADK runner integration', () => { + it('routes tool calls; ADK close() after each run keeps the session workspace', async () => { + const { sandbox, create } = mockSandbox() + const toolset = new E2BToolset() + const model = new ScriptedModel([toolCall, done, toolCall, done]) + const runner = new InMemoryRunner({ + agent: new LlmAgent({ name: 'agent', model, tools: [toolset] }), + }) + const events = await collect( + runner.runEphemeral({ userId: 'user', newMessage: message }), + ) + expect(sandbox.runCode).toHaveBeenCalledWith('6 * 7', { + language: 'python', + envs: undefined, + timeoutMs: 65_000, + }) + expect( + events + .flatMap((e) => e.content?.parts ?? []) + .some((p) => p.functionResponse?.name === 'run_code'), + ).toBe(true) + // runEphemeral uses a new session each time, so each gets a workspace. + await collect(runner.runEphemeral({ userId: 'user', newMessage: message })) + expect(create).toHaveBeenCalledTimes(2) + expect(sandbox.kill).not.toHaveBeenCalled() + await toolset.destroyAll() + expect(sandbox.kill).toHaveBeenCalledTimes(2) + }) + + it('a model error after tool execution leaves the workspace for the application to release', async () => { + const { sandbox } = mockSandbox() + const toolset = new E2BToolset() + const runner = new InMemoryRunner({ + agent: new LlmAgent({ + name: 'agent', + model: new ScriptedModel([toolCall, new Error('Model failed')]), + tools: [toolset], + }), + }) + const events = await collect( + runner.runEphemeral({ userId: 'user', newMessage: message }), + ) + expect(events.some((event) => event.errorMessage === 'Model failed')).toBe( + true, + ) + expect(sandbox.kill).not.toHaveBeenCalled() + await toolset.destroyAll() + expect(sandbox.kill).toHaveBeenCalledTimes(1) + }) + + it('forwards runner cancellation to a running command and kills its process group', async () => { + const { sandbox } = mockSandbox() + let exit!: (error: Error) => void + sandbox.handle.wait.mockReturnValueOnce( + new Promise((_, reject) => (exit = reject)), + ) + sandbox.commands.run.mockImplementation(async (command, opts) => { + if (opts?.background) return sandbox.handle + if (command === 'kill -9 -- -101') exit(new Error('killed')) + return { stdout: '', stderr: '', exitCode: 0 } + }) + const model = new ScriptedModel([ + { + content: { + role: 'model', + parts: [ + { + functionCall: { + id: 'c', + name: 'run_command', + args: { command: 'sleep 600' }, + }, + }, + ], + }, + }, + done, + ]) + const runner = new InMemoryRunner({ + agent: new LlmAgent({ name: 'agent', model, tools: [new E2BToolset()] }), + }) + const session = await runner.sessionService.createSession({ + appName: runner.appName, + userId: 'user', + }) + const abort = new AbortController() + const run = collect( + runner.runAsync({ + userId: 'user', + sessionId: session.id, + newMessage: message, + abortSignal: abort.signal, + }), + ) + await new Promise((resolve) => setTimeout(resolve, 20)) + abort.abort() + await run + expect(sandbox.commands.run).toHaveBeenCalledWith('kill -9 -- -101') + expect(model.requests).toHaveLength(1) + }) + + it('returns validation errors to the model so it can correct a call', async () => { + const { create } = mockSandbox() + const badCall: LlmResponse = { + content: { + role: 'model', + parts: [ + { functionCall: { id: 'bad', name: 'run_code', args: { code: 1 } } }, + ], + }, + } + const runner = new InMemoryRunner({ + agent: new LlmAgent({ + name: 'agent', + model: new ScriptedModel([badCall, toolCall, done]), + tools: [new E2BToolset()], + }), + }) + const events = await collect( + runner.runEphemeral({ userId: 'user', newMessage: message }), + ) + const responses = events + .flatMap((e) => e.content?.parts ?? []) + .filter((p) => p.functionResponse) + expect(responses).toHaveLength(2) + expect(JSON.stringify(responses[0])).toContain('"success":false') + expect(create).toHaveBeenCalledTimes(1) + }) + + it('documents that ADK 2.1.0 does not wire its code execution response processor', async () => { + const { sandbox } = mockSandbox() + const executor = new E2BCodeExecutor() + const model = new ScriptedModel([ + { + content: { role: 'model', parts: [{ text: '```python\n6 * 7\n```' }] }, + }, + done, + ]) + const runner = new InMemoryRunner({ + agent: new LlmAgent({ name: 'agent', model, codeExecutor: executor }), + }) + const events = await collect( + runner.runEphemeral({ userId: 'user', newMessage: message }), + ) + expect(events.filter((event) => event.errorCode)).toEqual([]) + expect(sandbox.runCode).not.toHaveBeenCalled() + expect( + events + .flatMap((e) => e.content?.parts ?? []) + .some((p) => p.codeExecutionResult), + ).toBe(false) + expect(sandbox.kill).not.toHaveBeenCalled() + await executor.close() + expect(sandbox.kill).not.toHaveBeenCalled() + }) + + it('does not reinterpret JavaScript fences as Python', async () => { + const { create } = mockSandbox() + const executor = new E2BCodeExecutor() + const model = new ScriptedModel([ + { + content: { + role: 'model', + parts: [{ text: '```javascript\nconsole.log(42)\n```' }], + }, + }, + ]) + const runner = new InMemoryRunner({ + agent: new LlmAgent({ name: 'agent', model, codeExecutor: executor }), + }) + await collect(runner.runEphemeral({ userId: 'user', newMessage: message })) + expect(create).not.toHaveBeenCalled() + await executor.close() + }) +}) diff --git a/packages/e2b-adk-js/tests/tools.test.ts b/packages/e2b-adk-js/tests/tools.test.ts new file mode 100644 index 0000000..5abcf45 --- /dev/null +++ b/packages/e2b-adk-js/tests/tools.test.ts @@ -0,0 +1,466 @@ +import { describe, expect, it, vi } from 'vitest' +import { + CommandExitError, + FileNotFoundError, + InvalidArgumentError, + NotFoundError, +} from '@e2b/code-interpreter' +import { Context } from '@google/adk' +import { E2BToolset } from '../src/index.js' +import { + context, + execution, + mockSandbox, + statefulSandboxes, + toolContext, +} from './helpers.js' + +async function invoke( + toolset: E2BToolset, + name: string, + args: Record, + ctx = toolContext(), +) { + const tool = (await toolset.getTools()).find((tool) => tool.name === name)! + return tool.runAsync({ args, toolContext: ctx }) +} + +describe('six tools', () => { + it('exports declarations without creating a sandbox', async () => { + const { create } = mockSandbox() + const tools = await new E2BToolset({ maxOperationMs: 90_000 }).getTools() + expect(tools.map((t) => t.name)).toEqual([ + 'run_code', + 'run_command', + 'write_file', + 'read_file', + 'list_files', + 'start_background_command', + ]) + for (const t of tools) expect(t._getDeclaration()?.parameters).toBeDefined() + expect(JSON.stringify(tools[0]!._getDeclaration())).toContain( + '1 to 90000. Defaults to 60000', + ) + expect(create).not.toHaveBeenCalled() + }) + + it.each(['python', 'javascript', 'typescript', 'r', 'java', 'bash'])( + 'forwards %s through its code context and preserves runtime error semantics', + async (language) => { + const { sandbox } = mockSandbox() + sandbox.runCode.mockResolvedValue( + execution({ + error: { name: 'ValueError', value: 'bad', traceback: 'trace' }, + }), + ) + const toolset = new E2BToolset() + const args = { code: 'code', language, timeoutMs: 1200, envs: { X: '1' } } + const expected = { + success: true, + stdout: 'hello\n', + stderr: '', + text: '42', + error: 'ValueError: bad\ntrace', + } + expect(await invoke(toolset, 'run_code', args)).toEqual({ + ...expected, + workspace: 'new', + }) + expect(await invoke(toolset, 'run_code', args)).toEqual(expected) + expect(sandbox.runCode).toHaveBeenCalledWith('code', { + language, + timeoutMs: 6200, + envs: { X: '1' }, + }) + }, + ) + + it.each([ + ['run_code', { code: 1 }], + ['run_code', { code: 'x', language: 'cmd' }], + ['run_code', { code: 'x', timeoutMs: -1 }], + ['run_code', { code: 'x', timeoutMs: 0 }], + ['run_code', { code: 'x', timeoutMs: 600_001 }], + ['run_code', { code: 'x', timeoutMs: 1.5 }], + ['run_code', { code: 'x', envs: { A: 2 } }], + ['run_command', { command: '' }], + ['run_command', { command: 'x', timeoutMs: 0 }], + ['run_command', { command: 'x', timeoutMs: 600_001 }], + ['write_file', { path: 'x' }], + ['read_file', { path: '' }], + ['list_files', { path: 7 }], + ['start_background_command', { command: 'x', port: 0 }], + ['start_background_command', { command: 'x', port: 65536 }], + ['start_background_command', { command: 'x', port: 1.5 }], + ['start_background_command', { command: 'x', timeoutMs: 0 }], + ])('rejects invalid %s input before SDK effects', async (name, args) => { + const { create } = mockSandbox() + expect( + await invoke( + new E2BToolset(), + name as string, + args as Record, + ), + ).toMatchObject({ success: false }) + expect(create).not.toHaveBeenCalled() + }) + + it('applies a custom maxOperationMs to the schema and the default limit', async () => { + const { sandbox, create } = mockSandbox() + const toolset = new E2BToolset({ maxOperationMs: 1000 }) + expect( + await invoke(toolset, 'run_command', { command: 'x', timeoutMs: 1001 }), + ).toMatchObject({ success: false }) + expect(create).not.toHaveBeenCalled() + await invoke(toolset, 'run_code', { code: 'x' }) + expect(sandbox.runCode.mock.calls[0]![1]).toMatchObject({ timeoutMs: 6000 }) + for (const maxOperationMs of [0, 2_147_483_648]) + expect(() => new E2BToolset({ maxOperationMs })).toThrow('maxOperationMs') + expect(() => new E2BToolset({ sandboxOptions: { timeoutMs: 0 } })).toThrow( + 'sandboxOptions.timeoutMs', + ) + }) + + it('uses one workspace for file operations, including empty files', async () => { + const { create, sandbox } = mockSandbox() + const toolset = new E2BToolset() + expect( + await invoke(toolset, 'write_file', { path: 'a.txt', content: '' }), + ).toEqual({ success: true, path: 'a.txt', workspace: 'new' }) + expect(sandbox.files.write).toHaveBeenCalledWith('a.txt', '') + expect(await invoke(toolset, 'read_file', { path: 'a.txt' })).toEqual({ + success: true, + path: 'a.txt', + content: 'contents', + }) + expect(await invoke(toolset, 'list_files', {})).toEqual({ + success: true, + entries: [{ name: 'test.txt', type: 'file' }], + truncated: false, + }) + expect(create).toHaveBeenCalledTimes(1) + }) + + it('runs commands in a process group and normalizes nonzero exits', async () => { + const { sandbox } = mockSandbox() + const toolset = new E2BToolset() + expect( + await invoke(toolset, 'run_command', { + command: 'echo ok', + cwd: '/tmp', + envs: { X: 'y' }, + timeoutMs: 5000, + }), + ).toEqual({ + success: true, + stdout: 'ok', + stderr: '', + exit_code: 0, + workspace: 'new', + }) + expect(sandbox.commands.run).toHaveBeenCalledWith( + "exec setsid -w /bin/bash -c 'echo ok'", + { cwd: '/tmp', envs: { X: 'y' }, background: true, timeoutMs: 0 }, + ) + sandbox.handle.wait.mockRejectedValueOnce( + new CommandExitError({ + stdout: 'out', + stderr: 'err', + exitCode: 7, + error: 'failed', + }), + ) + expect(await invoke(toolset, 'run_command', { command: 'exit 7' })).toEqual( + { success: true, stdout: 'out', stderr: 'err', exit_code: 7 }, + ) + }) + + it('reports stopped commands and code explicitly', async () => { + vi.useFakeTimers() + try { + const { sandbox } = mockSandbox() + const exited = deferred() + sandbox.handle.wait.mockReturnValueOnce(exited.promise) + sandbox.commands.run.mockImplementation(async (command, opts) => { + if (opts?.background) return sandbox.handle + if (command.startsWith('kill')) exited.reject(new Error('killed')) + return { stdout: '', stderr: '', exitCode: 0 } + }) + const toolset = new E2BToolset() + const command = invoke(toolset, 'run_command', { + command: 'sleep 600', + timeoutMs: 1000, + }) + await vi.advanceTimersByTimeAsync(1000) + expect(await command).toEqual({ + success: false, + error: + 'Command timed out after 1000 ms; it and its child processes were killed.', + timed_out: true, + stdout: '', + stderr: '', + workspace: 'new', + }) + const running = deferred() + sandbox.runCode.mockReturnValueOnce(running.promise) + sandbox.restartCodeContext.mockImplementation(async () => + running.reject(new Error('closed')), + ) + const abort = new AbortController() + const code = invoke( + toolset, + 'run_code', + { code: 'while True: pass', language: 'r' }, + toolContext(context('session', 'user', abort.signal)), + ) + await vi.advanceTimersByTimeAsync(10) + abort.abort() + await vi.advanceTimersByTimeAsync(10) + expect(await code).toEqual({ + success: false, + error: + 'Execution was cancelled. The r kernel was restarted; its variables and imports were lost.', + aborted: true, + kernel_restarted: true, + }) + } finally { + vi.useRealTimers() + } + }) + + it('returns safe, actionable failures without leaking SDK messages or logging arguments', async () => { + const { sandbox } = mockSandbox() + const log = vi.spyOn(console, 'log') + const debug = vi.spyOn(console, 'debug') + const warn = vi.spyOn(console, 'warn') + sandbox.runCode.mockRejectedValue( + new Error('secret-key raw-code private-file'), + ) + const toolset = new E2BToolset() + const result = await invoke(toolset, 'run_code', { + code: 'raw-code', + envs: { KEY: 'secret-key' }, + }) + expect(result).toEqual({ + success: false, + error: + 'The connection to the python kernel was lost, so it was restarted: its variables and imports were lost. Files in the sandbox are kept.', + kernel_restarted: true, + workspace: 'new', + }) + expect(JSON.stringify(result)).not.toMatch( + /secret-key|raw-code|private-file/, + ) + sandbox.files.read.mockRejectedValue( + new FileNotFoundError('/secret/path token=abc'), + ) + const missing = await invoke(toolset, 'read_file', { path: '/x' }) + expect(missing).toEqual({ + success: false, + error: 'Sandbox operation failed (FileNotFoundError).', + }) + expect(log).not.toHaveBeenCalled() + expect(debug).not.toHaveBeenCalled() + expect(warn).not.toHaveBeenCalled() + }) + + it('lets the model correct a refused command, but blocks after an ambiguous start', async () => { + const { sandbox } = mockSandbox() + const toolset = new E2BToolset() + sandbox.commands.run.mockRejectedValueOnce( + new InvalidArgumentError("[invalid_argument] cwd '/nope' does not exist"), + ) + expect( + await invoke(toolset, 'run_command', { command: 'ls', cwd: '/nope' }), + ).toEqual({ + success: false, + error: 'Sandbox operation failed (InvalidArgumentError).', + workspace: 'new', + }) + expect( + await invoke(toolset, 'run_command', { command: 'ls', cwd: '/tmp' }), + ).toMatchObject({ success: true }) + sandbox.commands.run.mockRejectedValueOnce(new TypeError('fetch failed')) + expect( + await invoke(toolset, 'run_command', { command: 'ls' }), + ).toMatchObject({ + success: false, + error: expect.stringContaining('may still be running'), + }) + expect(await invoke(toolset, 'read_file', { path: '/a' })).toMatchObject({ + success: false, + error: expect.stringContaining('destroySession()'), + }) + }) + + it('tells the model when recovery or the server lost its kernel state, without claiming files are gone', async () => { + const { sandbox, create } = mockSandbox() + const toolset = new E2BToolset() + await invoke(toolset, 'run_code', { code: 'x = 1' }) + sandbox.runCode.mockRejectedValueOnce(new TypeError('terminated')) + expect(await invoke(toolset, 'run_code', { code: 'x' })).toEqual({ + success: false, + error: + 'The connection to the python kernel was lost (TypeError), so it was restarted: its variables and imports were lost. Files in the sandbox are kept.', + kernel_restarted: true, + }) + sandbox.runCode.mockRejectedValueOnce( + new NotFoundError('context 1234 secret-path'), + ) + const missing = await invoke(toolset, 'run_code', { + code: 'x', + language: 'r', + }) + expect(missing).toEqual({ + success: false, + error: + 'The r kernel state is no longer available (NotFoundError); the next call starts an empty kernel. Files in the sandbox are kept.', + kernel_reset: true, + }) + expect(JSON.stringify(missing)).not.toContain('secret-path') + expect( + await invoke(toolset, 'run_code', { code: 'x', language: 'r' }), + ).toMatchObject({ success: true }) + expect(create).toHaveBeenCalledTimes(1) + }) + + it('surfaces sandbox expiry once, then a new workspace', async () => { + const { sandbox, create, registry } = mockSandbox() + const toolset = new E2BToolset() + await invoke(toolset, 'write_file', { path: 'a', content: 'x' }) + registry.kill(sandbox.sandboxId) + expect(await invoke(toolset, 'read_file', { path: 'a' })).toEqual({ + success: false, + error: expect.stringContaining('expired or was deleted'), + workspace_expired: true, + }) + expect(await invoke(toolset, 'read_file', { path: 'a' })).toMatchObject({ + success: true, + workspace: 'new', + }) + expect(create).toHaveBeenCalledTimes(2) + }) + + it('keys workspaces by app, user and session, never by one of them alone', async () => { + const { create } = mockSandbox() + const toolset = new E2BToolset() + for (const [id, user] of [ + ['s', 'alice'], + ['s', 'bob'], + ['t', 'alice'], + ['s', 'alice'], + ] as const) + await invoke(toolset, 'list_files', {}, toolContext(context(id, user))) + expect(create).toHaveBeenCalledTimes(3) + }) + + it('puts the namespace into the sandbox metadata used to find and destroy it', async () => { + const { create, registry } = mockSandbox() + statefulSandboxes(create) + const staging = new E2BToolset({ namespace: 'staging' }) + const prod = new E2BToolset({ namespace: 'prod' }) + const unset = new E2BToolset() + await invoke(staging, 'list_files', {}) + expect(await invoke(prod, 'list_files', {})).toMatchObject({ + workspace: 'new', + }) + // Without a namespace, an instance must not find the namespaced sandboxes. + expect(await invoke(unset, 'list_files', {})).toMatchObject({ + workspace: 'new', + }) + expect(create).toHaveBeenCalledTimes(3) + expect([...registry.entries.values()].map((e) => e.metadata)).toEqual([ + expect.objectContaining({ adkNamespace: 'staging', adkAppName: 'test' }), + expect.objectContaining({ adkNamespace: 'prod', adkAppName: 'test' }), + expect.objectContaining({ adkNamespace: 'default', adkAppName: 'test' }), + ]) + const session = { appName: 'test', userId: 'user', id: 'session' } + await unset.destroySession(session) + expect(registry.running()).toHaveLength(2) + await prod.destroySession(session) + expect(registry.running()).toHaveLength(1) + }) + + it('requires an ADK invocation context', async () => { + const { create } = mockSandbox() + const tool = (await new E2BToolset().getTools())[2]! + expect( + await tool.runAsync({ + args: { path: 'a', content: 'x' }, + toolContext: {} as Context, + }), + ).toMatchObject({ success: false }) + expect(create).not.toHaveBeenCalled() + }) + + it('bounds code, file and directory outputs', async () => { + const { sandbox } = mockSandbox() + sandbox.files.read.mockResolvedValue('x'.repeat(20000)) + sandbox.files.list.mockResolvedValue( + Array.from({ length: 150 }, () => ({ name: 'x', type: 'file' })), + ) + const toolset = new E2BToolset() + const read = (await invoke(toolset, 'read_file', { path: 'x' })) as { + content: string + } + expect(read.content).toHaveLength(10000) + expect(read.content).toContain('[truncated]') + const list = (await invoke(toolset, 'list_files', {})) as { + entries: unknown[] + truncated: boolean + } + expect(list.entries).toHaveLength(100) + expect(list.truncated).toBe(true) + }) + + it('keeps background processes alive by default, returns the PID and an unverified URL', async () => { + const { sandbox } = mockSandbox() + const toolset = new E2BToolset() + expect( + await invoke(toolset, 'start_background_command', { + command: 'server', + port: 3000, + }), + ).toEqual({ + success: true, + pid: 101, + preview_url: 'https://3000-fake.e2b.app', + readiness: 'unknown', + workspace: 'new', + }) + expect(sandbox.handle.disconnect).toHaveBeenCalledTimes(1) + expect(sandbox.commands.run).toHaveBeenCalledWith('server', { + background: true, + timeoutMs: 0, + cwd: undefined, + envs: undefined, + }) + await invoke(toolset, 'start_background_command', { + command: 'server', + timeoutMs: 5000, + }) + expect(sandbox.commands.run).toHaveBeenLastCalledWith( + 'server', + expect.objectContaining({ timeoutMs: 5000 }), + ) + sandbox.getHost.mockImplementation(() => { + throw new Error('host') + }) + expect( + await invoke(toolset, 'start_background_command', { + command: 'server', + port: 3000, + }), + ).toMatchObject({ success: true, pid: 101, preview_url: null }) + expect( + await invoke(toolset, 'start_background_command', { command: 'server' }), + ).toEqual({ success: true, pid: 101, preview_url: null }) + }) +}) + +function deferred() { + let reject!: (reason: unknown) => void + const promise = new Promise((_, rej) => (reject = rej)) + promise.catch(() => undefined) + return { promise, reject } +} diff --git a/packages/e2b-adk-js/tests/workspace.test.ts b/packages/e2b-adk-js/tests/workspace.test.ts new file mode 100644 index 0000000..4905014 --- /dev/null +++ b/packages/e2b-adk-js/tests/workspace.test.ts @@ -0,0 +1,956 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + CommandExitError, + FileNotFoundError, + InvalidArgumentError, + NotFoundError, + SandboxError, + TimeoutError, + type Sandbox, +} from '@e2b/code-interpreter' +import { + OperationAbortedError, + WorkspaceBlockedError, + WorkspaceDestroyedError, + WorkspaceExpiredError, +} from '../src/results.js' +import { Workspace } from '../src/workspace.js' +import { + deferred, + execution, + mockSandbox, + statefulSandboxes, + tick, + type FakeSandbox, +} from './helpers.js' + +afterEach(() => vi.useRealTimers()) + +const info = () => ({ created: false }) +const tag = { adkSessionId: 's' } +const workspace = (options = {}) => + new Workspace(options, 'Retry destroy.', tag) + +describe('workspace lifecycle', () => { + it('is lazy, forwards only supplied SDK options and reports creation', async () => { + const { create, sandbox } = mockSandbox() + const options = { + timeoutMs: 1200, + allowInternetAccess: false, + lifecycle: { onTimeout: 'pause' as const, autoResume: true }, + } + const ws = workspace({ template: 'custom', sandboxOptions: options }) + expect(create).not.toHaveBeenCalled() + const first = info() + await ws.run(async () => 1, first) + expect(first.created).toBe(true) + expect(create).toHaveBeenCalledWith('custom', { + ...options, + metadata: { adkSessionId: 's' }, + }) + const second = info() + await ws.run(async () => 1, second) + expect(second.created).toBe(false) + expect(create).toHaveBeenCalledTimes(1) + // Lifetime is extended through connect(), which never shortens it. + expect(sandbox.connect).toHaveBeenCalledWith({ timeoutMs: 1200 }) + expect(sandbox.kill).not.toHaveBeenCalled() + }) + + it('serializes work; a cancelled waiter has no SDK effect and does not let the next one overtake', async () => { + const { sandbox } = mockSandbox() + const ws = workspace() + const gate = deferred() + const order: string[] = [] + const a = ws.run(async () => { + order.push('a') + await gate.promise + order.push('a done') + }, info()) + const cancel = new AbortController() + const skipped = vi.fn() + const b = ws.run(skipped, info(), cancel.signal) + const c = ws.run(async () => order.push('c'), info()) + await tick() + cancel.abort() + await expect(b).rejects.toBeInstanceOf(OperationAbortedError) + await tick() + expect(order).toEqual(['a']) + gate.resolve() + await Promise.all([a, c]) + expect(order).toEqual(['a', 'a done', 'c']) + expect(skipped).not.toHaveBeenCalled() + expect(sandbox.restartCodeContext).not.toHaveBeenCalled() + expect(sandbox.commands.run).not.toHaveBeenCalled() + }) + + it('starts an operation deadline when it is served, not when it is queued', async () => { + vi.useFakeTimers() + const { sandbox } = mockSandbox() + sandbox.runCode.mockImplementation( + () => + new Promise((resolve) => setTimeout(() => resolve(execution()), 2000)), + ) + const ws = workspace() + const first = ws.run( + () => new Promise((resolve) => setTimeout(resolve, 5000)), + info(), + ) + const queued = ws.runCode('x', 'python', undefined, info(), { + timeoutMs: 3000, + }) + await vi.advanceTimersByTimeAsync(8000) + await first + expect(await queued).toHaveProperty('execution') + expect(sandbox.restartCodeContext).not.toHaveBeenCalled() + }) + + it('runs code in the sandbox default kernel of each language', async () => { + const { sandbox } = mockSandbox() + const ws = workspace() + await ws.runCode('a', 'python', { X: '1' }, info(), { timeoutMs: 1000 }) + await ws.runCode('c', 'r', undefined, info(), { timeoutMs: 1000 }) + expect(sandbox.runCode).toHaveBeenNthCalledWith(1, 'a', { + language: 'python', + envs: { X: '1' }, + timeoutMs: 6000, + }) + expect(sandbox.runCode).toHaveBeenNthCalledWith(2, 'c', { + language: 'r', + envs: undefined, + timeoutMs: 6000, + }) + }) + it('blocks the workspace when a kernel restart fails, until destroy recovers it', async () => { + const { sandbox, create } = mockSandbox() + sandbox.runCode.mockRejectedValueOnce(new TimeoutError('timed out')) + sandbox.restartCodeContext.mockRejectedValueOnce(new Error('down')) + const ws = workspace() + await expect( + ws.runCode('loop', 'java', undefined, info(), { timeoutMs: 1000 }), + ).rejects.toThrow('The java kernel could not be restarted') + const skipped = vi.fn() + await expect(ws.run(skipped, info())).rejects.toBeInstanceOf( + WorkspaceBlockedError, + ) + expect(skipped).not.toHaveBeenCalled() + await ws.destroy() + const later = info() + await ws.run(async () => 1, later) + expect(later.created).toBe(true) + expect(create).toHaveBeenCalledTimes(2) + }) + + it('runs commands in their own process group without passing the abort signal to the SDK', async () => { + const { sandbox } = mockSandbox() + const ws = workspace() + const abort = new AbortController() + const outcome = await ws.runCommand( + "echo 'hi'", + { cwd: '/tmp', envs: { X: 'y' } }, + info(), + { timeoutMs: 1000, signal: abort.signal }, + ) + expect(outcome).toEqual({ + result: { stdout: 'ok', stderr: '', exitCode: 0 }, + }) + expect(sandbox.commands.run).toHaveBeenCalledWith( + `exec setsid -w /bin/bash -c 'echo '\\''hi'\\'''`, + { cwd: '/tmp', envs: { X: 'y' }, background: true, timeoutMs: 0 }, + ) + const exit = new CommandExitError({ + stdout: 'out', + stderr: 'err', + exitCode: 7, + error: 'failed', + }) + sandbox.handle.wait.mockRejectedValueOnce(exit) + expect( + await ws.runCommand('exit 7', {}, info(), { timeoutMs: 1000 }), + ).toEqual({ result: exit }) + }) + + it.each(['deadline', 'abort', 'abort before the handle exists'])( + '%s kills the command process group', + async (how) => { + vi.useFakeTimers() + const { sandbox } = mockSandbox() + const exited = deferred() + exited.promise.catch(() => undefined) + sandbox.handle.wait.mockReturnValueOnce(exited.promise) + Object.assign(sandbox.handle, { stdout: 'partial', stderr: '' }) + const start = deferred() + sandbox.commands.run.mockImplementation(async (command, opts) => { + if (opts?.background) return start.promise + if (command === 'kill -9 -- -101') exited.reject(new Error('killed')) + return { stdout: '', stderr: '', exitCode: 0 } + }) + const abort = new AbortController() + const ws = workspace() + const result = ws.runCommand('sleep 600', {}, info(), { + timeoutMs: 1000, + signal: abort.signal, + }) + await vi.advanceTimersByTimeAsync(10) + if (how === 'abort before the handle exists') abort.abort() + start.resolve(sandbox.handle) + await vi.advanceTimersByTimeAsync(10) + if (how === 'abort') abort.abort() + await vi.advanceTimersByTimeAsync(1000) + const stopped = how === 'deadline' ? 'timed_out' : 'aborted' + expect(await result).toEqual({ stopped, stdout: 'partial', stderr: '' }) + expect(sandbox.commands.run).toHaveBeenCalledWith('kill -9 -- -101') + expect(vi.getTimerCount()).toBe(0) + }, + ) + + it('destroy fails queued work without recreating, rejects new work meanwhile, then allows a new workspace', async () => { + const { sandbox, create } = mockSandbox() + const ws = workspace() + await ws.run(async () => 1, info()) + const gate = deferred() + const running = ws.run(() => gate.promise, info()) + const queuedWork = vi.fn() + const queued = ws.run(queuedWork, info()) + const kill = deferred() + sandbox.kill.mockReturnValueOnce(kill.promise) + const destroy = ws.destroy() + expect(ws.destroy()).toBe(destroy) + await expect(ws.run(async () => 1, info())).rejects.toBeInstanceOf( + WorkspaceDestroyedError, + ) + kill.resolve(true) + await destroy + gate.resolve() + await expect(running).rejects.toBeInstanceOf(WorkspaceDestroyedError) + await expect(queued).rejects.toBeInstanceOf(WorkspaceDestroyedError) + expect(queuedWork).not.toHaveBeenCalled() + expect(create).toHaveBeenCalledTimes(1) + expect(ws.idle).toBe(true) + const later = info() + await ws.run(async () => 1, later) + expect(later.created).toBe(true) + expect(create).toHaveBeenCalledTimes(2) + }) + + it('keeps a failed kill handle blocked and retryable', async () => { + const { sandbox, create } = mockSandbox() + const ws = workspace() + await ws.run(async () => 1, info()) + sandbox.kill.mockRejectedValueOnce(new Error('Unavailable')) + await expect(ws.destroy()).rejects.toThrow( + 'Sandbox cleanup failed. Retry destroy.', + ) + const work = vi.fn() + const renewals = sandbox.connect.mock.calls.length + await expect(ws.run(work, info())).rejects.toThrow('Retry destroy.') + expect(work).not.toHaveBeenCalled() + expect(sandbox.connect).toHaveBeenCalledTimes(renewals) + expect(ws.idle).toBe(false) + await ws.destroy() + expect(sandbox.kill).toHaveBeenCalledTimes(2) + await ws.run(async () => 1, info()) + expect(create).toHaveBeenCalledTimes(2) + }) + + it('reports an expired sandbox once, then starts a new workspace', async () => { + const { sandbox, create, registry } = mockSandbox() + const ws = workspace() + await ws.runCode('x = 1', 'python', undefined, info(), { timeoutMs: 1000 }) + registry.kill(sandbox.sandboxId) + const work = vi.fn() + await expect(ws.run(work, info())).rejects.toBeInstanceOf( + WorkspaceExpiredError, + ) + expect(work).not.toHaveBeenCalled() + const next = info() + await ws.run(async () => 1, next) + expect(next.created).toBe(true) + expect(create).toHaveBeenCalledTimes(2) + }) + it('resumes a paused sandbox instead of replacing it', async () => { + const { sandbox, create, registry } = mockSandbox() + const ws = workspace({ sandboxOptions: { timeoutMs: 1200 } }) + await ws.runCode('x = 1', 'python', undefined, info(), { timeoutMs: 1000 }) + registry.pause(sandbox.sandboxId) + const resumed = info() + expect(await ws.run(async () => 'kept', resumed)).toBe('kept') + expect(sandbox.connect).toHaveBeenCalledWith({ timeoutMs: 1200 }) + expect(resumed.created).toBe(false) + expect(registry.alive(sandbox.sandboxId)?.state).toBe('running') + expect(create).toHaveBeenCalledTimes(1) + }) + it('does not treat a missing file or other refresh failure as expiry', async () => { + const { sandbox, create } = mockSandbox() + const ws = workspace() + await ws.run(async () => 1, info()) + await expect( + ws.run(async () => { + throw new FileNotFoundError('missing') + }, info()), + ).rejects.toBeInstanceOf(FileNotFoundError) + sandbox.connect.mockRejectedValue(new Error('Transient')) + expect(await ws.run(async () => 42, info())).toBe(42) + expect(create).toHaveBeenCalledTimes(1) + }) + + it('refreshes throughout operations exceeding TTL and stops when they end', async () => { + vi.useFakeTimers() + const { sandbox } = mockSandbox() + const ws = workspace({ sandboxOptions: { timeoutMs: 1000 } }) + await ws.run(async () => 1, info()) + const calls = sandbox.connect.mock.calls.length + const finish = deferred() + const work = ws.run(() => finish.promise, info()) + await vi.advanceTimersByTimeAsync(2500) + expect(sandbox.connect.mock.calls.length - calls).toBe(6) + const rejected = expect(work).rejects.toThrow('Execution failed') + finish.reject(new Error('Execution failed')) + await rejected + expect(vi.getTimerCount()).toBe(0) + await vi.advanceTimersByTimeAsync(10_000) + expect(sandbox.connect.mock.calls.length - calls).toBe(7) + }) +}) + +describe('cleanup and deadline boundaries', () => { + const noSuchGroup = () => + new CommandExitError({ + stdout: '', + stderr: 'bash: kill: (-101) - No such process\n', + exitCode: 1, + error: 'exit status 1', + }) + + it('destroy waits for an in-flight create and kills the late sandbox', async () => { + const { sandbox, create } = mockSandbox() + const pending = deferred() + create.mockReturnValueOnce(pending.promise) + const ws = workspace() + const work = vi.fn() + const first = ws.run(work, info()) + await tick() + let destroyed = false + const destroy = ws.destroy().then(() => (destroyed = true)) + await tick() + expect(destroyed).toBe(false) + pending.resolve(sandbox as unknown as Sandbox) + await destroy + expect(sandbox.kill).toHaveBeenCalledTimes(1) + await expect(first).rejects.toBeInstanceOf(WorkspaceDestroyedError) + expect(work).not.toHaveBeenCalled() + }) + + it('keeps a late sandbox whose kill failed blocked and retryable', async () => { + const { sandbox, create } = mockSandbox() + const pending = deferred() + create.mockReturnValueOnce(pending.promise) + sandbox.kill.mockRejectedValueOnce(new Error('network down')) + const ws = workspace() + const first = ws.run(async () => 1, info()).catch((error) => error) + await tick() + const destroy = ws.destroy() + pending.resolve(sandbox as unknown as Sandbox) + await expect(destroy).rejects.toThrow('Sandbox cleanup failed') + expect(await first).toBeInstanceOf(WorkspaceDestroyedError) + expect(ws.idle).toBe(false) + const work = vi.fn() + await expect(ws.run(work, info())).rejects.toThrow('Retry destroy.') + expect(work).not.toHaveBeenCalled() + await ws.destroy() + expect(sandbox.kill).toHaveBeenCalledTimes(2) + expect(ws.idle).toBe(true) + await ws.run(async () => 2, info()) + expect(create).toHaveBeenCalledTimes(2) + }) + + it('does not start an operation whose workspace was destroyed during the TTL refresh', async () => { + const { sandbox } = mockSandbox() + const ws = workspace() + await ws.run(async () => 1, info()) + const refresh = deferred() + sandbox.connect.mockReturnValueOnce(refresh.promise) + const effect = vi.fn(async () => 42) + const run = ws.run(effect, info()) + await tick() + await ws.destroy() + refresh.resolve() + await expect(run).rejects.toBeInstanceOf(WorkspaceDestroyedError) + expect(effect).not.toHaveBeenCalled() + }) + + it('does not start an operation after a failed destroy blocked the live sandbox', async () => { + const { sandbox } = mockSandbox() + const ws = workspace() + await ws.run(async () => 1, info()) + const refresh = deferred() + sandbox.connect.mockReturnValueOnce(refresh.promise) + const effect = vi.fn(async () => 42) + const run = ws.run(effect, info()) + await tick() + sandbox.kill.mockRejectedValueOnce(new Error('down')) + await expect(ws.destroy()).rejects.toThrow('Sandbox cleanup failed') + refresh.resolve() + await expect(run).rejects.toBeInstanceOf(WorkspaceDestroyedError) + expect(effect).not.toHaveBeenCalled() + }) + + it('does not report success for an operation that finished after destroy', async () => { + mockSandbox() + const ws = workspace() + const gate = deferred() + const run = ws.run(() => gate.promise, info()) + await tick() + await tick() + await ws.destroy() + gate.resolve(42) + await expect(run).rejects.toBeInstanceOf(WorkspaceDestroyedError) + }) + + it('blocks the workspace instead of trusting a leader-only kill when the group kill fails', async () => { + vi.useFakeTimers() + const { sandbox } = mockSandbox() + const exited = deferred() + exited.promise.catch(() => undefined) + sandbox.handle.wait.mockReturnValueOnce(exited.promise) + sandbox.handle.kill.mockImplementation(async () => { + exited.reject(new Error('killed')) + return true + }) + sandbox.commands.run.mockImplementation(async (_command, opts) => { + if (opts?.background) return sandbox.handle + throw new Error('network failure') + }) + const ws = workspace() + const run = ws + .runCommand('loop', {}, info(), { timeoutMs: 100 }) + .catch((error) => error) + await vi.advanceTimersByTimeAsync(100) + const error = await run + expect(error).toBeInstanceOf(WorkspaceBlockedError) + expect(error.message).toContain('could not be confirmed') + expect(sandbox.handle.kill).not.toHaveBeenCalled() + const effect = vi.fn(async () => 42) + await expect(ws.run(effect, info())).rejects.toBeInstanceOf( + WorkspaceBlockedError, + ) + expect(effect).not.toHaveBeenCalled() + await ws.destroy() + expect(await ws.run(effect, info())).toBe(42) + }) + + it('blocks the workspace when the command does not exit after its group was killed', async () => { + vi.useFakeTimers() + const { sandbox } = mockSandbox() + sandbox.handle.wait.mockReturnValue(new Promise(() => {})) + const ws = workspace() + const run = ws + .runCommand('loop', {}, info(), { timeoutMs: 100 }) + .catch((error) => error) + await vi.advanceTimersByTimeAsync(31_000) + expect(await run).toBeInstanceOf(WorkspaceBlockedError) + await expect(ws.run(async () => 1, info())).rejects.toBeInstanceOf( + WorkspaceBlockedError, + ) + }) + + it('treats "No such process" from the group kill as confirmed absence', async () => { + vi.useFakeTimers() + const { sandbox } = mockSandbox() + const exited = deferred() + exited.promise.catch(() => undefined) + sandbox.handle.wait.mockReturnValueOnce(exited.promise) + sandbox.commands.run.mockImplementation(async (_command, opts) => { + if (opts?.background) return sandbox.handle + exited.reject(new Error('exited')) + throw noSuchGroup() + }) + const ws = workspace() + const run = ws.runCommand('x', {}, info(), { timeoutMs: 100 }) + await vi.advanceTimersByTimeAsync(100) + expect(await run).toMatchObject({ stopped: 'timed_out' }) + expect(await ws.run(async () => 1, info())).toBe(1) + }) + + it('counts a slow command start against the deadline and kills the late handle', async () => { + vi.useFakeTimers() + const { sandbox } = mockSandbox() + const exited = deferred() + exited.promise.catch(() => undefined) + sandbox.handle.wait.mockReturnValueOnce(exited.promise) + const start = deferred() + sandbox.commands.run.mockImplementation(async (command, opts) => { + if (opts?.background) return start.promise + if (command === 'kill -9 -- -101') exited.reject(new Error('killed')) + return { stdout: '', stderr: '', exitCode: 0 } + }) + const ws = workspace() + let settled = false + const run = ws + .runCommand('echo x', {}, info(), { timeoutMs: 100 }) + .finally(() => (settled = true)) + await vi.advanceTimersByTimeAsync(10_000) + expect(settled).toBe(false) + start.resolve(sandbox.handle) + await vi.advanceTimersByTimeAsync(0) + expect(await run).toMatchObject({ stopped: 'timed_out' }) + expect(sandbox.commands.run).toHaveBeenCalledWith('kill -9 -- -101') + }) + + it('blocks the workspace when a stopped command never delivers its handle, and kills it if it arrives', async () => { + vi.useFakeTimers() + const { sandbox } = mockSandbox() + const start = deferred() + sandbox.commands.run.mockImplementation(async (_command, opts) => + opts?.background + ? start.promise + : { stdout: '', stderr: '', exitCode: 0 }, + ) + const ws = workspace() + const run = ws + .runCommand('echo x', {}, info(), { timeoutMs: 100 }) + .catch((error) => error) + await vi.advanceTimersByTimeAsync(31_000) + expect(await run).toBeInstanceOf(WorkspaceBlockedError) + await expect(ws.run(async () => 1, info())).rejects.toBeInstanceOf( + WorkspaceBlockedError, + ) + start.resolve(sandbox.handle) + await vi.advanceTimersByTimeAsync(0) + expect(sandbox.commands.run).toHaveBeenCalledWith('kill -9 -- -101') + }) + + it('lets a deadline request without a kernel settle, then restarts the kernel it started', async () => { + vi.useFakeTimers() + const { sandbox } = mockSandbox() + const ws = workspace() + const late = deferred() + late.promise.catch(() => undefined) + sandbox.runCode.mockReturnValueOnce(late.promise) + sandbox.listCodeContexts + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + { id: 'ctx-java', language: 'java', cwd: '/home/user' }, + ]) + const run = ws.runCode('x', 'java', undefined, info(), { timeoutMs: 100 }) + await vi.advanceTimersByTimeAsync(100) + expect(sandbox.restartCodeContext).not.toHaveBeenCalled() + late.reject(new Error('closed')) + expect(await run).toEqual({ stopped: 'timed_out' }) + expect(sandbox.restartCodeContext).toHaveBeenCalledTimes(1) + }) + + it('returns the result when the code finishes while no kernel is listed', async () => { + vi.useFakeTimers() + const { sandbox } = mockSandbox() + const ws = workspace() + const slow = deferred() + sandbox.runCode.mockReturnValueOnce(slow.promise) + sandbox.listCodeContexts.mockResolvedValue([]) + const run = ws.runCode('x', 'java', undefined, info(), { timeoutMs: 100 }) + await vi.advanceTimersByTimeAsync(100) + slow.resolve({ logs: { stdout: ['1'], stderr: [] } }) + expect(await run).toEqual({ + execution: { logs: { stdout: ['1'], stderr: [] } }, + }) + expect(sandbox.restartCodeContext).not.toHaveBeenCalled() + }) + + it.each([ + ['dies after the deadline', 100], + ['never settles', 31_000], + ])( + 'blocks the workspace when a deadline request without a kernel %s', + async (_case, wait) => { + vi.useFakeTimers() + const { sandbox } = mockSandbox() + const ws = workspace() + const request = deferred() + request.promise.catch(() => undefined) + sandbox.runCode.mockReturnValueOnce(request.promise) + sandbox.listCodeContexts.mockResolvedValue([]) + const run = ws + .runCode('x', 'java', undefined, info(), { timeoutMs: 100 }) + .catch((error) => error) + await vi.advanceTimersByTimeAsync(wait) + if (wait === 100) request.reject(new Error('never started')) + expect(await run).toBeInstanceOf(WorkspaceBlockedError) + expect(sandbox.restartCodeContext).not.toHaveBeenCalled() + await expect(ws.run(async () => 1, info())).rejects.toBeInstanceOf( + WorkspaceBlockedError, + ) + }, + ) + + it('blocks the workspace when a stream breaks before any kernel is listed', async () => { + const { sandbox } = mockSandbox() + const ws = workspace() + sandbox.runCode.mockRejectedValueOnce(new Error('connection reset')) + sandbox.listCodeContexts.mockResolvedValue([]) + await expect( + ws.runCode('x', 'java', undefined, info(), { timeoutMs: 1_000 }), + ).rejects.toBeInstanceOf(WorkspaceBlockedError) + expect(sandbox.restartCodeContext).not.toHaveBeenCalled() + }) +}) + +describe('uncertain SDK outcomes', () => { + const invalidCwd = () => + new InvalidArgumentError("[invalid_argument] cwd '/nope' does not exist") + + it('blocks the workspace when a command start fails ambiguously', async () => { + const { sandbox } = mockSandbox() + sandbox.commands.run.mockRejectedValueOnce(new TypeError('fetch failed')) + const ws = workspace() + await expect( + ws.runCommand('x', {}, info(), { timeoutMs: 1000 }), + ).rejects.toThrow('may still be running') + const effect = vi.fn(async () => 1) + await expect(ws.run(effect, info())).rejects.toBeInstanceOf( + WorkspaceBlockedError, + ) + expect(effect).not.toHaveBeenCalled() + await ws.destroy() + expect(await ws.run(effect, info())).toBe(1) + }) + + it('does not block after the server refuses to start a command', async () => { + const { sandbox } = mockSandbox() + sandbox.commands.run.mockRejectedValueOnce(invalidCwd()) + const ws = workspace() + await expect( + ws.runCommand('x', { cwd: '/nope' }, info(), { timeoutMs: 1000 }), + ).rejects.toBeInstanceOf(InvalidArgumentError) + expect( + await ws.runCommand('x', { cwd: '/tmp' }, info(), { timeoutMs: 1000 }), + ).toHaveProperty('result') + }) + + it('stops the group when an established command stream is lost, then reports the failure', async () => { + const { sandbox } = mockSandbox() + sandbox.handle.wait.mockRejectedValue(new SandboxError('stream lost')) + const ws = workspace() + await expect( + ws.runCommand('x', {}, info(), { timeoutMs: 1000 }), + ).rejects.toThrow('stream lost') + expect(sandbox.commands.run).toHaveBeenCalledWith('kill -9 -- -101') + sandbox.handle.wait.mockResolvedValue({ + stdout: 'ok', + stderr: '', + exitCode: 0, + }) + expect( + await ws.runCommand('y', {}, info(), { timeoutMs: 1000 }), + ).toHaveProperty('result') + }) + + it('blocks when a lost command stream cannot be followed by a confirmed stop', async () => { + const { sandbox } = mockSandbox() + sandbox.handle.wait.mockRejectedValue(new SandboxError('stream lost')) + sandbox.commands.run.mockImplementation(async (_command, opts) => { + if (opts?.background) return sandbox.handle + throw new TypeError('fetch failed') + }) + const ws = workspace() + await expect( + ws.runCommand('x', {}, info(), { timeoutMs: 1000 }), + ).rejects.toBeInstanceOf(WorkspaceBlockedError) + await expect(ws.run(async () => 1, info())).rejects.toBeInstanceOf( + WorkspaceBlockedError, + ) + }) + + it('restarts the kernel when a code stream is lost, then reports the failure', async () => { + const { sandbox } = mockSandbox() + sandbox.runCode.mockRejectedValueOnce(new Error('connection reset')) + const ws = workspace() + expect( + await ws.runCode('x', 'python', undefined, info(), { timeoutMs: 1000 }), + ).toEqual({ kernelLost: 'restarted', cause: new Error('connection reset') }) + expect(sandbox.restartCodeContext).toHaveBeenCalledTimes(1) + expect( + await ws.runCode('y', 'python', undefined, info(), { timeoutMs: 1000 }), + ).toHaveProperty('execution') + }) + + it('blocks when a lost code stream cannot be followed by a kernel restart', async () => { + const { sandbox } = mockSandbox() + sandbox.runCode.mockRejectedValueOnce(new Error('connection reset')) + sandbox.restartCodeContext.mockRejectedValueOnce(new Error('down')) + const ws = workspace() + await expect( + ws.runCode('x', 'python', undefined, info(), { timeoutMs: 1000 }), + ).rejects.toBeInstanceOf(WorkspaceBlockedError) + await expect(ws.run(async () => 1, info())).rejects.toBeInstanceOf( + WorkspaceBlockedError, + ) + }) + + it('does not restart or block after a local argument error', async () => { + const { sandbox } = mockSandbox() + sandbox.runCode.mockRejectedValueOnce(new InvalidArgumentError('bad')) + const ws = workspace() + await expect( + ws.runCode('x', 'python', undefined, info(), { timeoutMs: 1000 }), + ).rejects.toBeInstanceOf(InvalidArgumentError) + expect(sandbox.restartCodeContext).not.toHaveBeenCalled() + expect( + await ws.runCode('y', 'python', undefined, info(), { timeoutMs: 1000 }), + ).toHaveProperty('execution') + }) + + it('reports a kernel the server no longer knows without restarting anything', async () => { + const { sandbox } = mockSandbox() + const ws = workspace() + const missing = new NotFoundError('context missing') + sandbox.runCode.mockRejectedValueOnce(missing) + expect( + await ws.runCode('b', 'python', undefined, info(), { timeoutMs: 1000 }), + ).toEqual({ kernelLost: 'missing', cause: missing }) + expect(sandbox.restartCodeContext).not.toHaveBeenCalled() + expect( + await ws.runCode('c', 'python', undefined, info(), { timeoutMs: 1000 }), + ).toHaveProperty('execution') + }) + it('applies the same start rule to background commands', async () => { + const { sandbox } = mockSandbox() + const ws = workspace() + sandbox.commands.run.mockRejectedValueOnce(invalidCwd()) + await expect( + ws.startBackground('server', { cwd: '/nope' }, undefined, info()), + ).rejects.toBeInstanceOf(InvalidArgumentError) + expect( + await ws.startBackground('server', {}, undefined, info()), + ).toMatchObject({ pid: 101 }) + expect(sandbox.handle.disconnect).toHaveBeenCalledTimes(1) + sandbox.commands.run.mockRejectedValueOnce(new TypeError('fetch failed')) + await expect( + ws.startBackground('server', {}, undefined, info()), + ).rejects.toBeInstanceOf(WorkspaceBlockedError) + await expect(ws.run(async () => 1, info())).rejects.toBeInstanceOf( + WorkspaceBlockedError, + ) + }) +}) + +describe('reconnect across processes', () => { + const other = () => new Workspace({}, 'Retry destroy.', tag) + + it('another process finds the session sandbox by metadata instead of creating one', async () => { + const { sandbox, create, list, connect } = mockSandbox() + const a = workspace() + const first = info() + await a.run(async () => 1, first) + expect(first.created).toBe(true) + const b = other() + const second = info() + expect(await b.run(async (box) => box.sandboxId, second)).toBe( + sandbox.sandboxId, + ) + expect(second.created).toBe(false) + expect(create).toHaveBeenCalledTimes(1) + expect(list).toHaveBeenCalledWith({ + query: { metadata: tag, state: ['running', 'paused'] }, + }) + expect(connect).toHaveBeenCalledWith(sandbox.sandboxId, { + timeoutMs: 300_000, + }) + }) + + it('another process resumes a paused session sandbox', async () => { + const { sandbox, create, registry } = mockSandbox() + await workspace().run(async () => 1, info()) + registry.pause(sandbox.sandboxId) + expect(await other().run(async (box) => box.sandboxId, info())).toBe( + sandbox.sandboxId, + ) + expect(registry.alive(sandbox.sandboxId)?.state).toBe('running') + expect(create).toHaveBeenCalledTimes(1) + }) + + it('never attaches to a sandbox tagged with another session', async () => { + const { create, registry } = mockSandbox() + const boxes = statefulSandboxes(create) + await new Workspace({}, 'x', { adkSessionId: 'someone-else' }).run( + async () => 1, + info(), + ) + const mine = info() + await workspace().run(async () => 1, mine) + expect(mine.created).toBe(true) + expect(boxes).toHaveLength(2) + expect(registry.running()).toHaveLength(2) + }) + + it('resolves a create race by keeping the oldest sandbox and killing its own', async () => { + const { create, list, registry } = mockSandbox() + const boxes = statefulSandboxes(create) + const a = workspace() + await a.run(async () => 1, info()) + // Process B listed before A's sandbox was visible, then created its own. + list.mockImplementationOnce(() => { + let done = false + return { + get hasNext() { + return !done + }, + nextItems: async () => { + done = true + return [] + }, + } as unknown as ReturnType + }) + const b = other() + const outcome = info() + const used = await b.run(async (box) => box.sandboxId, outcome) + expect(boxes).toHaveLength(2) + expect(used).toBe(boxes[0]!.sandboxId) + expect(outcome.created).toBe(false) + expect(boxes[1]!.kill).toHaveBeenCalledTimes(1) + expect(registry.running().map((e) => e.box.sandboxId)).toEqual([ + boxes[0]!.sandboxId, + ]) + }) + + it('a destroy from another process surfaces here as expiry, then a new workspace', async () => { + const { create } = mockSandbox() + statefulSandboxes(create) + const a = workspace() + await a.run(async () => 1, info()) + await other().destroy() + await expect(a.run(async () => 1, info())).rejects.toBeInstanceOf( + WorkspaceExpiredError, + ) + const fresh = info() + await a.run(async () => 1, fresh) + expect(fresh.created).toBe(true) + expect(create).toHaveBeenCalledTimes(2) + }) + + it('destroy kills session sandboxes this process never held', async () => { + const { create, kill, registry } = mockSandbox() + const boxes = statefulSandboxes(create) + await workspace().run(async () => 1, info()) + const b = other() + expect(b.idle).toBe(true) + await b.destroy() + expect(kill).toHaveBeenCalledWith(boxes[0]!.sandboxId) + expect(registry.running()).toHaveLength(0) + expect(b.idle).toBe(true) + }) + + it('a failed sweep kill blocks the workspace until a retry succeeds', async () => { + const { create, kill, registry } = mockSandbox() + statefulSandboxes(create) + await workspace().run(async () => 1, info()) + kill.mockRejectedValueOnce(new Error('down')) + const b = other() + await expect(b.destroy()).rejects.toThrow('Sandbox cleanup failed') + await expect(b.run(async () => 1, info())).rejects.toThrow('Retry destroy.') + await b.destroy() + expect(registry.running()).toHaveLength(0) + expect(await b.run(async () => 2, info())).toBe(2) + }) + + it('reports a sandbox that vanished mid-operation as expired, not blocked', async () => { + vi.useFakeTimers() + const { sandbox, registry } = mockSandbox() + sandbox.handle.wait.mockReturnValueOnce(new Promise(() => {})) + sandbox.commands.run.mockImplementation(async (_command, opts) => { + if (opts?.background) return sandbox.handle + // Killed elsewhere while the command ran: the kill request fails too. + registry.kill(sandbox.sandboxId) + throw new Error('connection lost') + }) + const ws = workspace() + const run = ws + .runCommand('loop', {}, info(), { timeoutMs: 100 }) + .catch((error) => error) + await vi.advanceTimersByTimeAsync(100) + expect(await run).toBeInstanceOf(WorkspaceExpiredError) + expect(ws.idle).toBe(true) + }) + + it('never uses its own sandbox when the race check fails; a retry reconciles', async () => { + const { create, list, registry } = mockSandbox() + const boxes = statefulSandboxes(create) + const a = workspace() + await a.run(async () => 1, info()) + const empty = () => { + let done = false + return { + get hasNext() { + return !done + }, + nextItems: async () => { + done = true + return [] + }, + } as unknown as ReturnType + } + list.mockImplementationOnce(empty).mockImplementationOnce(() => { + throw new Error('list failed') + }) + const b = other() + await expect(b.run(async () => 1, info())).rejects.toThrow('list failed') + expect(boxes[1]!.kill).toHaveBeenCalledTimes(1) + expect(b.idle).toBe(true) + // The next call finds the session's sandbox, not the discarded one. + expect(await b.run(async (box) => box.sandboxId, info())).toBe( + boxes[0]!.sandboxId, + ) + expect(registry.running().map((e) => e.box.sandboxId)).toEqual([ + boxes[0]!.sandboxId, + ]) + }) + + it('keeps a discarded sandbox blocked and retryable when its kill fails', async () => { + const { create, list } = mockSandbox() + const boxes = statefulSandboxes(create) + await workspace().run(async () => 1, info()) + const factory = create.getMockImplementation()! + create.mockImplementationOnce(async (...args: unknown[]) => { + const box = (await factory(...args)) as FakeSandbox + box.kill.mockRejectedValueOnce(new Error('down')) + return box + }) + const empty = () => { + let done = false + return { + get hasNext() { + return !done + }, + nextItems: async () => { + done = true + return [] + }, + } as unknown as ReturnType + } + list.mockImplementationOnce(empty).mockImplementationOnce(() => { + throw new Error('list failed') + }) + const b = other() + await expect(b.run(async () => 1, info())).rejects.toThrow( + 'Sandbox cleanup failed', + ) + expect(boxes).toHaveLength(2) + await expect(b.run(async () => 1, info())).rejects.toThrow('Retry destroy.') + await b.destroy() + expect(b.idle).toBe(true) + }) + it('scopes the session tag by namespace so deployments on one key stay apart', async () => { + const { create, registry } = mockSandbox() + const boxes = statefulSandboxes(create) + const staging = new Workspace({}, 'x', { adkNamespace: 'staging', ...tag }) + const prod = new Workspace({}, 'x', { adkNamespace: 'prod', ...tag }) + await staging.run(async () => 1, info()) + const mine = info() + await prod.run(async () => 1, mine) + expect(mine.created).toBe(true) + expect(boxes).toHaveLength(2) + await prod.destroy() + expect(registry.running().map((e) => e.box.sandboxId)).toEqual([ + boxes[0]!.sandboxId, + ]) + }) +}) diff --git a/packages/e2b-adk-js/tsconfig.build.json b/packages/e2b-adk-js/tsconfig.build.json new file mode 100644 index 0000000..55be52d --- /dev/null +++ b/packages/e2b-adk-js/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/e2b-adk-js/tsconfig.json b/packages/e2b-adk-js/tsconfig.json new file mode 100644 index 0000000..a9998fe --- /dev/null +++ b/packages/e2b-adk-js/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "types": ["node"], + "paths": { "@e2b/adk": ["./src/index.ts"] } + }, + "include": [ + "src/**/*.ts", + "tests/**/*.ts", + "scripts/**/*.ts", + "examples/**/*.ts", + "vitest.config.ts" + ] +} diff --git a/packages/e2b-adk-js/vitest.config.ts b/packages/e2b-adk-js/vitest.config.ts new file mode 100644 index 0000000..f89a083 --- /dev/null +++ b/packages/e2b-adk-js/vitest.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { include: ['tests/**/*.test.ts'], restoreMocks: true }, +}) diff --git a/.env.example b/packages/e2b-adk-python/.env.example similarity index 100% rename from .env.example rename to packages/e2b-adk-python/.env.example diff --git a/packages/e2b-adk-python/LICENSE b/packages/e2b-adk-python/LICENSE new file mode 100644 index 0000000..d4b8c19 --- /dev/null +++ b/packages/e2b-adk-python/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 FoundryLabs, Inc. + +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/e2b-adk-python/README.md b/packages/e2b-adk-python/README.md new file mode 100644 index 0000000..372fcbf --- /dev/null +++ b/packages/e2b-adk-python/README.md @@ -0,0 +1,151 @@ +# e2b-adk + +A [Google ADK](https://google.github.io/adk-docs/) plugin backed by +[E2B](https://e2b.dev) sandboxes. Attach it to an ADK agent and the agent can +generate and run code, execute shell commands, read and write files, and start +long-running processes — all inside an isolated E2B sandbox, with no +infrastructure of your own. + +Under the hood every tool call runs in the +[E2B Code Interpreter](https://e2b.dev/docs), so the model's code executes in a +real Python kernel instead of being trusted blind. + +## Install + +```bash +pip install e2b-adk +``` + +Or with [uv](https://docs.astral.sh/uv/): + +```bash +uv pip install e2b-adk +``` + +## Credentials + +You need an E2B API key for the sandbox and a model key for the agent's LLM +(Gemini is ADK's default): + +```bash +export E2B_API_KEY="..." +export GOOGLE_API_KEY="..." +``` + +Get an E2B key at [e2b.dev/dashboard](https://e2b.dev/dashboard). + +## Quickstart + +Create the plugin, hand its tools to an `Agent`, and register the plugin on the +`App`. The plugin owns the sandbox lifecycle — you never create or tear one down +yourself. + +```python +import asyncio + +from google.adk.agents import Agent +from google.adk.apps import App +from google.adk.runners import InMemoryRunner + +from e2b_adk import E2BPlugin + + +async def main() -> None: + plugin = E2BPlugin() + agent = Agent( + model="gemini-2.5-flash", + name="coder", + instruction="Write and run Python to answer the user. Verify with run_code.", + tools=plugin.get_tools(), + ) + app = App(name="demo", root_agent=agent, plugins=[plugin]) + + async with InMemoryRunner(app=app) as runner: + # run_debug prints the conversation as it runs. + await runner.run_debug("Compute the 20th Fibonacci number.") + + +asyncio.run(main()) +``` + +## Tools + +`plugin.get_tools()` returns six tools. Each returns a JSON-serializable dict +with a `success` flag — tools report failures in the result rather than raising, +so a bad call never aborts the agent run. `success` means the tool *ran*: code +that raised or a command that exited non-zero still returns `success: True` with +the failure captured in `error` / `exit_code`. Only a call that could not run at +all (sandbox unavailable, timeout) returns `success: False`. + +| Tool | Does | Key result fields | +|------|------|-------------------| +| `run_code` | Run code in a stateful kernel (variables persist across calls) | `stdout`, `stderr`, `text`, `error` | +| `run_command` | Run a shell command (each call is independent) | `stdout`, `stderr`, `exit_code` | +| `write_file` | Write a file in the sandbox | `path` | +| `read_file` | Read a file back | `path`, `content` | +| `list_files` | List a directory | `entries` (`[{name, type}]`) | +| `start_background_command` | Start a long-running process; optional preview URL for a port | `pid`, `preview_url` (+ `readiness` when `port` is set) | + +### Configuration + +`E2BPlugin` takes keyword-only options, all optional. Anything you don't set +falls back to the E2B SDK's own default — the plugin overrides none of them. + +```python +E2BPlugin( + # Common options + api_key=None, # defaults to the E2B_API_KEY env var + template=None, # E2B sandbox template + metadata=None, # dict[str, str] attached to the sandbox + envs=None, # dict[str, str] environment variables + timeout=None, # sandbox timeout in seconds (re-applied on every tool call) + plugin_name="e2b_plugin", + # Passed straight through to AsyncSandbox.create() — see the E2B SDK docs + secure=None, + allow_internet_access=None, + mcp=None, + network=None, + lifecycle=None, # e.g. pause/auto-resume; defaults to E2B's behavior + volume_mounts=None, + # **opts: any other AsyncSandbox.create() / connection param (proxy, request_timeout, …) +) +``` + +## Examples + +- [`examples/data_analysis.py`](examples/data_analysis.py) — a data-analysis + agent that saves a dataset and computes real answers with pandas instead of + guessing. +- [`examples/code_generator.py`](examples/code_generator.py) — a code generator + that executes every snippet before returning it. + +Run one with the credentials above: + +```bash +uv run --extra examples python examples/data_analysis.py +``` + +## v1 notes + +- **Sequential tool calls.** One sandbox is shared per plugin; calls are meant + to run one at a time, not concurrently. Concurrent calls are safe for the + sandbox lifecycle (creation is locked) but share one filesystem and kernel. +- **Text-only output.** Tools return text — `stdout`/`stderr`/file contents. + Rich results (charts, dataframes, images) are not surfaced yet. +- **Lazy, shared sandbox lifecycle.** The sandbox is created on the first tool + call and reused for the agent's lifetime, then killed when the runner exits. +- **Keep-alive while active, expiry when idle.** While a sandbox SDK operation + is active, a best-effort heartbeat refreshes its `timeout` (E2B's default is + 300s), including when a foreground call runs longer than that timeout. A + final refresh starts the full idle window when the operation ends. An idle + gap longer than `timeout` still expires the sandbox under E2B's default + lifecycle (`on_timeout: kill`), and later tool calls return failure results — + pass e.g. + `lifecycle={"on_timeout": {"action": "pause"}, "auto_resume": True}` to pause + and auto-resume across idle gaps instead. +- **Bounded output.** Output larger than 10 KB per field is truncated with a + `…[truncated N bytes]` marker so a single call can't flood the model's context. + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/e2b_adk/__init__.py b/packages/e2b-adk-python/e2b_adk/__init__.py similarity index 100% rename from e2b_adk/__init__.py rename to packages/e2b-adk-python/e2b_adk/__init__.py diff --git a/e2b_adk/_results.py b/packages/e2b-adk-python/e2b_adk/_results.py similarity index 100% rename from e2b_adk/_results.py rename to packages/e2b-adk-python/e2b_adk/_results.py diff --git a/e2b_adk/_sandbox.py b/packages/e2b-adk-python/e2b_adk/_sandbox.py similarity index 100% rename from e2b_adk/_sandbox.py rename to packages/e2b-adk-python/e2b_adk/_sandbox.py diff --git a/e2b_adk/plugin.py b/packages/e2b-adk-python/e2b_adk/plugin.py similarity index 100% rename from e2b_adk/plugin.py rename to packages/e2b-adk-python/e2b_adk/plugin.py diff --git a/e2b_adk/py.typed b/packages/e2b-adk-python/e2b_adk/py.typed similarity index 100% rename from e2b_adk/py.typed rename to packages/e2b-adk-python/e2b_adk/py.typed diff --git a/e2b_adk/tools/__init__.py b/packages/e2b-adk-python/e2b_adk/tools/__init__.py similarity index 100% rename from e2b_adk/tools/__init__.py rename to packages/e2b-adk-python/e2b_adk/tools/__init__.py diff --git a/e2b_adk/tools/_common.py b/packages/e2b-adk-python/e2b_adk/tools/_common.py similarity index 100% rename from e2b_adk/tools/_common.py rename to packages/e2b-adk-python/e2b_adk/tools/_common.py diff --git a/e2b_adk/tools/list_files.py b/packages/e2b-adk-python/e2b_adk/tools/list_files.py similarity index 100% rename from e2b_adk/tools/list_files.py rename to packages/e2b-adk-python/e2b_adk/tools/list_files.py diff --git a/e2b_adk/tools/read_file.py b/packages/e2b-adk-python/e2b_adk/tools/read_file.py similarity index 100% rename from e2b_adk/tools/read_file.py rename to packages/e2b-adk-python/e2b_adk/tools/read_file.py diff --git a/e2b_adk/tools/run_code.py b/packages/e2b-adk-python/e2b_adk/tools/run_code.py similarity index 100% rename from e2b_adk/tools/run_code.py rename to packages/e2b-adk-python/e2b_adk/tools/run_code.py diff --git a/e2b_adk/tools/run_command.py b/packages/e2b-adk-python/e2b_adk/tools/run_command.py similarity index 100% rename from e2b_adk/tools/run_command.py rename to packages/e2b-adk-python/e2b_adk/tools/run_command.py diff --git a/e2b_adk/tools/start_background_command.py b/packages/e2b-adk-python/e2b_adk/tools/start_background_command.py similarity index 100% rename from e2b_adk/tools/start_background_command.py rename to packages/e2b-adk-python/e2b_adk/tools/start_background_command.py diff --git a/e2b_adk/tools/write_file.py b/packages/e2b-adk-python/e2b_adk/tools/write_file.py similarity index 100% rename from e2b_adk/tools/write_file.py rename to packages/e2b-adk-python/e2b_adk/tools/write_file.py diff --git a/examples/code_generator.py b/packages/e2b-adk-python/examples/code_generator.py similarity index 100% rename from examples/code_generator.py rename to packages/e2b-adk-python/examples/code_generator.py diff --git a/examples/data_analysis.py b/packages/e2b-adk-python/examples/data_analysis.py similarity index 100% rename from examples/data_analysis.py rename to packages/e2b-adk-python/examples/data_analysis.py diff --git a/packages/e2b-adk-python/package.json b/packages/e2b-adk-python/package.json new file mode 100644 index 0000000..a8359c1 --- /dev/null +++ b/packages/e2b-adk-python/package.json @@ -0,0 +1,15 @@ +{ + "name": "@e2b/adk-python", + "private": true, + "version": "0.1.1", + "scripts": { + "test": "uv run --frozen --extra dev pytest", + "lint": "uv run --frozen --extra dev ruff check e2b_adk tests examples", + "typecheck": "uv run --frozen --extra dev mypy e2b_adk", + "build": "rm -rf dist && uv build", + "check": "pnpm lint && pnpm typecheck && pnpm test && pnpm build", + "postVersion": "uv version --frozen $(pnpm pkg get version | tr -d '\"')", + "lock": "uv lock", + "release": "rm -rf dist && uv build && uv publish --trusted-publishing always --check-url https://pypi.org/simple/" + } +} diff --git a/pyproject.toml b/packages/e2b-adk-python/pyproject.toml similarity index 100% rename from pyproject.toml rename to packages/e2b-adk-python/pyproject.toml diff --git a/tests/__init__.py b/packages/e2b-adk-python/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to packages/e2b-adk-python/tests/__init__.py diff --git a/tests/conftest.py b/packages/e2b-adk-python/tests/conftest.py similarity index 100% rename from tests/conftest.py rename to packages/e2b-adk-python/tests/conftest.py diff --git a/tests/test_live_smoke.py b/packages/e2b-adk-python/tests/test_live_smoke.py similarity index 100% rename from tests/test_live_smoke.py rename to packages/e2b-adk-python/tests/test_live_smoke.py diff --git a/tests/test_plugin.py b/packages/e2b-adk-python/tests/test_plugin.py similarity index 100% rename from tests/test_plugin.py rename to packages/e2b-adk-python/tests/test_plugin.py diff --git a/tests/test_results.py b/packages/e2b-adk-python/tests/test_results.py similarity index 100% rename from tests/test_results.py rename to packages/e2b-adk-python/tests/test_results.py diff --git a/tests/test_sandbox.py b/packages/e2b-adk-python/tests/test_sandbox.py similarity index 100% rename from tests/test_sandbox.py rename to packages/e2b-adk-python/tests/test_sandbox.py diff --git a/tests/test_tools.py b/packages/e2b-adk-python/tests/test_tools.py similarity index 100% rename from tests/test_tools.py rename to packages/e2b-adk-python/tests/test_tools.py diff --git a/uv.lock b/packages/e2b-adk-python/uv.lock similarity index 100% rename from uv.lock rename to packages/e2b-adk-python/uv.lock diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..57a189a --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4198 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: 2.31.1 + version: 2.31.1(@types/node@22.20.4) + + packages/e2b-adk-js: + dependencies: + '@e2b/code-interpreter': + specifier: ^2.8.0 + version: 2.8.0 + zod: + specifier: ^4.2.1 + version: 4.6.5 + devDependencies: + '@eslint/js': + specifier: ^9.0.0 + version: 9.39.5 + '@google/adk': + specifier: 2.1.0 + version: 2.1.0(@bufbuild/protobuf@2.15.0)(@grpc/grpc-js@1.14.5)(supports-color@7.2.0) + '@types/node': + specifier: ^22.0.0 + version: 22.20.4 + eslint: + specifier: ^9.0.0 + version: 9.39.5(supports-color@7.2.0) + prettier: + specifier: ^3.0.0 + version: 3.9.8 + tsx: + specifier: ^4.20.0 + version: 4.23.15 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.0.0 + version: 8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/node@22.20.4)(supports-color@7.2.0)(tsx@4.23.15) + + packages/e2b-adk-python: {} + +packages: + + '@a2a-js/sdk@0.3.14': + resolution: {integrity: sha512-F6Ew1AtPzCLhTn8h9yiqTe7DiDf6XVrSnq9V1YqSl9eWqPm6anMveTiKdCSb/76cW0YiJc24rNaUrVezFFHbqQ==} + engines: {node: '>=18'} + peerDependencies: + '@bufbuild/protobuf': ^2.10.2 + '@grpc/grpc-js': ^1.11.0 + express: ^4.21.2 || ^5.1.0 + peerDependenciesMeta: + '@bufbuild/protobuf': + optional: true + '@grpc/grpc-js': + optional: true + express: + optional: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@balena/dockerignore@1.0.2': + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + + '@bufbuild/protobuf@2.15.0': + resolution: {integrity: sha512-DAheWUkVr/SJTWCc+lg9dhY0eN4SaWlf4+bG1KzHeXbnqt0AfB/NX0Z+VunGlM1ki1B4zVvye27MpKh/svySUA==} + + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.31.1': + resolution: {integrity: sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==} + hasBin: true + + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} + + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@colors/colors@1.6.1': + resolution: {integrity: sha512-dTmUJzXSuayBK+hZydEaXd2mhx61qWQwkwaBBY6LyEOVx/L9aQU5ac8eFNEsd9nrD1+zb9zvDCphLSe8g1F4Qw==} + engines: {node: '>=0.1.90'} + + '@connectrpc/connect-web@2.2.0': + resolution: {integrity: sha512-uuBUcBTYfb/r1o98LzCCZk1H90yx0vmZ5m21dpw/q4EUwsKwkCDGI8H0aEdjcZld+Q2jJ5A5ADvPqfb6lzQ/sg==} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + '@connectrpc/connect': 2.2.0 + + '@connectrpc/connect@2.2.0': + resolution: {integrity: sha512-M63OQ1ZFcPUm85CuewJ9G9CMV8LjzGR9935xOnXNQGSazAbwMb3x2W7vXBYM1zqrP7XFD8uxp/dUAhirclgwnQ==} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + + '@dabh/diagnostics@2.0.9': + resolution: {integrity: sha512-R6siwR65Hm+3yfgP7o8DKhNvputQAwfoz9zTc3kyDudnomj2/BcLmD+uGQQPICjuFUp8ounPBU+jmKsocwVVAg==} + + '@e2b/code-interpreter@2.8.0': + resolution: {integrity: sha512-tWFi8IJYV3V6jOVfQ8V5PkzockrYP+ndWB/jKiJvRATTmNAy8+S/7S+iSh2Lb2Hrul869G2adRiN2xW8peJTig==} + engines: {node: '>=20.18.1 <21 || >=22'} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.7': + resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@google-cloud/vertexai@1.12.0': + resolution: {integrity: sha512-XMJIk7GIeavFLP5A3YEUlowKa5Y5PZRrnnuTJcqR0k+lFKkv7+IWpdRp+Xbqb8xNDrvQaE2hP2RYPUylyD5EdA==} + engines: {node: '>=18.0.0'} + + '@google/adk@2.1.0': + resolution: {integrity: sha512-+dMsMonJpkOS7iYhOIyUI5RTQtn6Q/Y5GM5zu00uEwKNcOPuv8ZeLexvrMkltwV10bz/BDlIKXOXavM4UHPdVw==} + peerDependencies: + '@google-cloud/opentelemetry-cloud-monitoring-exporter': ^0.21.0 + '@google-cloud/opentelemetry-cloud-trace-exporter': ^3.0.0 + '@google-cloud/storage': ^7.17.1 + '@mikro-orm/mariadb': ^7.2.0 + '@mikro-orm/mssql': ^7.2.0 + '@mikro-orm/mysql': ^7.2.0 + '@mikro-orm/postgresql': ^7.2.0 + '@mikro-orm/sqlite': ^7.2.0 + '@modelcontextprotocol/sdk': ^1.26.0 + express: ^4.22.1 || ^5.1.0 + peerDependenciesMeta: + '@google-cloud/opentelemetry-cloud-monitoring-exporter': + optional: true + '@google-cloud/opentelemetry-cloud-trace-exporter': + optional: true + '@google-cloud/storage': + optional: true + '@mikro-orm/mariadb': + optional: true + '@mikro-orm/mssql': + optional: true + '@mikro-orm/mysql': + optional: true + '@mikro-orm/postgresql': + optional: true + '@mikro-orm/sqlite': + optional: true + '@modelcontextprotocol/sdk': + optional: true + express: + optional: true + + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@google/genai@2.23.0': + resolution: {integrity: sha512-NFMzktoZ7jJg9AA3vUbWZocnkQP6DSGzL21ORwdVcBWkHli3k7v6B9zy07M4bTiKDAyIWfUh2J/oLYxCu0hNpQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@grpc/grpc-js@1.14.5': + resolution: {integrity: sha512-7VZM+SVdEcUUqSQeNI3zM8Qs/BhQKZndPo2h5VkYkAM8Iz0wJIa8mKV5ekQGqG8UUsnkQ0NMxIxwkIHYvj0qOw==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@mikro-orm/core@7.2.1': + resolution: {integrity: sha512-+gtJoScbin/C3wPItSZkj5vlMTq/LphBLIxHyi5brCbeA6VDcYuoMopiqoFUMQxdhXXrZlKNIHXGrhb8r3biVg==} + engines: {node: '>= 22.17.0'} + peerDependencies: + dataloader: 2.2.3 + peerDependenciesMeta: + dataloader: + optional: true + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@opentelemetry/api-logs@0.222.0': + resolution: {integrity: sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@2.11.0': + resolution: {integrity: sha512-Tr79DyWI8itsBdg+jH+opjfrwLzX+erk1/ExkIwhWoAVjVrJIn2y5+cGjTC0Vy8fyNIA/y8wuJPZwr1T3xCZeQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.11.0': + resolution: {integrity: sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-http@0.222.0': + resolution: {integrity: sha512-GqwLZohJTf/sttYlOpT1oFTJ9ScYaKnk0sOx4Qe/+KmXYVOa+v94cfnkUkFKDuvTODWhCL50Hz3gvsZD6ngQpg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.222.0': + resolution: {integrity: sha512-6Ko+0bgNP6V4G/RsmNBSk5lO1B/lo9z6tvLpFY3ykPPevXNzFmOW3C9hEmTt0zKCvHovyrol3TYlg4HVb5gwjg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.222.0': + resolution: {integrity: sha512-RCnPWcHppwiquQ+cV3nWvNwdf0MG1w26e5jewW2T83nTZOlXgcg88sY9ulCVgagGHcq1mj0L0GP6YHgzk2v8oA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.222.0': + resolution: {integrity: sha512-YbywG3veEm2Fb6TbdxRkuquWob6eVWXuA8/Ba1tXz9jHfUqpdE3keilOHEtPboC4CvS1bjeeVfNkWGOOrLj+lw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.222.0': + resolution: {integrity: sha512-/F3BZ89+CJQnZkMh2tCrtcdB+XT2Dxhj4FFE+WPQ//413hmFL0/RfEX6vgOIWGhiSzrkHWTK3+6SiT7K5/g/jQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resource-detector-gcp@0.40.3': + resolution: {integrity: sha512-C796YjBA5P1JQldovApYfFA/8bQwFfpxjUbOtGhn1YZkVTLoNQN+kvBwgALfTPWzug6fWsd0xhn9dzeiUcndag==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resources@2.11.0': + resolution: {integrity: sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.222.0': + resolution: {integrity: sha512-+19YHODIjaUCArxleaJtuufFZVpz/xvvK+VllQqE+W8hHolxdoRwHfK/s667zezwh1hkx6FFF+oYzetYgqK+Bg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.11.0': + resolution: {integrity: sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.11.0': + resolution: {integrity: sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.11.0': + resolution: {integrity: sha512-CuvCMJmZxswhNLlM2LfuLOW3h3fZujA4hsG4B+Sz4dX2zvaXO8Ng74cnDHWD64gLszTlhiG3c0iNUjj4g+0/sA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.11.0': + resolution: {integrity: sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + + '@rollup/rollup-android-arm-eabi@4.63.4': + resolution: {integrity: sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.4': + resolution: {integrity: sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.4': + resolution: {integrity: sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.4': + resolution: {integrity: sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.4': + resolution: {integrity: sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.4': + resolution: {integrity: sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + resolution: {integrity: sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + resolution: {integrity: sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + resolution: {integrity: sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.4': + resolution: {integrity: sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + resolution: {integrity: sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.4': + resolution: {integrity: sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + resolution: {integrity: sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + resolution: {integrity: sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + resolution: {integrity: sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + resolution: {integrity: sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + resolution: {integrity: sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.4': + resolution: {integrity: sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.4': + resolution: {integrity: sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.4': + resolution: {integrity: sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.4': + resolution: {integrity: sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + resolution: {integrity: sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + resolution: {integrity: sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.4': + resolution: {integrity: sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.4': + resolution: {integrity: sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==} + cpu: [x64] + os: [win32] + + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@4.0.1': + resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/node@22.20.4': + resolution: {integrity: sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/ssh2@1.15.6': + resolution: {integrity: sha512-oGdxhBqcRTwSTKFm+9EiKzkNVYRLEFkcW44lhguvBalGJbWfGnDt/ezwSUZc+SF9m9bMc3VyklNAtp7zICjS5w==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@typescript-eslint/eslint-plugin@8.70.0': + resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.70.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.70.0': + resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.70.0': + resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.70.0': + resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.70.0': + resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.70.0': + resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.70.0': + resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.70.0': + resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.70.0': + resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.70.0': + resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + adm-zip@0.5.18: + resolution: {integrity: sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==} + engines: {node: '>=12.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-name@2.1.1: + resolution: {integrity: sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + docker-modem@5.0.7: + resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} + engines: {node: '>= 8.0'} + + dockerfile-ast@0.7.1: + resolution: {integrity: sha512-oX/A4I0EhSkGqrFv0YuvPkBUSYp1XiY8O8zAKc8Djglx8ocz+JfOr8gP0ryRMC2myqvDLagmnZaU9ot1vG2ijw==} + + dockerode@5.0.1: + resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==} + engines: {node: '>= 14.17'} + + e2b@2.51.0: + resolution: {integrity: sha512-P+50lx1nOYpgCFyE1xuz6JVN/TqJivMUiH7v+RW0CUSfb4+NCu8IkC5KruT2sNPgp6aexddQQ77+OZKGpZZJ+Q==} + engines: {node: '>=20.18.1 <21 || >=22'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gaxios@7.3.1: + resolution: {integrity: sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==} + engines: {node: '>=18'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + google-auth-library@10.9.1: + resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} + engines: {node: '>=18'} + + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-id@4.2.2: + resolution: {integrity: sha512-Z+8unD6em/MJwyrkWQ2M3wygRGI1+MztbutiNwk1DCuEPXOZRWGjtcMPZYP1exY9iNZMhd7ZOSMQSo9WYtiQRw==} + hasBin: true + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.9: + resolution: {integrity: sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@3.15.2: + resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} + hasBin: true + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonpath-plus@10.4.0: + resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} + engines: {node: '>=18.0.0'} + hasBin: true + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nan@2.29.0: + resolution: {integrity: sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + 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==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.9.8: + resolution: {integrity: sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==} + engines: {node: '>=14'} + hasBin: true + + protobufjs@7.6.6: + resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} + engines: {node: '>=12.0.0'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.63.4: + resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsx@4.23.15: + resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} + engines: {node: '>=18.0.0'} + hasBin: true + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.70.0: + resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.29.1: + resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} + engines: {node: '>=20.18.1'} + + undici@8.10.2: + resolution: {integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==} + engines: {node: '>=22.19.0'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-languageserver-textdocument@1.0.14: + resolution: {integrity: sha512-EQyqJMi552E4ZTf46izQ4Fj6XquqxCySR3J5ZSD1SisMf6RfpeOWHxGBE8Gr6V0/3GHIGdAzDn8F8+1nTGCnoQ==} + + vscode-languageserver-types@3.18.3: + resolution: {integrity: sha512-XIlzJ7Qp/jzSI1ds7/FwPAWrPeTZA7pAtlW4hdJ1J6xXWJL6dR9QYnDhJOdLzdKhUQ5Mm6mvUMw+3DcOQQasPw==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + +snapshots: + + '@a2a-js/sdk@0.3.14(@bufbuild/protobuf@2.15.0)(@grpc/grpc-js@1.14.5)': + dependencies: + uuid: 11.1.1 + optionalDependencies: + '@bufbuild/protobuf': 2.15.0 + '@grpc/grpc-js': 1.14.5 + + '@babel/runtime@7.29.7': {} + + '@balena/dockerignore@1.0.2': + optional: true + + '@bufbuild/protobuf@2.15.0': {} + + '@changesets/apply-release-plan@7.1.1': + dependencies: + '@changesets/config': 3.1.4 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.8.5 + + '@changesets/assemble-release-plan@6.0.10': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.8.5 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.31.1(@types/node@22.20.4)': + dependencies: + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.4 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3(@types/node@22.20.4) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.8.5 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.4': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.4': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.8.5 + + '@changesets/get-release-plan@4.0.16': + dependencies: + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.3.2 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.2.2 + prettier: 2.8.8 + + '@colors/colors@1.6.0': {} + + '@colors/colors@1.6.1': {} + + '@connectrpc/connect-web@2.2.0(@bufbuild/protobuf@2.15.0)(@connectrpc/connect@2.2.0(@bufbuild/protobuf@2.15.0))': + dependencies: + '@bufbuild/protobuf': 2.15.0 + '@connectrpc/connect': 2.2.0(@bufbuild/protobuf@2.15.0) + + '@connectrpc/connect@2.2.0(@bufbuild/protobuf@2.15.0)': + dependencies: + '@bufbuild/protobuf': 2.15.0 + + '@dabh/diagnostics@2.0.9': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + + '@e2b/code-interpreter@2.8.0': + dependencies: + e2b: 2.51.0 + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(supports-color@7.2.0))': + dependencies: + eslint: 9.39.5(supports-color@7.2.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2(supports-color@7.2.0)': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.7(supports-color@7.2.0)': + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@7.2.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@google-cloud/vertexai@1.12.0(supports-color@7.2.0)': + dependencies: + '@google/genai': 1.52.0(supports-color@7.2.0) + google-auth-library: 9.15.1(supports-color@7.2.0) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - encoding + - supports-color + - utf-8-validate + + '@google/adk@2.1.0(@bufbuild/protobuf@2.15.0)(@grpc/grpc-js@1.14.5)(supports-color@7.2.0)': + dependencies: + '@a2a-js/sdk': 0.3.14(@bufbuild/protobuf@2.15.0)(@grpc/grpc-js@1.14.5) + '@google-cloud/vertexai': 1.12.0(supports-color@7.2.0) + '@google/genai': 2.23.0(supports-color@7.2.0) + '@mikro-orm/core': 7.2.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.222.0 + '@opentelemetry/exporter-logs-otlp-http': 0.222.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.222.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.222.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-gcp': 0.40.3(@opentelemetry/api@1.9.0)(supports-color@7.2.0) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.222.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.11.0(@opentelemetry/api@1.9.0) + '@types/dockerode': 4.0.1 + adm-zip: 0.5.18 + google-auth-library: 10.9.1(supports-color@7.2.0) + js-yaml: 4.3.2 + jsonpath-plus: 10.4.0 + lodash-es: 4.18.1 + winston: 3.19.0 + zod: 4.6.5 + zod-to-json-schema: 3.25.2(zod@4.6.5) + optionalDependencies: + dockerode: 5.0.1(supports-color@7.2.0) + transitivePeerDependencies: + - '@bufbuild/protobuf' + - '@grpc/grpc-js' + - bufferutil + - dataloader + - encoding + - supports-color + - utf-8-validate + + '@google/genai@1.52.0(supports-color@7.2.0)': + dependencies: + google-auth-library: 10.9.1(supports-color@7.2.0) + p-retry: 4.6.2 + protobufjs: 7.6.6 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@google/genai@2.23.0(supports-color@7.2.0)': + dependencies: + google-auth-library: 10.9.1(supports-color@7.2.0) + p-retry: 4.6.2 + protobufjs: 7.6.6 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@grpc/grpc-js@1.14.5': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + optional: true + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.6 + yargs: 17.7.3 + optional: true + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.6 + yargs: 17.7.3 + optional: true + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/external-editor@1.0.3(@types/node@22.20.4)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 22.20.4 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@js-sdsl/ordered-map@4.4.2': + optional: true + + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.7 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.7 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@mikro-orm/core@7.2.1': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 + + '@opentelemetry/api-logs@0.222.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/context-async-hooks@2.11.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/exporter-logs-otlp-http@0.222.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/otlp-exporter-base': 0.222.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.222.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.222.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.222.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.222.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.222.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.11.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.222.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/otlp-exporter-base': 0.222.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.222.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace': 2.11.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.222.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.222.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.222.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.222.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.222.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace': 2.11.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/resource-detector-gcp@0.40.3(@opentelemetry/api@1.9.0)(supports-color@7.2.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.0) + gcp-metadata: 6.1.1(supports-color@7.2.0) + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.222.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace-node@2.11.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace@2.11.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/semantic-conventions@1.43.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + + '@rollup/rollup-android-arm-eabi@4.63.4': + optional: true + + '@rollup/rollup-android-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-x64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.4': + optional: true + + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/docker-modem@3.0.6': + dependencies: + '@types/node': 22.20.4 + '@types/ssh2': 1.15.6 + + '@types/dockerode@4.0.1': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 22.20.4 + '@types/ssh2': 1.15.6 + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@12.20.55': {} + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/node@22.20.4': + dependencies: + undici-types: 6.21.0 + + '@types/retry@0.12.0': {} + + '@types/ssh2@1.15.6': + dependencies: + '@types/node': 18.19.130 + + '@types/triple-beam@1.3.5': {} + + '@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/type-utils': 8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.70.0 + eslint: 9.39.5(supports-color@7.2.0) + ignore: 7.0.9 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.70.0 + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.70.0(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@5.9.3) + '@typescript-eslint/types': 8.70.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.70.0': + dependencies: + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 + + '@typescript-eslint/tsconfig-utils@8.70.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 9.39.5(supports-color@7.2.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.70.0': {} + + '@typescript-eslint/typescript-estree@8.70.0(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.70.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@5.9.3) + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.70.0': + dependencies: + '@typescript-eslint/types': 8.70.0 + eslint-visitor-keys: 5.0.1 + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.20.4)(tsx@4.23.15))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.20.4)(tsx@4.23.15) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.6 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + adm-zip@0.5.18: {} + + agent-base@7.1.4: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + optional: true + + assertion-error@2.0.1: {} + + async@3.2.6: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + optional: true + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + bignumber.js@9.3.1: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + brace-expansion@1.1.21: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-equal-constant-time@1.0.1: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + optional: true + + buildcheck@0.0.7: + optional: true + + cac@6.7.14: {} + + callsites@3.1.0: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + chardet@2.2.0: {} + + check-error@2.1.3: {} + + chownr@1.1.4: + optional: true + + chownr@3.0.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + optional: true + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-convert@3.1.3: + dependencies: + color-name: 2.1.1 + + color-name@1.1.4: {} + + color-name@2.1.1: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.1 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + + compare-versions@6.1.1: {} + + concat-map@0.0.1: {} + + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.29.0 + optional: true + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-uri-to-buffer@4.0.1: {} + + debug@4.4.3(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + detect-indent@6.1.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + docker-modem@5.0.7(supports-color@7.2.0): + dependencies: + debug: 4.4.3(supports-color@7.2.0) + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color + optional: true + + dockerfile-ast@0.7.1: + dependencies: + vscode-languageserver-textdocument: 1.0.14 + vscode-languageserver-types: 3.18.3 + + dockerode@5.0.1(supports-color@7.2.0): + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.14.5 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.7(supports-color@7.2.0) + protobufjs: 7.6.6 + tar-fs: 2.1.5 + transitivePeerDependencies: + - supports-color + optional: true + + e2b@2.51.0: + dependencies: + '@bufbuild/protobuf': 2.15.0 + '@connectrpc/connect': 2.2.0(@bufbuild/protobuf@2.15.0) + '@connectrpc/connect-web': 2.2.0(@bufbuild/protobuf@2.15.0)(@connectrpc/connect@2.2.0(@bufbuild/protobuf@2.15.0)) + chalk: 5.6.2 + compare-versions: 6.1.1 + dockerfile-ast: 0.7.1 + glob: 13.0.6 + openapi-fetch: 0.14.1 + platform: 1.3.6 + tar: 7.5.22 + undici: 7.29.1 + optionalDependencies: + undici8: undici@8.10.2 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + emoji-regex@8.0.0: + optional: true + + enabled@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + optional: true + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + es-module-lexer@1.7.0: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: + optional: true + + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5(supports-color@7.2.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(supports-color@7.2.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2(supports-color@7.2.0) + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.7(supports-color@7.2.0) + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@7.2.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + extend@3.0.2: {} + + extendable-error@0.1.7: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fecha@4.2.3: {} + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + + fn.name@1.1.0: {} + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + fs-constants@1.0.0: + optional: true + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fsevents@2.3.3: + optional: true + + gaxios@6.7.1(supports-color@7.2.0): + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6(supports-color@7.2.0) + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gaxios@7.3.1(supports-color@7.2.0): + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6(supports-color@7.2.0) + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@6.1.1(supports-color@7.2.0): + dependencies: + gaxios: 6.7.1(supports-color@7.2.0) + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@8.1.2(supports-color@7.2.0): + dependencies: + gaxios: 7.3.1(supports-color@7.2.0) + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + get-caller-file@2.0.5: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@13.0.6: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + + globals@14.0.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + google-auth-library@10.9.1(supports-color@7.2.0): + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.3.1(supports-color@7.2.0) + gcp-metadata: 8.1.2(supports-color@7.2.0) + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-auth-library@9.15.1(supports-color@7.2.0): + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1(supports-color@7.2.0) + gcp-metadata: 6.1.1(supports-color@7.2.0) + gtoken: 7.1.0(supports-color@7.2.0) + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + + google-logging-utils@1.1.3: {} + + graceful-fs@4.2.11: {} + + gtoken@7.1.0(supports-color@7.2.0): + dependencies: + gaxios: 6.7.1(supports-color@7.2.0) + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + has-flag@4.0.0: {} + + https-proxy-agent@7.0.6(supports-color@7.2.0): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@7.2.0) + transitivePeerDependencies: + - supports-color + + human-id@4.2.2: {} + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: + optional: true + + ignore@5.3.2: {} + + ignore@7.0.9: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-stream@2.0.1: {} + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@3.15.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsep@1.4.0: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonpath-plus@10.4.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kuler@2.0.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.18.1: {} + + lodash.camelcase@4.3.0: + optional: true + + lodash.merge@4.6.2: {} + + lodash.startcase@4.4.0: {} + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + long@5.3.2: {} + + loupe@3.2.1: {} + + lru-cache@11.5.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.21 + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + mkdirp-classic@0.5.3: + optional: true + + mri@1.2.0: {} + + ms@2.1.3: {} + + nan@2.29.0: + optional: true + + nanoid@3.3.19: {} + + natural-compare@1.4.0: {} + + node-domexception@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optional: true + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + openapi-fetch@0.14.1: + dependencies: + openapi-typescript-helpers: 0.0.15 + + openapi-typescript-helpers@0.0.15: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@2.1.0: {} + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.3 + minipass: 7.1.3 + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + pify@4.0.1: {} + + platform@1.3.6: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@2.8.8: {} + + prettier@3.9.8: {} + + protobufjs@7.6.6: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 22.20.4 + long: 5.3.2 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + optional: true + + punycode@2.3.1: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.15.2 + pify: 4.0.1 + strip-bom: 3.0.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + require-directory@2.1.1: + optional: true + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + retry@0.13.1: {} + + reusify@1.1.0: {} + + rollup@4.63.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.4 + '@rollup/rollup-android-arm64': 4.63.4 + '@rollup/rollup-darwin-arm64': 4.63.4 + '@rollup/rollup-darwin-x64': 4.63.4 + '@rollup/rollup-freebsd-arm64': 4.63.4 + '@rollup/rollup-freebsd-x64': 4.63.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.4 + '@rollup/rollup-linux-arm-musleabihf': 4.63.4 + '@rollup/rollup-linux-arm64-gnu': 4.63.4 + '@rollup/rollup-linux-arm64-musl': 4.63.4 + '@rollup/rollup-linux-loong64-gnu': 4.63.4 + '@rollup/rollup-linux-loong64-musl': 4.63.4 + '@rollup/rollup-linux-ppc64-gnu': 4.63.4 + '@rollup/rollup-linux-ppc64-musl': 4.63.4 + '@rollup/rollup-linux-riscv64-gnu': 4.63.4 + '@rollup/rollup-linux-riscv64-musl': 4.63.4 + '@rollup/rollup-linux-s390x-gnu': 4.63.4 + '@rollup/rollup-linux-x64-gnu': 4.63.4 + '@rollup/rollup-linux-x64-musl': 4.63.4 + '@rollup/rollup-openbsd-x64': 4.63.4 + '@rollup/rollup-openharmony-arm64': 4.63.4 + '@rollup/rollup-win32-arm64-msvc': 4.63.4 + '@rollup/rollup-win32-ia32-msvc': 4.63.4 + '@rollup/rollup-win32-x64-gnu': 4.63.4 + '@rollup/rollup-win32-x64-msvc': 4.63.4 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + split-ca@1.0.1: + optional: true + + sprintf-js@1.0.3: {} + + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.29.0 + optional: true + + stack-trace@0.0.10: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + optional: true + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + optional: true + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + term-size@2.2.1: {} + + text-hex@1.0.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.6: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + triple-beam@1.4.1: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsx@4.23.15: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + tweetnacl@0.14.5: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.70.0(eslint@9.39.5(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) + eslint: 9.39.5(supports-color@7.2.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@5.26.5: {} + + undici-types@6.21.0: {} + + undici@7.29.1: {} + + undici@8.10.2: + optional: true + + universalify@0.1.2: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + uuid@11.1.1: {} + + uuid@9.0.1: {} + + vite-node@3.2.4(@types/node@22.20.4)(supports-color@7.2.0)(tsx@4.23.15): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@7.2.0) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@22.20.4)(tsx@4.23.15) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@22.20.4)(tsx@4.23.15): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.28 + rollup: 4.63.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.4 + fsevents: 2.3.3 + tsx: 4.23.15 + + vitest@3.2.7(@types/node@22.20.4)(supports-color@7.2.0)(tsx@4.23.15): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.20.4)(tsx@4.23.15)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3(supports-color@7.2.0) + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@22.20.4)(tsx@4.23.15) + vite-node: 3.2.4(@types/node@22.20.4)(supports-color@7.2.0)(tsx@4.23.15) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.4 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vscode-languageserver-textdocument@1.0.14: {} + + vscode-languageserver-types@3.18.3: {} + + web-streams-polyfill@3.3.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.1 + '@dabh/diagnostics': 2.0.9 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + optional: true + + wrappy@1.0.2: + optional: true + + ws@8.21.3: {} + + y18n@5.0.8: + optional: true + + yallist@5.0.0: {} + + yargs-parser@21.1.1: + optional: true + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + optional: true + + yocto-queue@0.1.0: {} + + zod-to-json-schema@3.25.2(zod@4.6.5): + dependencies: + zod: 4.6.5 + + zod@4.6.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..fc3a859 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,13 @@ +packages: + - packages/* + +# Supply-chain guard: skip package versions published in the last 7 days (in minutes). +minimumReleaseAge: 10080 + +# No dependency needs its install script: esbuild ships its binary as an optional package. +allowBuilds: + '@google/genai': false + cpu-features: false + esbuild: false + protobufjs: false + ssh2: false