Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions agentkit/toolkit/cli/sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,12 @@ agentkit sandbox create \
Options:

- `--tool-type`: optional. Tool type to create; defaults to `CodeEnv`.
`CustomToolEnv` is a CLI-side convention for private images based on aio-sandbox.
It is sent to CreateTool as `ToolType: Private`.
- `--tool-name`: optional. Tool name. If omitted, the CLI generates a name like
`agentkit-codeenv-<random>`.
- `--image-url`: optional custom image URL. Required when
`--tool-type CustomToolEnv`.
- `--tos-bucket`: optional. TOS bucket to mount. If omitted, the tool is
created without TOS mount configuration.
- `--tos-mount`: optional. Local mount path for `--tos-bucket`; defaults to
Expand Down Expand Up @@ -85,6 +89,20 @@ Options:
The sandbox create request maps `--cpu` to `CpuMilli=<cpu * 1000>` and
`MemoryMb=<cpu * 2048>`, so the default shape is 4 vCPU / 8 GiB.

When `--tool-type CustomToolEnv` is used, the CLI creates a private-image tool
without requiring a control-plane tool type change:

```bash
agentkit sandbox create \
--tool-type CustomToolEnv \
--image-url registry.example.com/custom-image:latest
```

The CreateTool request is translated to `ToolType: Private`,
`Command: /opt/gem/run.sh`, and the environment variables matching the
aio-sandbox startup profile. The CLI stores the resulting tool locally under the
`CustomToolEnv` type for future sandbox resolution.

