From b0c93505c56ae608c43b1c0dcd301b8ce0060bc6 Mon Sep 17 00:00:00 2001 From: wayneweiwen Date: Sun, 20 Sep 2026 18:20:13 +0800 Subject: [PATCH 1/3] code_executors/cube: support Tencent AGS client configuration Add a provider-specific configuration adapter for Tencent Cloud Agent Sandbox while preserving the existing Cube sandbox client and workspace runtime. Cover endpoint precedence, request options, creation metadata, public exports, and bilingual documentation. RELEASE NOTES: Cube workspaces can now connect to Tencent Cloud Agent Sandbox through TencentAGSClientConfig. --- docs/mkdocs/en/code_executor.md | 33 ++++ docs/mkdocs/en/skill.md | 2 +- docs/mkdocs/zh/code_executor.md | 31 ++++ docs/mkdocs/zh/skill.md | 2 +- examples/skills_with_cube/README.md | 23 ++- tests/code_executors/cube/test_tencent_ags.py | 144 ++++++++++++++++++ .../code_executors/cube/__init__.py | 2 + .../code_executors/cube/_sandbox.py | 13 +- .../code_executors/cube/_tencent_ags.py | 89 +++++++++++ trpc_agent_sdk/code_executors/cube/_types.py | 21 +++ 10 files changed, 346 insertions(+), 14 deletions(-) create mode 100644 tests/code_executors/cube/test_tencent_ags.py create mode 100644 trpc_agent_sdk/code_executors/cube/_tencent_ags.py diff --git a/docs/mkdocs/en/code_executor.md b/docs/mkdocs/en/code_executor.md index a41eef25d..f38b53b89 100644 --- a/docs/mkdocs/en/code_executor.md +++ b/docs/mkdocs/en/code_executor.md @@ -338,6 +338,39 @@ cfg = CubeCodeExecutorConfig( executor = await CubeCodeExecutor.create(cfg) ``` +### Tencent Cloud Agent Sandbox + +Tencent Cloud Agent Sandbox (AGS) exposes an E2B-compatible endpoint through +a regional domain. Use `TencentAGSClientConfig` to select that endpoint without +turning the domain into an `api_url` manually: + +```python +from trpc_agent_sdk.code_executors.cube import ( + TencentAGSClientConfig, + create_cube_sandbox_client, + create_cube_workspace_runtime, +) + +cfg = TencentAGSClientConfig( + template="your-template-id", + api_key="your-ags-api-key", + domain="ap-guangzhou.tencentags.com", + request_timeout=60, + metadata={"application": "my-agent"}, + auto_recover=True, +) +sandbox_client = await create_cube_sandbox_client(cfg) +runtime = create_cube_workspace_runtime(sandbox_client=sandbox_client) +``` + +`domain`, `api_key`, and `template` may instead be supplied through +`E2B_DOMAIN`, `E2B_API_KEY`, and `CUBE_TEMPLATE_ID`. Local E2B key-format +validation defaults to `False` for AGS keys; remote authentication is still +performed by AGS. When both endpoint fields are passed explicitly, `api_url` +takes precedence over `domain`; explicit fields always take precedence over +environment variables. The API URL form is useful when traffic must go through +a private gateway. + `CubeCodeExecutor` accepts the same `code_block_delimiters` as the other executors; by default it adds a `bash` delimiter on top of the default `python` and `tool_code` delimiters so plain `\`\`\`bash\n ... \n\`\`\`` diff --git a/docs/mkdocs/en/skill.md b/docs/mkdocs/en/skill.md index dcdc2eaae..07b137deb 100644 --- a/docs/mkdocs/en/skill.md +++ b/docs/mkdocs/en/skill.md @@ -1103,7 +1103,7 @@ LLM calls skill_run(skill="python-math", command="python3 scripts/fib.py 10") - **Cube executor** (remote E2B sandbox): [trpc_agent_sdk/code_executors/cube/_runtime.py](../../../trpc_agent_sdk/code_executors/cube/_runtime.py) - Executes inside a remote Cube/E2B sandbox; suitable for environments without local Docker, or when strong remote isolation is required - Construct via `create_cube_workspace_runtime(executor, workspace_cfg=...)`; see [code_executor.md](code_executor.md#cubeworkspaceruntime) for details - - Requires the optional `[cube]` extra (`pip install 'trpc-agent-py[cube]'`) and the `E2B_API_URL` / `E2B_API_KEY` / `CUBE_TEMPLATE_ID` environment variables (or equivalent cfg fields) + - Requires the optional `[cube]` extra (`pip install 'trpc-agent-py[cube]'`) and either a generic E2B endpoint (`E2B_API_URL`) or `TencentAGSClientConfig` with `E2B_DOMAIN`; credentials and template come from `E2B_API_KEY` / `CUBE_TEMPLATE_ID` or equivalent cfg fields **Container executor notes**: - The run base directory is writable; when `$SKILLS_ROOT` is set, it is mounted in read-only mode diff --git a/docs/mkdocs/zh/code_executor.md b/docs/mkdocs/zh/code_executor.md index 30af7405e..30b11e3f6 100644 --- a/docs/mkdocs/zh/code_executor.md +++ b/docs/mkdocs/zh/code_executor.md @@ -333,6 +333,37 @@ cfg = CubeCodeExecutorConfig( executor = await CubeCodeExecutor.create(cfg) ``` +### 腾讯云 Agent Sandbox + +腾讯云 Agent Sandbox(AGS)通过地域域名提供 E2B 兼容接口。使用 +`TencentAGSClientConfig` 可以直接配置该域名,无需手工将其转换成 +`api_url`: + +```python +from trpc_agent_sdk.code_executors.cube import ( + TencentAGSClientConfig, + create_cube_sandbox_client, + create_cube_workspace_runtime, +) + +cfg = TencentAGSClientConfig( + template="your-template-id", + api_key="your-ags-api-key", + domain="ap-guangzhou.tencentags.com", + request_timeout=60, + metadata={"application": "my-agent"}, + auto_recover=True, +) +sandbox_client = await create_cube_sandbox_client(cfg) +runtime = create_cube_workspace_runtime(sandbox_client=sandbox_client) +``` + +`domain`、`api_key` 和 `template` 也可以分别通过 `E2B_DOMAIN`、 +`E2B_API_KEY` 和 `CUBE_TEMPLATE_ID` 提供。AGS API Key 默认关闭 E2B SDK +的本地格式校验,但 AGS 服务端鉴权仍然生效。同时显式设置两个端点字段时, +`api_url` 优先于 `domain`;显式字段始终优先于环境变量。API URL 形式适用于 +需要经过私有网关的场景。 + `CubeCodeExecutor` 也支持 `code_block_delimiters`,默认在标准的 `python` 和 `tool_code` 之外又加了一个 `bash` 分隔符,所以普通的 \`\`\`bash\n ... \n\`\`\` 围栏也能被识别。 diff --git a/docs/mkdocs/zh/skill.md b/docs/mkdocs/zh/skill.md index abc60e02a..566877f56 100644 --- a/docs/mkdocs/zh/skill.md +++ b/docs/mkdocs/zh/skill.md @@ -1102,7 +1102,7 @@ LLM 调用 skill_run(skill="python-math", command="python3 scripts/fib.py 10") - **Cube 执行器**(远端 E2B 沙箱):[trpc_agent_sdk/code_executors/cube/_runtime.py](../../../trpc_agent_sdk/code_executors/cube/_runtime.py) - 在远端 Cube/E2B 沙箱中执行;适合宿主上没有 Docker、或者需要强远端隔离的场景 - 通过 `create_cube_workspace_runtime(executor, workspace_cfg=...)` 构造;详见 [code_executor.md](code_executor.md#cubeworkspaceruntime) - - 需要安装可选 extra `[cube]`(`pip install 'trpc-agent-py[cube]'`),并配置 `E2B_API_URL` / `E2B_API_KEY` / `CUBE_TEMPLATE_ID` 环境变量(或对应 cfg 字段) + - 需要安装可选 extra `[cube]`(`pip install 'trpc-agent-py[cube]'`),并配置通用 E2B 地址(`E2B_API_URL`),或使用 `TencentAGSClientConfig` 配合 `E2B_DOMAIN`;凭证与模板可通过 `E2B_API_KEY` / `CUBE_TEMPLATE_ID` 或对应 cfg 字段提供 **容器执行器注意事项**: - 运行基础目录可写;当设置了 `$SKILLS_ROOT` 时,会以只读方式挂载 diff --git a/examples/skills_with_cube/README.md b/examples/skills_with_cube/README.md index e93202ec8..3e2bf1912 100644 --- a/examples/skills_with_cube/README.md +++ b/examples/skills_with_cube/README.md @@ -43,9 +43,30 @@ source .venv/bin/activate - `TRPC_AGENT_BASE_URL` - `TRPC_AGENT_MODEL_NAME` - `CUBE_TEMPLATE_ID=your-cube-template-id` -- `E2B_API_URL=your-e2b-api-url` - `E2B_API_KEY=your-e2b-api-key` +通用 E2B/Cube 网关配置: + +- `E2B_API_URL=your-e2b-api-url` + +腾讯云 Agent Sandbox 可改用 `TencentAGSClientConfig`,并配置: + +- `E2B_DOMAIN=ap-guangzhou.tencentags.com` + +```python +from trpc_agent_sdk.code_executors.cube import TencentAGSClientConfig + +cfg = TencentAGSClientConfig( + request_timeout=60, + metadata={"application": "skills-with-cube"}, + auto_recover=True, +) +``` + +`TencentAGSClientConfig` 默认关闭 E2B SDK 对 AGS API Key 的本地格式校验; +AGS 服务端鉴权仍然生效。显式配置优先于环境变量;同时显式传入 `api_url` +和 `domain` 时,`api_url` 优先,可用于私有网关或代理。 + ### 运行命令 ```bash diff --git a/tests/code_executors/cube/test_tencent_ags.py b/tests/code_executors/cube/test_tencent_ags.py new file mode 100644 index 000000000..afebd457a --- /dev/null +++ b/tests/code_executors/cube/test_tencent_ags.py @@ -0,0 +1,144 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +"""Tests for Tencent Cloud Agent Sandbox configuration.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +import pytest + +from trpc_agent_sdk.code_executors.cube import CubeSandboxClient +from trpc_agent_sdk.code_executors.cube import TencentAGSClientConfig + + +def _cfg(**overrides) -> TencentAGSClientConfig: + values = { + "template": "tmpl", + "api_key": "ark-secret", + "domain": "ap-guangzhou.tencentags.com", + "idle_timeout": 123, + "execute_timeout": 45.0, + } + values.update(overrides) + return TencentAGSClientConfig(**values) + + +class TestTencentAGSClientConfig: + + def test_defaults_disable_local_api_key_format_validation(self): + assert _cfg().validate_api_key is False + + def test_domain_falls_back_to_environment(self, monkeypatch): + monkeypatch.setenv("E2B_DOMAIN", "ap-shanghai.tencentags.com") + assert _cfg(domain=None).resolve_domain() == "ap-shanghai.tencentags.com" + + def test_missing_endpoint_raises(self, monkeypatch): + monkeypatch.delenv("E2B_API_URL", raising=False) + monkeypatch.delenv("E2B_DOMAIN", raising=False) + with pytest.raises(ValueError, match="E2B_DOMAIN"): + _cfg(domain=None)._e2b_connection_kwargs() + + @pytest.mark.parametrize("request_timeout", [0, -1.0]) + def test_rejects_non_positive_request_timeout(self, request_timeout): + with pytest.raises(ValueError, match="request_timeout must be > 0"): + _cfg(request_timeout=request_timeout) + + @pytest.mark.parametrize("request_timeout", [True, "30"]) + def test_rejects_non_numeric_request_timeout(self, request_timeout): + with pytest.raises(TypeError, match="request_timeout must be a number"): + _cfg(request_timeout=request_timeout) + + def test_rejects_non_bool_api_key_validation(self): + with pytest.raises(TypeError, match="validate_api_key must be a bool"): + _cfg(validate_api_key=0) + + +class TestTencentAGSClientConnectionOptions: + + @pytest.mark.asyncio + async def test_create_uses_domain_and_creation_metadata(self, fake_e2b, fake_async_sandbox): + fake_e2b.AsyncSandbox.create = AsyncMock(return_value=fake_async_sandbox) + cfg = _cfg( + request_timeout=30.0, + metadata={"application": "agent-test"}, + ) + + await CubeSandboxClient.open_new(cfg) + + fake_e2b.AsyncSandbox.create.assert_awaited_once_with( + template="tmpl", + timeout=123, + domain="ap-guangzhou.tencentags.com", + api_key="ark-secret", + validate_api_key=False, + request_timeout=30.0, + metadata={"application": "agent-test"}, + ) + + @pytest.mark.asyncio + async def test_attach_omits_creation_only_options(self, fake_e2b, fake_async_sandbox): + fake_e2b.AsyncSandbox.connect = AsyncMock(return_value=fake_async_sandbox) + cfg = _cfg( + sandbox_id="sbx-42", + request_timeout=30.0, + metadata={"application": "agent-test"}, + ) + + await CubeSandboxClient.open_existing(cfg) + + fake_e2b.AsyncSandbox.connect.assert_awaited_once_with( + "sbx-42", + domain="ap-guangzhou.tencentags.com", + api_key="ark-secret", + validate_api_key=False, + request_timeout=30.0, + ) + + @pytest.mark.asyncio + async def test_explicit_api_url_takes_precedence_over_domain(self, fake_e2b, fake_async_sandbox): + fake_e2b.AsyncSandbox.create = AsyncMock(return_value=fake_async_sandbox) + + await CubeSandboxClient.open_new(_cfg(api_url="https://private-gateway.example.com")) + + kwargs = fake_e2b.AsyncSandbox.create.await_args.kwargs + assert kwargs["api_url"] == "https://private-gateway.example.com" + assert "domain" not in kwargs + + @pytest.mark.asyncio + async def test_explicit_domain_takes_precedence_over_environment_api_url(self, fake_e2b, fake_async_sandbox, + monkeypatch): + fake_e2b.AsyncSandbox.create = AsyncMock(return_value=fake_async_sandbox) + monkeypatch.setenv("E2B_API_URL", "https://stale.example.com") + + await CubeSandboxClient.open_new(_cfg(domain="ap-shanghai.tencentags.com")) + + kwargs = fake_e2b.AsyncSandbox.create.await_args.kwargs + assert kwargs["domain"] == "ap-shanghai.tencentags.com" + assert "api_url" not in kwargs + + @pytest.mark.asyncio + async def test_environment_api_url_is_used_when_endpoint_fields_are_empty(self, fake_e2b, fake_async_sandbox, + monkeypatch): + fake_e2b.AsyncSandbox.create = AsyncMock(return_value=fake_async_sandbox) + monkeypatch.setenv("E2B_API_URL", "https://env-gateway.example.com") + + await CubeSandboxClient.open_new(_cfg(domain=None)) + + kwargs = fake_e2b.AsyncSandbox.create.await_args.kwargs + assert kwargs["api_url"] == "https://env-gateway.example.com" + assert "domain" not in kwargs + + @pytest.mark.asyncio + async def test_metadata_is_copied_before_passing_to_sdk(self, fake_e2b, fake_async_sandbox): + fake_e2b.AsyncSandbox.create = AsyncMock(return_value=fake_async_sandbox) + metadata = {"application": "agent-test"} + + await CubeSandboxClient.open_new(_cfg(metadata=metadata)) + + passed_metadata = fake_e2b.AsyncSandbox.create.await_args.kwargs["metadata"] + assert passed_metadata == metadata + assert passed_metadata is not metadata diff --git a/trpc_agent_sdk/code_executors/cube/__init__.py b/trpc_agent_sdk/code_executors/cube/__init__.py index 47fe50e89..9857b71b6 100644 --- a/trpc_agent_sdk/code_executors/cube/__init__.py +++ b/trpc_agent_sdk/code_executors/cube/__init__.py @@ -22,6 +22,7 @@ from ._sandbox import CubeCommandResult from ._sandbox import CubeSandboxClient from ._sandbox import create_cube_sandbox_client +from ._tencent_ags import TencentAGSClientConfig from ._transfer import OnExisting from ._types import CubeClientConfig from ._types import CubeCodeExecutorConfig @@ -39,6 +40,7 @@ "CubeWorkspaceManager", "CubeWorkspaceRuntime", "CubeWorkspaceRuntimeConfig", + "TencentAGSClientConfig", "OnExisting", "create_cube_workspace_runtime", ] diff --git a/trpc_agent_sdk/code_executors/cube/_sandbox.py b/trpc_agent_sdk/code_executors/cube/_sandbox.py index e6d432efb..c594a6059 100644 --- a/trpc_agent_sdk/code_executors/cube/_sandbox.py +++ b/trpc_agent_sdk/code_executors/cube/_sandbox.py @@ -140,12 +140,7 @@ def sandbox_id(self) -> str: @classmethod async def open_new(cls, cfg: CubeClientConfig) -> "CubeSandboxClient": """Create a brand-new remote sandbox.""" - sbx = await e2b.AsyncSandbox.create( - template=cfg.resolve_template(), - api_url=cfg.resolve_api_url(), - api_key=cfg.resolve_api_key(), - timeout=cfg.idle_timeout, - ) + sbx = await e2b.AsyncSandbox.create(**cfg._e2b_create_kwargs()) return cls(sbx, cfg) @classmethod @@ -164,11 +159,7 @@ async def open_existing( """ if not cfg.sandbox_id: raise ValueError("CubeSandboxClient.open_existing requires cfg.sandbox_id") - sbx = await e2b.AsyncSandbox.connect( - cfg.sandbox_id, - api_url=cfg.resolve_api_url(), - api_key=cfg.resolve_api_key(), - ) + sbx = await e2b.AsyncSandbox.connect(cfg.sandbox_id, **cfg._e2b_connection_kwargs()) client = cls(sbx, cfg) await client.assert_running() return client diff --git a/trpc_agent_sdk/code_executors/cube/_tencent_ags.py b/trpc_agent_sdk/code_executors/cube/_tencent_ags.py new file mode 100644 index 000000000..1a9288e99 --- /dev/null +++ b/trpc_agent_sdk/code_executors/cube/_tencent_ags.py @@ -0,0 +1,89 @@ +# Tencent is pleased to support the open source community by making tRPC-Agent-Python available. +# +# Copyright (C) 2026 Tencent. All rights reserved. +# +# tRPC-Agent-Python is licensed under Apache-2.0. +"""Tencent Cloud Agent Sandbox configuration for the Cube/E2B client.""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import Any +from typing import Mapping +from typing import Optional + +from ._types import ENV_API_URL +from ._types import CubeClientConfig + +ENV_TENCENT_AGS_DOMAIN = "E2B_DOMAIN" + + +@dataclass +class TencentAGSClientConfig(CubeClientConfig): + """Connect :class:`CubeSandboxClient` to Tencent Cloud Agent Sandbox. + + Tencent AGS exposes an E2B-compatible endpoint selected by ``domain``. + Its API keys do not use the public E2B key format, so local key-format + validation is disabled by default while remote authentication remains in + force. + + An explicit ``api_url`` takes precedence over an explicit ``domain``. + Environment values are consulted only when neither endpoint field is set, + preserving the usual explicit-configuration-over-environment precedence. + The API URL form supports private gateways and proxies without requiring a + second provider-specific configuration type. + """ + + domain: Optional[str] = None + """Tencent AGS domain. Falls back to ``E2B_DOMAIN``.""" + + validate_api_key: bool = False + """Whether the E2B SDK should validate the API key format locally.""" + + request_timeout: Optional[float] = None + """Optional E2B control-plane request timeout in seconds.""" + + metadata: Optional[Mapping[str, str]] = None + """Metadata attached when a new sandbox is created.""" + + def __post_init__(self) -> None: + super().__post_init__() + if not isinstance(self.validate_api_key, bool): + raise TypeError("validate_api_key must be a bool") + if self.request_timeout is not None: + if isinstance(self.request_timeout, bool) or not isinstance(self.request_timeout, (int, float)): + raise TypeError("request_timeout must be a number of seconds") + if self.request_timeout <= 0: + raise ValueError("request_timeout must be > 0 seconds") + + def resolve_domain(self) -> str: + """Resolve the Tencent AGS domain from the field or environment.""" + value = self.domain or os.getenv(ENV_TENCENT_AGS_DOMAIN) + if not value: + raise ValueError("Tencent AGS requires `domain`, E2B_DOMAIN, or an explicit " + "`api_url` / E2B_API_URL.") + return value + + def _e2b_connection_kwargs(self) -> dict[str, Any]: + if self.api_url: + endpoint = {"api_url": self.api_url} + elif self.domain: + endpoint = {"domain": self.domain} + else: + api_url = os.getenv(ENV_API_URL) + endpoint = {"api_url": api_url} if api_url else {"domain": self.resolve_domain()} + kwargs: dict[str, Any] = { + **endpoint, + "api_key": self.resolve_api_key(), + "validate_api_key": self.validate_api_key, + } + if self.request_timeout is not None: + kwargs["request_timeout"] = self.request_timeout + return kwargs + + def _e2b_create_kwargs(self) -> dict[str, Any]: + kwargs = super()._e2b_create_kwargs() + if self.metadata is not None: + kwargs["metadata"] = dict(self.metadata) + return kwargs diff --git a/trpc_agent_sdk/code_executors/cube/_types.py b/trpc_agent_sdk/code_executors/cube/_types.py index 4d25c609b..9a8b04a53 100644 --- a/trpc_agent_sdk/code_executors/cube/_types.py +++ b/trpc_agent_sdk/code_executors/cube/_types.py @@ -9,6 +9,7 @@ import os from dataclasses import dataclass +from typing import Any from typing import Optional DEFAULT_REMOTE_WORKSPACE = "/workspace/cube_agent" @@ -104,6 +105,26 @@ def resolve_api_key(self) -> str: raise ValueError(f"Cube sandbox requires `api_key` or {ENV_API_KEY} env.") return value + def _e2b_connection_kwargs(self) -> dict[str, Any]: + """Build SDK options shared by sandbox creation and attachment. + + This is the provider adaptation seam for E2B-compatible services. + Subclasses may select a different endpoint mechanism or add transport + options without coupling :class:`CubeSandboxClient` to a provider. + """ + return { + "api_url": self.resolve_api_url(), + "api_key": self.resolve_api_key(), + } + + def _e2b_create_kwargs(self) -> dict[str, Any]: + """Build SDK options for creating a sandbox.""" + return { + "template": self.resolve_template(), + "timeout": self.idle_timeout, + **self._e2b_connection_kwargs(), + } + # Deprecated, will be removed in the future CubeCodeExecutorConfig = CubeClientConfig From a5b5cfbb27caed26abb9532b403eb3aaca57f05b Mon Sep 17 00:00:00 2001 From: wayneweiwen Date: Sun, 20 Sep 2026 19:23:24 +0800 Subject: [PATCH 2/3] code_executors/cube: honor Tencent AGS domain precedence Derive and pass the AGS control-plane URL alongside its domain so stale E2B_API_URL values cannot override provider configuration. Validate bare domains, cover the real E2B ConnectionConfig behavior, and keep the generic Cube example provider-neutral. RELEASE NOTES: Tencent AGS Cube configuration now reliably overrides stale E2B endpoint environment values and rejects malformed domains early. --- docs/mkdocs/en/code_executor.md | 11 +++-- docs/mkdocs/zh/code_executor.md | 8 ++-- examples/skills_with_cube/README.md | 23 +--------- tests/code_executors/cube/test_tencent_ags.py | 42 ++++++++++++++++++- .../code_executors/cube/_tencent_ags.py | 40 +++++++++++++++--- 5 files changed, 88 insertions(+), 36 deletions(-) diff --git a/docs/mkdocs/en/code_executor.md b/docs/mkdocs/en/code_executor.md index f38b53b89..8c59674bb 100644 --- a/docs/mkdocs/en/code_executor.md +++ b/docs/mkdocs/en/code_executor.md @@ -366,10 +366,13 @@ runtime = create_cube_workspace_runtime(sandbox_client=sandbox_client) `domain`, `api_key`, and `template` may instead be supplied through `E2B_DOMAIN`, `E2B_API_KEY`, and `CUBE_TEMPLATE_ID`. Local E2B key-format validation defaults to `False` for AGS keys; remote authentication is still -performed by AGS. When both endpoint fields are passed explicitly, `api_url` -takes precedence over `domain`; explicit fields always take precedence over -environment variables. The API URL form is useful when traffic must go through -a private gateway. +performed by AGS. A domain must be a bare hostname; pass a full URL through +`api_url` instead. When a domain is selected, the config also supplies the +derived `https://api.` control-plane URL so a stale `E2B_API_URL` +cannot override it. An explicit `api_url` selects the control-plane endpoint; +when `domain` is also supplied it remains the sandbox-routing domain. With +environment-only configuration, `E2B_DOMAIN` takes precedence over +`E2B_API_URL` for this provider-specific config. `CubeCodeExecutor` accepts the same `code_block_delimiters` as the other executors; by default it adds a `bash` delimiter on top of the default diff --git a/docs/mkdocs/zh/code_executor.md b/docs/mkdocs/zh/code_executor.md index 30b11e3f6..b1e8bd8a7 100644 --- a/docs/mkdocs/zh/code_executor.md +++ b/docs/mkdocs/zh/code_executor.md @@ -360,9 +360,11 @@ runtime = create_cube_workspace_runtime(sandbox_client=sandbox_client) `domain`、`api_key` 和 `template` 也可以分别通过 `E2B_DOMAIN`、 `E2B_API_KEY` 和 `CUBE_TEMPLATE_ID` 提供。AGS API Key 默认关闭 E2B SDK -的本地格式校验,但 AGS 服务端鉴权仍然生效。同时显式设置两个端点字段时, -`api_url` 优先于 `domain`;显式字段始终优先于环境变量。API URL 形式适用于 -需要经过私有网关的场景。 +的本地格式校验,但 AGS 服务端鉴权仍然生效。`domain` 必须是裸域名;完整 URL +请通过 `api_url` 传入。选择 domain 时,配置还会显式传入派生的 +`https://api.` 控制面 URL,避免残留的 `E2B_API_URL` 覆盖它。显式 +`api_url` 用于选择控制面端点;同时提供 `domain` 时,后者仍用于沙箱路由。 +仅使用环境变量时,该提供商配置中的 `E2B_DOMAIN` 优先于 `E2B_API_URL`。 `CubeCodeExecutor` 也支持 `code_block_delimiters`,默认在标准的 `python` 和 `tool_code` 之外又加了一个 `bash` 分隔符,所以普通的 diff --git a/examples/skills_with_cube/README.md b/examples/skills_with_cube/README.md index 3e2bf1912..e93202ec8 100644 --- a/examples/skills_with_cube/README.md +++ b/examples/skills_with_cube/README.md @@ -43,29 +43,8 @@ source .venv/bin/activate - `TRPC_AGENT_BASE_URL` - `TRPC_AGENT_MODEL_NAME` - `CUBE_TEMPLATE_ID=your-cube-template-id` -- `E2B_API_KEY=your-e2b-api-key` - -通用 E2B/Cube 网关配置: - - `E2B_API_URL=your-e2b-api-url` - -腾讯云 Agent Sandbox 可改用 `TencentAGSClientConfig`,并配置: - -- `E2B_DOMAIN=ap-guangzhou.tencentags.com` - -```python -from trpc_agent_sdk.code_executors.cube import TencentAGSClientConfig - -cfg = TencentAGSClientConfig( - request_timeout=60, - metadata={"application": "skills-with-cube"}, - auto_recover=True, -) -``` - -`TencentAGSClientConfig` 默认关闭 E2B SDK 对 AGS API Key 的本地格式校验; -AGS 服务端鉴权仍然生效。显式配置优先于环境变量;同时显式传入 `api_url` -和 `domain` 时,`api_url` 优先,可用于私有网关或代理。 +- `E2B_API_KEY=your-e2b-api-key` ### 运行命令 diff --git a/tests/code_executors/cube/test_tencent_ags.py b/tests/code_executors/cube/test_tencent_ags.py index afebd457a..5cdc46161 100644 --- a/tests/code_executors/cube/test_tencent_ags.py +++ b/tests/code_executors/cube/test_tencent_ags.py @@ -10,6 +10,7 @@ from unittest.mock import AsyncMock import pytest +from e2b import ConnectionConfig from trpc_agent_sdk.code_executors.cube import CubeSandboxClient from trpc_agent_sdk.code_executors.cube import TencentAGSClientConfig @@ -56,6 +57,23 @@ def test_rejects_non_bool_api_key_validation(self): with pytest.raises(TypeError, match="validate_api_key must be a bool"): _cfg(validate_api_key=0) + @pytest.mark.parametrize("domain", [ + "https://ap-guangzhou.tencentags.com", + "ap-guangzhou.tencentags.com/v1", + "ap-guangzhou.tencentags.com:443", + " ap-guangzhou.tencentags.com", + ]) + def test_rejects_domain_that_is_not_a_bare_hostname(self, domain): + with pytest.raises(ValueError, match="domain must be a bare hostname"): + _cfg(domain=domain) + + def test_rejects_invalid_environment_domain_when_resolved(self, monkeypatch): + monkeypatch.setenv("E2B_DOMAIN", "https://ap-guangzhou.tencentags.com") + cfg = _cfg(domain=None, api_url=None) + + with pytest.raises(ValueError, match="domain must be a bare hostname"): + cfg._e2b_connection_kwargs() + class TestTencentAGSClientConnectionOptions: @@ -72,6 +90,7 @@ async def test_create_uses_domain_and_creation_metadata(self, fake_e2b, fake_asy fake_e2b.AsyncSandbox.create.assert_awaited_once_with( template="tmpl", timeout=123, + api_url="https://api.ap-guangzhou.tencentags.com", domain="ap-guangzhou.tencentags.com", api_key="ark-secret", validate_api_key=False, @@ -92,6 +111,7 @@ async def test_attach_omits_creation_only_options(self, fake_e2b, fake_async_san fake_e2b.AsyncSandbox.connect.assert_awaited_once_with( "sbx-42", + api_url="https://api.ap-guangzhou.tencentags.com", domain="ap-guangzhou.tencentags.com", api_key="ark-secret", validate_api_key=False, @@ -106,7 +126,7 @@ async def test_explicit_api_url_takes_precedence_over_domain(self, fake_e2b, fak kwargs = fake_e2b.AsyncSandbox.create.await_args.kwargs assert kwargs["api_url"] == "https://private-gateway.example.com" - assert "domain" not in kwargs + assert kwargs["domain"] == "ap-guangzhou.tencentags.com" @pytest.mark.asyncio async def test_explicit_domain_takes_precedence_over_environment_api_url(self, fake_e2b, fake_async_sandbox, @@ -118,12 +138,30 @@ async def test_explicit_domain_takes_precedence_over_environment_api_url(self, f kwargs = fake_e2b.AsyncSandbox.create.await_args.kwargs assert kwargs["domain"] == "ap-shanghai.tencentags.com" - assert "api_url" not in kwargs + assert kwargs["api_url"] == "https://api.ap-shanghai.tencentags.com" + + sdk_config = ConnectionConfig(**_cfg(domain="ap-shanghai.tencentags.com")._e2b_connection_kwargs()) + assert sdk_config.domain == "ap-shanghai.tencentags.com" + assert sdk_config.api_url == "https://api.ap-shanghai.tencentags.com" + + @pytest.mark.asyncio + async def test_environment_domain_takes_precedence_over_environment_api_url(self, fake_e2b, fake_async_sandbox, + monkeypatch): + fake_e2b.AsyncSandbox.create = AsyncMock(return_value=fake_async_sandbox) + monkeypatch.setenv("E2B_DOMAIN", "ap-shanghai.tencentags.com") + monkeypatch.setenv("E2B_API_URL", "https://stale.example.com") + + await CubeSandboxClient.open_new(_cfg(domain=None)) + + kwargs = fake_e2b.AsyncSandbox.create.await_args.kwargs + assert kwargs["domain"] == "ap-shanghai.tencentags.com" + assert kwargs["api_url"] == "https://api.ap-shanghai.tencentags.com" @pytest.mark.asyncio async def test_environment_api_url_is_used_when_endpoint_fields_are_empty(self, fake_e2b, fake_async_sandbox, monkeypatch): fake_e2b.AsyncSandbox.create = AsyncMock(return_value=fake_async_sandbox) + monkeypatch.delenv("E2B_DOMAIN", raising=False) monkeypatch.setenv("E2B_API_URL", "https://env-gateway.example.com") await CubeSandboxClient.open_new(_cfg(domain=None)) diff --git a/trpc_agent_sdk/code_executors/cube/_tencent_ags.py b/trpc_agent_sdk/code_executors/cube/_tencent_ags.py index 1a9288e99..6d015199c 100644 --- a/trpc_agent_sdk/code_executors/cube/_tencent_ags.py +++ b/trpc_agent_sdk/code_executors/cube/_tencent_ags.py @@ -13,7 +13,6 @@ from typing import Mapping from typing import Optional -from ._types import ENV_API_URL from ._types import CubeClientConfig ENV_TENCENT_AGS_DOMAIN = "E2B_DOMAIN" @@ -49,6 +48,8 @@ class TencentAGSClientConfig(CubeClientConfig): def __post_init__(self) -> None: super().__post_init__() + if self.domain is not None: + self._validate_domain(self.domain) if not isinstance(self.validate_api_key, bool): raise TypeError("validate_api_key must be a bool") if self.request_timeout is not None: @@ -63,16 +64,45 @@ def resolve_domain(self) -> str: if not value: raise ValueError("Tencent AGS requires `domain`, E2B_DOMAIN, or an explicit " "`api_url` / E2B_API_URL.") + return self._validate_domain(value) + + @staticmethod + def _validate_domain(value: str) -> str: + if not isinstance(value, str): + raise TypeError("domain must be a string containing a bare hostname") + if (not value or value != value.strip() or any(char.isspace() for char in value) + or any(marker in value for marker in (":", "/", "\\", "?", "#"))): + raise ValueError("domain must be a bare hostname such as " + "'ap-guangzhou.tencentags.com'; use api_url for a full URL") return value + @staticmethod + def _api_url_for_domain(domain: str) -> str: + """Build the control-plane URL using the E2B SDK's domain rule.""" + return f"https://api.{domain}" + def _e2b_connection_kwargs(self) -> dict[str, Any]: + configured_domain = self.domain or os.getenv(ENV_TENCENT_AGS_DOMAIN) + domain = self.resolve_domain() if configured_domain else None + if self.api_url: endpoint = {"api_url": self.api_url} - elif self.domain: - endpoint = {"domain": self.domain} + if domain is not None: + endpoint["domain"] = domain + elif domain is not None: + # ConnectionConfig resolves api_url as: explicit value, + # E2B_API_URL, then https://api.. Pass the derived URL + # explicitly so a stale E2B_API_URL cannot override this domain. + endpoint = { + "api_url": self._api_url_for_domain(domain), + "domain": domain, + } else: - api_url = os.getenv(ENV_API_URL) - endpoint = {"api_url": api_url} if api_url else {"domain": self.resolve_domain()} + try: + endpoint = {"api_url": self.resolve_api_url()} + except ValueError as exc: + raise ValueError("Tencent AGS requires `domain`, E2B_DOMAIN, or an explicit " + "`api_url` / E2B_API_URL.") from exc kwargs: dict[str, Any] = { **endpoint, "api_key": self.resolve_api_key(), From 657a67045c172f914efaefe628fed26ac0e67372 Mon Sep 17 00:00:00 2001 From: wayneweiwen Date: Sun, 20 Sep 2026 20:20:26 +0800 Subject: [PATCH 3/3] test(cube): align AGS endpoint assertion with E2B config --- tests/code_executors/cube/test_tencent_ags.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/code_executors/cube/test_tencent_ags.py b/tests/code_executors/cube/test_tencent_ags.py index 5cdc46161..b9c3e98f0 100644 --- a/tests/code_executors/cube/test_tencent_ags.py +++ b/tests/code_executors/cube/test_tencent_ags.py @@ -140,7 +140,12 @@ async def test_explicit_domain_takes_precedence_over_environment_api_url(self, f assert kwargs["domain"] == "ap-shanghai.tencentags.com" assert kwargs["api_url"] == "https://api.ap-shanghai.tencentags.com" - sdk_config = ConnectionConfig(**_cfg(domain="ap-shanghai.tencentags.com")._e2b_connection_kwargs()) + connection_kwargs = _cfg(domain="ap-shanghai.tencentags.com")._e2b_connection_kwargs() + sdk_config = ConnectionConfig( + api_key=connection_kwargs["api_key"], + api_url=connection_kwargs["api_url"], + domain=connection_kwargs["domain"], + ) assert sdk_config.domain == "ap-shanghai.tencentags.com" assert sdk_config.api_url == "https://api.ap-shanghai.tencentags.com"