From 785a334e82221a48ea0a60fce824c8f9af1da838 Mon Sep 17 00:00:00 2001 From: evanlowe <62918515+evanlowe@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:56:28 +0800 Subject: [PATCH] fix(studio): normalize Codex sandbox tool names --- frontend/server/studio_update_resources.py | 8 ++- tests/cli/test_studio_deploy_target.py | 5 ++ tests/cli/test_studio_sandbox_tools.py | 58 ++++++++++++++++++- tests/cli/test_studio_update.py | 3 + .../server/test_studio_update_resources.py | 17 ++++-- veadk/cli/cli_frontend.py | 44 ++++++++------ veadk/cli/studio_sandbox_tools.py | 52 ++++++++++++++--- 7 files changed, 151 insertions(+), 36 deletions(-) diff --git a/frontend/server/studio_update_resources.py b/frontend/server/studio_update_resources.py index 8e38c74a..0607aded 100644 --- a/frontend/server/studio_update_resources.py +++ b/frontend/server/studio_update_resources.py @@ -26,7 +26,7 @@ SnapshotKind = Literal["codex", "openclaw", "hermes"] _SNAPSHOT_ENVIRONMENTS: tuple[tuple[str, SnapshotKind, str], ...] = ( - ("SANDBOX_CHAT_CODEX_SNAPSHOT", "codex", "chat"), + ("SANDBOX_CHAT_CODEX_SNAPSHOT", "codex", "codex"), ("SANDBOX_CHAT_OPENCLAW_SNAPSHOT", "openclaw", "openclaw"), ("SANDBOX_CHAT_HERMES_SNAPSHOT", "hermes", "hermes"), ) @@ -69,18 +69,20 @@ def _provision_snapshot_tool( ensure_studio_code_env_tool, studio_sandbox_agent_model_name, studio_sandbox_model_base_url, - studio_sandbox_tool_name, + studio_sandbox_tool_name_candidates, ) - tool_name = studio_sandbox_tool_name( + tool_names = studio_sandbox_tool_name_candidates( application_id, purpose, snapshot=True, ) + tool_name = tool_names[0] model_name = studio_sandbox_agent_model_name(provider) if kind == "codex": tool_id = ensure_studio_code_env_tool( name=tool_name, + legacy_names=tool_names[1:], enable_snapshot=True, region=region, access_key=access_key, diff --git a/tests/cli/test_studio_deploy_target.py b/tests/cli/test_studio_deploy_target.py index e7231677..4cdb8d82 100644 --- a/tests/cli/test_studio_deploy_target.py +++ b/tests/cli/test_studio_deploy_target.py @@ -1524,6 +1524,11 @@ def _ensure_tool(**kwargs: object) -> str: creation_barrier.wait(timeout=5) assert kwargs["create_min_interval"] == 0.5 snapshot = bool(kwargs["enable_snapshot"]) + assert "-codex-" in str(kwargs["name"]) + legacy_names = kwargs["legacy_names"] + assert isinstance(legacy_names, tuple) + assert len(legacy_names) == 1 + assert "-chat-" in str(legacy_names[0]) with created_kinds_lock: created_kinds.append("codex_snapshot" if snapshot else "codex") if snapshot: diff --git a/tests/cli/test_studio_sandbox_tools.py b/tests/cli/test_studio_sandbox_tools.py index c9a4a3ef..c785ab98 100644 --- a/tests/cli/test_studio_sandbox_tools.py +++ b/tests/cli/test_studio_sandbox_tools.py @@ -16,7 +16,7 @@ import os from types import SimpleNamespace -from typing import cast +from typing import Any, cast from unittest.mock import patch import pytest @@ -32,6 +32,7 @@ studio_sandbox_agent_model_name, studio_sandbox_model_base_url, studio_sandbox_tool_name, + studio_sandbox_tool_name_candidates, ) @@ -64,6 +65,46 @@ def test_ensure_studio_code_env_tool_reuses_ready_exact_name() -> None: ) +def test_ensure_studio_code_env_tool_reuses_legacy_name() -> None: + requested_names: list[str] = [] + + def _list_tools(request: object) -> SimpleNamespace: + filters = cast(Any, request).filters + requested_name = str(filters[0].values[0]) + requested_names.append(requested_name) + tools = [] + if requested_name == "studio-demo-chat-123456": + tools.append( + SimpleNamespace( + name=requested_name, + project_name="default", + tool_type="CodeEnv", + tool_id="legacy-tool", + ) + ) + return SimpleNamespace(tools=tools, next_token=None) + + client = SimpleNamespace( + list_tools=_list_tools, + get_tool=lambda _: SimpleNamespace(status="Ready"), + create_tool=lambda _: pytest.fail("the legacy Tool must be reused"), + ) + + assert ( + ensure_studio_code_env_tool( + name="studio-demo-codex-123456", + legacy_names=("studio-demo-chat-123456",), + client=client, + timeout_seconds=0, + ) + == "legacy-tool" + ) + assert requested_names == [ + "studio-demo-codex-123456", + "studio-demo-chat-123456", + ] + + def test_ensure_studio_code_env_tool_creates_ready_code_env() -> None: requests: list[object] = [] @@ -391,6 +432,21 @@ def test_studio_sandbox_tool_name_uses_short_studio_format() -> None: ) +def test_codex_tool_name_candidates_prefer_codex_and_fall_back_to_chat() -> None: + assert studio_sandbox_tool_name_candidates("Studio App", "codex") == ( + "studio-studio-app-codex-1d66ce", + "studio-studio-app-chat-1d66ce", + ) + assert studio_sandbox_tool_name_candidates( + "Studio App", + "codex", + snapshot=True, + ) == ( + "studio-studio-app-codex-1d66ce_snapshot", + "studio-studio-app-chat-1d66ce_snapshot", + ) + + def test_studio_sandbox_tool_name_normalizes_invalid_characters() -> None: assert studio_sandbox_tool_name(" My_App@2026! ", "skill") == ( "studio-my-app-2026-skill-6c7faf" diff --git a/tests/cli/test_studio_update.py b/tests/cli/test_studio_update.py index a7af1309..ba4aaa22 100644 --- a/tests/cli/test_studio_update.py +++ b/tests/cli/test_studio_update.py @@ -900,6 +900,9 @@ def update_application_code_bundle(self, **kwargs: object) -> str: assert result.exit_code == 0, result.output assert len(code_tools) == 1 assert code_tools[0]["enable_snapshot"] is True + assert "-codex-" in str(code_tools[0]["name"]) + assert len(code_tools[0]["legacy_names"]) == 1 + assert "-chat-" in str(code_tools[0]["legacy_names"][0]) assert str(code_tools[0]["name"]).endswith("_snapshot") if dev_tool_id is None: assert len(dev_tools) == 1 diff --git a/tests/frontend/server/test_studio_update_resources.py b/tests/frontend/server/test_studio_update_resources.py index 0019105c..e29160ec 100644 --- a/tests/frontend/server/test_studio_update_resources.py +++ b/tests/frontend/server/test_studio_update_resources.py @@ -186,6 +186,10 @@ def _tool(**kwargs: Any) -> str: ] assert {call["kind"] for call in tool_calls} == {"codex", "openclaw", "hermes"} assert all(call["application_id"] == "application-id" for call in tool_calls) + assert ( + next(call for call in tool_calls if call["kind"] == "codex")["purpose"] + == "codex" + ) def test_reconcile_studio_update_resources_only_repairs_missing_items( @@ -228,7 +232,7 @@ def test_provision_codex_snapshot_tool_binds_model_credential( ) -> None: captured: dict[str, Any] = {} monkeypatch.setattr( - "veadk.cli.studio_sandbox_tools.studio_sandbox_tool_name", + "veadk.cli.studio_sandbox_tools.studio_sandbox_tool_name_candidates", lambda application_id, purpose, *, snapshot: ( captured.update( { @@ -237,7 +241,7 @@ def test_provision_codex_snapshot_tool_binds_model_credential( "snapshot": snapshot, } ) - or "snapshot-tool-name" + or ("snapshot-tool-name", "legacy-snapshot-tool-name") ), ) monkeypatch.setattr( @@ -255,7 +259,7 @@ def test_provision_codex_snapshot_tool_binds_model_credential( tool_id = _provision_snapshot_tool( kind="codex", - purpose="chat", + purpose="codex", provider="byteplus", region="ap-southeast-1", application_id="application-id", @@ -265,7 +269,10 @@ def test_provision_codex_snapshot_tool_binds_model_credential( ) assert tool_id == "tool-id" + assert captured["purpose"] == "codex" assert captured["snapshot"] is True + assert captured["tool"]["name"] == "snapshot-tool-name" + assert captured["tool"]["legacy_names"] == ("legacy-snapshot-tool-name",) assert captured["tool"]["enable_snapshot"] is True assert captured["credential"]["tool_id"] == "tool-id" assert captured["credential"]["provider"] == "byteplus" @@ -277,8 +284,8 @@ def test_provision_agent_snapshot_tool_binds_provider_model( ) -> None: captured: dict[str, Any] = {} monkeypatch.setattr( - "veadk.cli.studio_sandbox_tools.studio_sandbox_tool_name", - lambda *_args, **_kwargs: "snapshot-tool-name", + "veadk.cli.studio_sandbox_tools.studio_sandbox_tool_name_candidates", + lambda *_args, **_kwargs: ("snapshot-tool-name",), ) monkeypatch.setattr( "veadk.cli.studio_sandbox_tools.studio_sandbox_agent_model_name", diff --git a/veadk/cli/cli_frontend.py b/veadk/cli/cli_frontend.py index d69ac6f1..a30fac1e 100644 --- a/veadk/cli/cli_frontend.py +++ b/veadk/cli/cli_frontend.py @@ -8237,8 +8237,8 @@ def frontend_deploy( "dev": "Dev Sandbox", } sandbox_tool_purposes = { - "codex": "chat", - "codex_snapshot": "chat", + "codex": "codex", + "codex_snapshot": "codex", "openclaw": "openclaw", "openclaw_snapshot": "openclaw", "hermes": "hermes", @@ -8252,38 +8252,40 @@ def frontend_deploy( ensure_studio_dev_env_tool, studio_sandbox_agent_model_name, studio_sandbox_model_base_url, - studio_sandbox_tool_name, + studio_sandbox_tool_name_candidates, ) sandbox_agent_model_name = studio_sandbox_agent_model_name(provider_id) sandbox_model_base_url = studio_sandbox_model_base_url(provider_id) - missing_sandbox_tools: dict[str, str] = {} + missing_sandbox_tools: dict[str, tuple[str, ...]] = {} for kind, tool_id in sandbox_tool_ids.items(): label = sandbox_tool_labels[kind] if tool_id: click.echo(f"Using configured AgentKit {label} Tool '{tool_id}'.") continue - tool_name = studio_sandbox_tool_name( + tool_names = studio_sandbox_tool_name_candidates( vefaas_app_name, sandbox_tool_purposes[kind], snapshot=kind.endswith("_snapshot"), ) - click.echo(f"Creating AgentKit {label} Tool '{tool_name}'…") - missing_sandbox_tools[kind] = tool_name + click.echo(f"Creating AgentKit {label} Tool '{tool_names[0]}'…") + missing_sandbox_tools[kind] = tool_names if missing_sandbox_tools: with ThreadPoolExecutor(max_workers=len(missing_sandbox_tools)) as executor: tool_futures = {} - for kind, tool_name in missing_sandbox_tools.items(): + for kind, tool_names in missing_sandbox_tools.items(): if tool_futures: sleep(_SANDBOX_TOOL_CREATE_STAGGER_SECONDS) + tool_name = tool_names[0] base_kind = kind.removesuffix("_snapshot") enable_snapshot = kind.endswith("_snapshot") if base_kind == "codex": future = executor.submit( ensure_studio_code_env_tool, name=tool_name, + legacy_names=tool_names[1:], enable_snapshot=enable_snapshot, create_min_interval=_SANDBOX_TOOL_CREATE_STAGGER_SECONDS, region=region, @@ -8948,7 +8950,7 @@ def frontend_update( ensure_studio_code_env_tool, studio_sandbox_agent_model_name, studio_sandbox_model_base_url, - studio_sandbox_tool_name, + studio_sandbox_tool_name_candidates, ) snapshot_labels = { @@ -8957,14 +8959,14 @@ def frontend_update( "hermes_snapshot": "Hermes Snapshot", } snapshot_purposes = { - "codex_snapshot": "chat", + "codex_snapshot": "codex", "openclaw_snapshot": "openclaw", "hermes_snapshot": "hermes", } sandbox_agent_model_name = studio_sandbox_agent_model_name(provider_id) sandbox_model_base_url = studio_sandbox_model_base_url(provider_id) missing_snapshot_tools = { - kind: studio_sandbox_tool_name( + kind: studio_sandbox_tool_name_candidates( vefaas_app_name, snapshot_purposes[kind], snapshot=True, @@ -8977,9 +8979,10 @@ def frontend_update( max_workers=len(missing_snapshot_tools) ) as executor: tool_futures = {} - for kind, tool_name in missing_snapshot_tools.items(): + for kind, tool_names in missing_snapshot_tools.items(): if tool_futures: sleep(_SANDBOX_TOOL_CREATE_STAGGER_SECONDS) + tool_name = tool_names[0] label = snapshot_labels[kind] click.echo(f"Creating AgentKit {label} Tool '{tool_name}'…") base_kind = kind.removesuffix("_snapshot") @@ -8987,6 +8990,7 @@ def frontend_update( future = executor.submit( ensure_studio_code_env_tool, name=tool_name, + legacy_names=tool_names[1:], enable_snapshot=True, create_min_interval=( _SANDBOX_TOOL_CREATE_STAGGER_SECONDS @@ -9126,7 +9130,7 @@ def frontend_update( } byteplus_sandbox_purposes = { "dev": "dev", - "codex": "chat", + "codex": "codex", "openclaw": "openclaw", "hermes": "hermes", } @@ -9141,39 +9145,41 @@ def frontend_update( ensure_studio_dev_env_tool, studio_sandbox_agent_model_name, studio_sandbox_model_base_url, - studio_sandbox_tool_name, + studio_sandbox_tool_name_candidates, ) sandbox_agent_model_name = studio_sandbox_agent_model_name(provider_id) sandbox_model_base_url = studio_sandbox_model_base_url(provider_id) - missing_sandbox_tools: dict[str, str] = {} + missing_sandbox_tools: dict[str, tuple[str, ...]] = {} for kind, tool_id in byteplus_sandbox_tool_ids.items(): label = byteplus_sandbox_labels[kind] if str(tool_id or "").strip(): click.echo(f"Using AgentKit {label} Tool '{tool_id}'.") continue - tool_name = studio_sandbox_tool_name( + tool_names = studio_sandbox_tool_name_candidates( vefaas_app_name, byteplus_sandbox_purposes[kind], snapshot=kind.endswith("_snapshot"), ) - click.echo(f"Creating AgentKit {label} Tool '{tool_name}'…") - missing_sandbox_tools[kind] = tool_name + click.echo(f"Creating AgentKit {label} Tool '{tool_names[0]}'…") + missing_sandbox_tools[kind] = tool_names if missing_sandbox_tools: with ThreadPoolExecutor( max_workers=len(missing_sandbox_tools) ) as ex: tool_futures = {} - for kind, tool_name in missing_sandbox_tools.items(): + for kind, tool_names in missing_sandbox_tools.items(): if tool_futures: sleep(_SANDBOX_TOOL_CREATE_STAGGER_SECONDS) + tool_name = tool_names[0] base_kind = kind.removesuffix("_snapshot") enable_snapshot = kind.endswith("_snapshot") if base_kind == "codex": future = ex.submit( ensure_studio_code_env_tool, name=tool_name, + legacy_names=tool_names[1:], enable_snapshot=enable_snapshot, create_min_interval=( _SANDBOX_TOOL_CREATE_STAGGER_SECONDS diff --git a/veadk/cli/studio_sandbox_tools.py b/veadk/cli/studio_sandbox_tools.py index 9c159783..ef9dca46 100644 --- a/veadk/cli/studio_sandbox_tools.py +++ b/veadk/cli/studio_sandbox_tools.py @@ -107,6 +107,31 @@ def studio_sandbox_tool_name( return f"{name}{suffix}" +def studio_sandbox_tool_name_candidates( + application_name: str, + purpose: str, + *, + snapshot: bool = False, +) -> tuple[str, ...]: + """Return the preferred Tool name followed by compatible legacy names.""" + names = [ + studio_sandbox_tool_name( + application_name, + purpose, + snapshot=snapshot, + ) + ] + if purpose == "codex": + names.append( + studio_sandbox_tool_name( + application_name, + "chat", + snapshot=snapshot, + ) + ) + return tuple(names) + + def _wait_for_ready_tool( tools_client: Any, tools_types: Any, @@ -326,6 +351,7 @@ def _ensure_studio_environment_tool( region: str = "cn-beijing", session_token: str = "", enable_snapshot: bool = False, + legacy_names: tuple[str, ...] = (), client: Any | None = None, timeout_seconds: float = 600.0, poll_interval: float = 5.0, @@ -344,14 +370,24 @@ def _ensure_studio_environment_tool( region=region, session_token=session_token, ) - match = _find_exact_tool( - tools_client, - tools_types, - name=name, - tool_type=tool_type, - ) + matched_name = name + match = None + seen_names: set[str] = set() + for candidate_name in (name, *legacy_names): + if candidate_name in seen_names: + continue + seen_names.add(candidate_name) + match = _find_exact_tool( + tools_client, + tools_types, + name=candidate_name, + tool_type=tool_type, + ) + if match is not None: + matched_name = candidate_name + break if match is not None: - tool_id = _created_tool_id(match, name=name) + tool_id = _created_tool_id(match, name=matched_name) assert tool_id is not None else: tool_id = _create_tool_with_retry( @@ -388,7 +424,7 @@ def _ensure_studio_environment_tool( tools_client, tools_types, tool_id=tool_id, - name=name, + name=matched_name, enable_snapshot=enable_snapshot, timeout_seconds=timeout_seconds, poll_interval=poll_interval,