The tool injects the selected built-in provider's Volcengine Ark compatible
endpoints into `OPENCODE_BASE_URL`, `CODEX_BASE_URL`, `MODEL_BASE_URL`, and
`ANTHROPIC_BASE_URL`, and stores the selected provider in
Expand Down
73 changes: 59 additions & 14 deletions agentkit/toolkit/cli/sandbox/cli_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@
from agentkit.sdk.tools.client import AgentkitToolsClient
from agentkit.sdk.tools import types as tools_types
from agentkit.toolkit.cli.sandbox.env_config import (
CUSTOM_TOOL_ENV_COMMAND,
CUSTOM_TOOL_ENV_OPENAPI_TOOL_TYPE,
CUSTOM_TOOL_ENV_PORT,
CUSTOM_TOOL_ENV_TOOL_TYPE,
DEFAULT_CREATE_TOOL_TYPE,
build_create_tool_envs,
build_custom_tool_envs,
)
from agentkit.toolkit.cli.sandbox.model_config import (
ModelProviderType,
Expand Down Expand Up @@ -102,9 +107,42 @@ def _build_create_tool_request(
model_base_url_was_provided: Optional[bool] = None,
role_name: Optional[str] = None,
websearch_apikey: Optional[str] = None,
image_url: Optional[str] = None,
) -> tools_types.CreateToolRequest:
resolved_tool_type = tool_type.strip() or DEFAULT_CREATE_TOOL_TYPE
resolved_name = (name or "").strip() or _generate_tool_name(resolved_tool_type)
is_custom_tool_env = resolved_tool_type == CUSTOM_TOOL_ENV_TOOL_TYPE
if is_custom_tool_env and not (image_url or "").strip():
error("--image-url is required when --tool-type CustomToolEnv")
openapi_tool_type = (
CUSTOM_TOOL_ENV_OPENAPI_TOOL_TYPE
if is_custom_tool_env
else resolved_tool_type
)
command = CUSTOM_TOOL_ENV_COMMAND if is_custom_tool_env else None
port = CUSTOM_TOOL_ENV_PORT if is_custom_tool_env else None
envs = (
build_custom_tool_envs(
model_name=model_name,
model_api_key=model_api_key,
model_provider=model_provider,
model_base_url=model_base_url,
model_provider_was_provided=model_provider_was_provided,
model_base_url_was_provided=model_base_url_was_provided,
websearch_apikey=websearch_apikey,
)
if is_custom_tool_env
else build_create_tool_envs(
tool_type=resolved_tool_type,
model_name=model_name,
model_api_key=model_api_key,
model_provider=model_provider,
model_base_url=model_base_url,
model_provider_was_provided=model_provider_was_provided,
model_base_url_was_provided=model_base_url_was_provided,
websearch_apikey=websearch_apikey,
)
)
tos_mount_config = build_create_tool_tos_mount_config(
tos_bucket,
tos_region,
Expand All @@ -116,7 +154,10 @@ def _build_create_tool_request(

return tools_types.CreateToolRequest(
Name=resolved_name,
ToolType=resolved_tool_type,
ToolType=openapi_tool_type,
Command=command,
ImageUrl=(image_url or "").strip() or None,
Port=port,
CpuMilli=cpu_milli,
MemoryMb=memory_mb,
RoleName=role_name,
Expand All @@ -131,16 +172,7 @@ def _build_create_tool_request(
EnablePrivateNetwork=False,
),
TosMountConfig=tos_mount_config,
Envs=build_create_tool_envs(
tool_type=resolved_tool_type,
model_name=model_name,
model_api_key=model_api_key,
model_provider=model_provider,
model_base_url=model_base_url,
model_provider_was_provided=model_provider_was_provided,
model_base_url_was_provided=model_base_url_was_provided,
websearch_apikey=websearch_apikey,
),
Envs=envs,
)


Expand Down Expand Up @@ -314,7 +346,9 @@ def create_tool(
skill_role_name: Optional[str] = None,
skill_role_name_provided: bool = False,
websearch_apikey: Optional[str] = None,
image_url: Optional[str] = None,
) -> dict[str, object]:
is_custom_tool_env = tool_type.strip() == CUSTOM_TOOL_ENV_TOOL_TYPE
resolved_model_base_url = normalize_model_base_url(model_base_url)
raw_model_provider = (
model_provider.value
Expand Down Expand Up @@ -353,6 +387,7 @@ def create_tool(
model_base_url_was_provided=bool(resolved_model_base_url),
role_name=resolved_role_name,
websearch_apikey=resolved_websearch_apikey,
image_url=image_url,
)
client = AgentkitToolsClient(
region=region,
Expand All @@ -364,11 +399,15 @@ def create_tool(
final_tool = _wait_for_tool_ready(client, tool_id)
return {
"tool_id": tool_id,
"tool_type": final_tool.tool_type or request.tool_type,
"tool_type": (
CUSTOM_TOOL_ENV_TOOL_TYPE
if is_custom_tool_env
else final_tool.tool_type or request.tool_type
),
"name": final_tool.name or request.name,
"status": final_tool.status or TOOL_READY_STATUS,
"model_provider": resolved_model_provider,
"model_base_url": resolved_model_base_url,
"model_provider": None if is_custom_tool_env else resolved_model_provider,
"model_base_url": None if is_custom_tool_env else resolved_model_base_url,
"role_name": resolved_role_name,
"websearch_apikey_set": bool(resolved_websearch_apikey),
}
Expand Down Expand Up @@ -443,6 +482,11 @@ def create_command(
"Use --disable-websearch-apikey in exec to disable it per session."
),
),
image_url: Optional[str] = typer.Option(
None,
"--image-url",
help="Custom image URL. Required when --tool-type CustomToolEnv.",
),
) -> None:
"""Create an AgentKit Tool with optional TOS mount.

Expand All @@ -468,6 +512,7 @@ def create_command(
skill_role_name=skill_role_name,
skill_role_name_provided=skill_role_name_provided,
websearch_apikey=websearch_apikey,
image_url=image_url,
)
save_tool_result(str(result["tool_type"]), result)
except (typer.Abort, typer.Exit):
Expand Down
135 changes: 135 additions & 0 deletions agentkit/toolkit/cli/sandbox/env_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,108 @@
)

DEFAULT_CREATE_TOOL_TYPE = "CodeEnv"
CUSTOM_TOOL_ENV_TOOL_TYPE = "CustomToolEnv"
CUSTOM_TOOL_ENV_OPENAPI_TOOL_TYPE = "Private"
CUSTOM_TOOL_ENV_COMMAND = "/opt/gem/run.sh"
CUSTOM_TOOL_ENV_PORT = 8080
CUSTOM_TOOL_ENV_VARS = (
(
"PATH",
"/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:"
"/usr/sbin:/usr/bin:/sbin:/bin",
),
("DEBIAN_FRONTEND", "noninteractive"),
("USER", "gem"),
("USER_UID", "1000"),
("USER_GID", "1000"),
("DISPLAY", ":99.0"),
("DISPLAY_WIDTH", "1280"),
("DISPLAY_HEIGHT", "1024"),
("DISPLAY_DEPTH", "24"),
("XDG_RUNTIME_DIR", "/tmp/runtime-gem"),
("BROWSER_EXECUTABLE_PATH", "/usr/local/bin/browser"),
("BROWSER_REMOTE_DEBUGGING_PORT", "9222"),
(
"BROWSER_COMMANDLINE_ARGS",
"--disable-backgrounding-occluded-windows "
"--disable-background-timer-throttling "
"--disable-blink-features=AutomationControlled "
"--disable-dev-shm-usage "
"--disable-external-intent-requests "
"--disable-features=IPH_DesktopCustomizeChrome,IsolateOrigins,"
"site-per-proces,Translate "
"--disable-focus-on-load "
"--disable-gpu "
"--disable-infobars "
"--disable-popup-blocking "
"--disable-prompt-on-repost "
"--disable-renderer-backgrounding "
"--disable-site-isolation-trials "
"--disable-web-security "
"--disable-window-activation "
"--mute-audio "
"--no-default-browser-check "
"--no-first-run "
"--noerrdialogs "
"--remote-allow-origins=* "
"--remote-debugging-port=9222 "
"--suppress-message-center-popups "
"--start-maximized",
),
("BROWSER_EXTRA_ARGS", ""),
("DNS_OVER_HTTPS_TEMPLATES", ""),
("LOG_DIR", "/var/log/gem"),
("JWT_PUBLIC_KEY", ""),
("VNC_SERVER_PORT", "5900"),
("WEBSOCKET_PROXY_PORT", "6080"),
("GEM_SERVER_PORT", "8088"),
("MCP_SERVER_PORT", "8089"),
("PUBLIC_PORT", "8080"),
("AUTH_BACKEND_PORT", "8081"),
("WAIT_PORTS", "8091"),
("WAIT_TIMEOUT", "300"),
("WAIT_INTERVAL", "0.25"),
("RUN_HOOK_INIT", ""),
("RUN_HOOK_PRE_SERVICES", ""),
("RUN_HOOK_POST_READY", ""),
("RUN_HOOKS_STRICT", "false"),
("SANDBOX_SRV_PORT", "8091"),
("JUPYTER_LAB_PORT", "8888"),
("CODE_SERVER_PORT", "8200"),
("MCP_SERVER_BROWSER_PORT", "8100"),
("TINYPROXY_PORT", "8118"),
("MAX_SHELL_SESSIONS", "50"),
("PYTHONPATH", ""),
("LOG_TOOL_TRACE", "false"),
("LANG", "en_US.UTF-8"),
("LANGUAGE", "en_US:en"),
("LC_ALL", "en_US.UTF-8"),
("PUPPETEER_EXECUTABLE_PATH", "/usr/local/bin/browser"),
("PUPPETEER_SKIP_CHROMIUM_DOWNLOAD", "true"),
("BROWSER_NO_SANDBOX", ""),
("BROWSER_LANG", "en-US"),
(
"BROWSER_USER_AGENT",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/140.0.0.0 Safari/537.36",
),
("UV_TOOL_BIN_DIR", "/usr/local/bin/"),
("UV_TOOL_DIR", "/usr/local/share/uv/tools"),
("DISABLE_JUPYTER", "false"),
("DISABLE_CODE_SERVER", "false"),
("EXTRA_MCP_SERVERS", ""),
("OTEL_SDK_DISABLED", "false"),
(
"SRV_PYTHONPATH",
"/otel-auto-instrumentation-python/opentelemetry/instrumentation/"
"auto_instrumentation:/otel-auto-instrumentation-python",
),
("OTEL_PYTHON_DISABLED_INSTRUMENTATIONS", "redis"),
("FAAS_SANDBOX_RUNTIME_INJECTION_ENABLE_SANDBOXD", "false"),
("PYTHON_CODE_EXEC_VERSION", "python3"),
("GO_PATH", "/usr/local/go"),
)
DISABLED_SERVICE_ENV_KEYS = (
"DISABLE_JUPYTER",
"DISABLE_CODE_SERVER",
Expand Down Expand Up @@ -319,6 +421,39 @@ def build_create_tool_envs(
return bundle.to_create_tool_envs()


def build_custom_tool_envs(
*,
model_name: Optional[str] = None,
model_api_key: Optional[str] = None,
model_provider: str | ModelProviderType | None = None,
model_base_url: Optional[str] = None,
model_provider_was_provided: Optional[bool] = None,
model_base_url_was_provided: Optional[bool] = None,
websearch_apikey: Optional[str] = None,
) -> list[tools_types.EnvsItemForCreateTool]:
"""Build CreateTool.Envs for CustomToolEnv plus CodeEnv-only envs."""

envs = [
tools_types.EnvsItemForCreateTool(Key=key, Value=value)
for key, value in CUSTOM_TOOL_ENV_VARS
]
custom_tool_env_keys = {key for key, _value in CUSTOM_TOOL_ENV_VARS}
code_envs = build_create_tool_envs(
tool_type=DEFAULT_CREATE_TOOL_TYPE,
model_name=model_name,
model_api_key=model_api_key,
model_provider=model_provider,
model_base_url=model_base_url,
model_provider_was_provided=model_provider_was_provided,
model_base_url_was_provided=model_base_url_was_provided,
websearch_apikey=websearch_apikey,
)
for env in code_envs or []:
if env.key not in custom_tool_env_keys:
envs.append(env)
return envs


def build_exec_session_envs(
*,
model_name: Optional[str] = None,
Expand Down
3 changes: 2 additions & 1 deletion agentkit/toolkit/cli/sandbox/tool_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@

SANDBOX_TOOL_STORE_PATH = Path(".agentkit") / "sandbox" / "tools.json"
DEFAULT_SANDBOX_TOOL_TYPE = "CodeEnv"
VALID_SANDBOX_TOOL_TYPES = ("CodeEnv", "SkillEnv")
VALID_SANDBOX_TOOL_TYPES = ("CodeEnv", "SkillEnv", "CustomToolEnv")
READY_TOOL_STATUS = "Ready"
TOOL_NOT_FOUND_ERROR_CODE = "InvalidResource.NotFound"


class SandboxToolType(str, Enum):
CODE_ENV = "CodeEnv"
SKILL_ENV = "SkillEnv"
CUSTOM_TOOL_ENV = "CustomToolEnv"


def normalize_tool_type(tool_type: str | SandboxToolType | None) -> str:
Expand Down
Loading