From ee4a5e6331a1c8e3cdda3bf223a20996bbd4b66c Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 23 Jul 2026 14:07:35 +0800 Subject: [PATCH 1/3] fix(studio): bind sandbox model credential directly --- README.md | 2 +- docs/content/docs/framework/frontend.en.mdx | 28 +++--- tests/cli/test_frontend_skill_creator.py | 86 ++++++++++-------- tests/cli/test_studio_deploy_target.py | 4 +- veadk/cli/cli_frontend.py | 6 +- veadk/cli/frontend_skill_creator.py | 98 +++++++-------------- 6 files changed, 97 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 6a24036b..89fd6b53 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ VeADK provides several useful command line tools for faster deployment and optim two dedicated AgentKit CodeEnv Tools are created automatically for temporary chats and Skill creation unless their IDs are supplied with `--sandbox-chat-codex-tool-id` and `--sandbox-skill-creator-tool-id`; Tool or - credential-relay provisioning failures print the underlying error verbatim + model-credential provisioning failures print the underlying error verbatim after credential values are redacted - `veadk studio update --vefaas-app-name `: build the frontend from a local VeADK source checkout and release it through the existing VeFaaS diff --git a/docs/content/docs/framework/frontend.en.mdx b/docs/content/docs/framework/frontend.en.mdx index 0f92c968..2b635da9 100644 --- a/docs/content/docs/framework/frontend.en.mdx +++ b/docs/content/docs/framework/frontend.en.mdx @@ -180,11 +180,11 @@ The new-chat **Skill creation** mode runs Ark Responses. Each candidate gets an independent AgentKit CodeEnv session. The server validates the generated directory name, `SKILL.md` frontmatter, file count, size, and safe paths before packaging a ZIP. AgentKit credential -hosting keeps the real Ark API key; Studio and Sandbox receive only its -revocable gateway ticket. Per-job Session variables cannot override that -hosted ticket, and Studio accepts only Volcengine HTTPS credential relay URLs. -Model credentials are never returned to the browser, and the Skill creation -APIs require the Studio developer or admin role. +configuration resolves the Ark API key with the deployer's AK/SK and writes it +directly to each dedicated CodeEnv Tool. Per-job Session variables cannot +override the Tool's model credential. Model credentials are never returned to +the browser, and the Skill creation APIs require the Studio developer or admin +role. After submission, both candidate conversations appear immediately and independently render public reasoning summaries, tool calls, and assistant @@ -204,11 +204,10 @@ export SANDBOX_SKILL_CREATOR= veadk frontend --agents-dir examples ``` -For local Studio, run the AgentKit `credential-hosting` command and choose to -write its result into both dedicated CodeEnv Tools. Adding a -candidate to AgentKit uploads its ZIP to TOS, then creates or updates the Skill -through the Skills API. It is published to a SkillSpace only when the user -provides SkillSpace IDs. +For local Studio, configure the Ark model variables on both dedicated CodeEnv +Tools. Adding a candidate to AgentKit uploads its ZIP to TOS, then creates or +updates the Skill through the Skills API. It is published to a SkillSpace only +when the user provides SkillSpace IDs. | Environment variable | Default | Description | | :-- | :-- | :-- | @@ -226,11 +225,12 @@ requests reach different instances. On the first cloud deployment, the command creates two independent CodeEnv Tools with the deployer's AK/SK when Tool IDs are omitted. Existing Tools can be selected with `--sandbox-chat-codex-tool-id` and -`--sandbox-skill-creator-tool-id`. AgentKit credential hosting stores the Ark -credential in KMS and binds only the relay URL and revocable ticket to both -Tools. The VeFaaS Function receives only Tool IDs, never model credentials: +`--sandbox-skill-creator-tool-id`. The Ark credential is resolved with the +deployer's AK/SK and written directly to both Tools as model environment +variables. Sandbox sessions inherit those Tool variables. The browser and +VeFaaS Function receive only Tool IDs, never model credentials. -If Tool or credential-relay provisioning fails, the CLI prints the underlying +If Tool or model-credential provisioning fails, the CLI prints the underlying error verbatim, including its original line breaks, after redacting credential values. Use the service action, error code, request ID, and request details to identify the missing permission or account resource. diff --git a/tests/cli/test_frontend_skill_creator.py b/tests/cli/test_frontend_skill_creator.py index e1c61a9e..dd24e043 100644 --- a/tests/cli/test_frontend_skill_creator.py +++ b/tests/cli/test_frontend_skill_creator.py @@ -40,7 +40,7 @@ mount_skill_creator_routes, ) -_RELAY_URL = "https://test.apigateway-cn-beijing.volceapi.com/api/v3" +_MODEL_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3" def _skill_zip(name: str = "weather-report") -> bytes: @@ -101,7 +101,7 @@ def create_candidate( request: str, ) -> dict[str, str]: del tool_id, job_id, label, request - assert model_base_url == _RELAY_URL + assert model_base_url == _MODEL_BASE_URL calls.append((candidate_id, model)) return {"instanceId": f"instance-{candidate_id}", "endpoint": "endpoint"} @@ -109,7 +109,7 @@ def create_candidate( patch.object( service, "_validate_tool", - return_value=_RELAY_URL, + return_value=_MODEL_BASE_URL, ), patch.object(service, "_create_candidate", side_effect=create_candidate), ): @@ -275,7 +275,7 @@ def create_candidate(*args: object, **kwargs: object) -> dict[str, str]: patch.object( service, "_validate_tool", - return_value=_RELAY_URL, + return_value=_MODEL_BASE_URL, ), patch.object(service, "_create_candidate", side_effect=create_candidate), patch.object(service, "_delete_instances") as delete_instances, @@ -329,50 +329,58 @@ def test_archive_metadata_rejects_symlink_entry() -> None: SkillCreatorService(tool_id="tool-id")._archive_metadata(output.getvalue()) -def test_credential_hosting_is_bound_to_tool_without_raw_key() -> None: +def test_model_credential_is_bound_directly_to_tool() -> None: + access_key = os.urandom(16).hex() + model_api_key = os.urandom(24).hex() + secret_key = os.urandom(24).hex() + calls: list[tuple[str, dict[str, object]]] = [] + class FakeApi: - def call(self, *_args: object, **_kwargs: object) -> dict[str, object]: - return { - "Tool": { - "Envs": [ - {"Key": "CODEX_API_KEY", "Value": "raw-key"}, - {"Key": "CODEX_BASE_URL", "Value": "https://ark.example"}, - ] + def call( + self, + _service: str, + action: str, + _version: str, + body: dict[str, object], + ) -> dict[str, object]: + calls.append((action, body)) + if action == "GetTool": + return { + "Tool": {"Envs": [{"Key": "EXISTING_ENV", "Value": "preserved"}]} } - } + return {} - updates: dict[str, str] = {} with ( patch("agentkit.auth._openapi.OpenApiClient", return_value=FakeApi()), patch( - "agentkit.auth.credential_hosting.list_gateways", - return_value=[{"id": "gateway-id", "name": "agentkit-credhost-gw"}], - ), - patch("veadk.auth.veauth.ark_veauth.get_ark_token", return_value="raw-key"), - patch( - "agentkit.auth.credential_hosting.host_model_key", - return_value=SimpleNamespace( - ticket="ck-hosted-ticket", - model_base_url=_RELAY_URL, - ), - ), - patch( - "agentkit.auth.credential_hosting.set_tool_env", - side_effect=lambda _api, _tool_id, values: updates.update(values), - ), + "veadk.auth.veauth.ark_veauth.get_ark_token", + return_value=model_api_key, + ) as get_ark_token, ): ensure_skill_creator_model_credential( tool_id="tool-id", - access_key="access-key", - secret_key="secret-key", + access_key=access_key, + secret_key=secret_key, ) - assert updates["CODEX_API_KEY"] == "ck-hosted-ticket" - assert updates["CODEX_BASE_URL"] == _RELAY_URL - assert "raw-key" not in updates.values() + get_ark_token.assert_called_once_with( + region="cn-beijing", + access_key=access_key, + secret_key=secret_key, + session_token=None, + ) + assert [action for action, _ in calls] == ["GetTool", "UpdateTool"] + update_body = calls[1][1] + envs = { + item["Key"]: item["Value"] + for item in cast(list[dict[str, str]], update_body["Envs"]) + } + assert envs["EXISTING_ENV"] == "preserved" + assert envs["CODEX_API_KEY"] == model_api_key + assert envs["CODEX_BASE_URL"] == _MODEL_BASE_URL -def test_candidate_session_never_overrides_hosted_tool_ticket(monkeypatch) -> None: +def test_candidate_session_never_overrides_tool_model_credential(monkeypatch) -> None: service = SkillCreatorService(tool_id="tool-id") captured: dict[str, object] = {} @@ -400,7 +408,7 @@ def delete_session(self, _request: object) -> None: "a", "doubao-seed-2-0-pro-260215", "豆包 Seed 2.0 Pro", - _RELAY_URL, + _MODEL_BASE_URL, "Create a release notes Skill", ) @@ -412,13 +420,13 @@ def delete_session(self, _request: object) -> None: ) -def test_tool_rejects_untrusted_credential_relay_url() -> None: +def test_tool_rejects_untrusted_model_base_url() -> None: service = SkillCreatorService(tool_id="tool-id") tool = SimpleNamespace( tool_type="CodeEnv", status="Ready", envs=[ - SimpleNamespace(key="CODEX_API_KEY", value="ck-hosted-ticket"), + SimpleNamespace(key="CODEX_API_KEY", value=os.urandom(24).hex()), SimpleNamespace( key="CODEX_BASE_URL", value="http://attacker.invalid/api/v3" ), @@ -426,7 +434,7 @@ def test_tool_rejects_untrusted_credential_relay_url() -> None: ) with ( patch("veadk.cli.frontend_skill_creator.AgentkitToolsClient") as client_class, - pytest.raises(SkillCreatorError, match="中继地址无效"), + pytest.raises(SkillCreatorError, match="模型服务地址无效"), ): client_class.return_value.get_tool.return_value = tool service._validate_tool("tool-id") diff --git a/tests/cli/test_studio_deploy_target.py b/tests/cli/test_studio_deploy_target.py index b710bc8f..348e60d0 100644 --- a/tests/cli/test_studio_deploy_target.py +++ b/tests/cli/test_studio_deploy_target.py @@ -50,8 +50,8 @@ def _skip_serverless_role_setup(monkeypatch: pytest.MonkeyPatch) -> None: [ ("tool", "Failed to provision the AgentKit chat CodeEnv Tool"), ( - "relay", - "Failed to provision the AgentKit chat model credential relay", + "credential", + "Failed to provision the AgentKit chat model credential", ), ], ) diff --git a/veadk/cli/cli_frontend.py b/veadk/cli/cli_frontend.py index 408896df..95d5a8c0 100644 --- a/veadk/cli/cli_frontend.py +++ b/veadk/cli/cli_frontend.py @@ -3738,7 +3738,7 @@ def frontend_deploy( raise click.ClickException( f"AgentKit {purpose} CodeEnv Tool did not return a Tool ID." ) - click.echo(f"Ensuring the AgentKit {purpose} model credential relay…") + click.echo(f"Ensuring the AgentKit {purpose} model credential…") try: ensure_skill_creator_model_credential( tool_id=tool_id, @@ -3753,10 +3753,10 @@ def frontend_deploy( secrets=(ak, sk, session_token), ) raise click.ClickException( - f"Failed to provision the AgentKit {purpose} model credential relay. " + f"Failed to provision the AgentKit {purpose} model credential. " f"Underlying error:\n{detail}" ) from error - click.echo(f"AgentKit {purpose} model credential relay is ready.") + click.echo(f"AgentKit {purpose} model credential is ready.") chat_codex_tool_id = sandbox_tool_ids["chat"] skill_creator_tool_id = sandbox_tool_ids["skill"] diff --git a/veadk/cli/frontend_skill_creator.py b/veadk/cli/frontend_skill_creator.py index 3c64199e..97927523 100644 --- a/veadk/cli/frontend_skill_creator.py +++ b/veadk/cli/frontend_skill_creator.py @@ -34,8 +34,6 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path, PurePosixPath from typing import Any -from urllib.parse import urlsplit - import requests from agentkit.sdk.skills import types as skills_types @@ -60,6 +58,7 @@ ("b", "deepseek-v4-flash-260425", "DeepSeek V4 Flash"), ) _MODEL_PROVIDER = "model_square" +_MODEL_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3" _REGION = "cn-beijing" _SESSION_TTL_SECONDS = 1800 _SESSION_DISCOVERY_ATTEMPTS = 6 @@ -685,22 +684,12 @@ def _safe_json_response( return payload -def _validate_credential_relay_url(value: str) -> str: - """Require an HTTPS AgentKit credential relay endpoint.""" - parsed = urlsplit(value) - hostname = (parsed.hostname or "").lower() - if ( - parsed.scheme != "https" - or not hostname.endswith(".volceapi.com") - or parsed.port not in (None, 443) - or parsed.username is not None - or parsed.password is not None - or parsed.path.rstrip("/") != "/api/v3" - or parsed.query - or parsed.fragment - ): - raise SkillCreatorError("Sandbox 模型凭证中继地址无效") - return value.rstrip("/") +def _validate_model_base_url(value: str) -> str: + """Require the Ark model endpoint configured by Studio deployment.""" + normalized = value.rstrip("/") + if normalized != _MODEL_BASE_URL: + raise SkillCreatorError("Sandbox 模型服务地址无效") + return normalized def ensure_skill_creator_model_credential( @@ -711,13 +700,8 @@ def ensure_skill_creator_model_credential( session_token: str | None = None, region: str = _REGION, ) -> None: - """Bind an AgentKit-hosted Ark credential to the dedicated CodeEnv Tool.""" + """Resolve an Ark API key and bind it directly to the CodeEnv Tool.""" from agentkit.auth._openapi import OpenApiClient - from agentkit.auth.credential_hosting import ( - host_model_key, - list_gateways, - set_tool_env, - ) from veadk.auth.veauth.ark_veauth import get_ark_token @@ -736,46 +720,17 @@ def ensure_skill_creator_model_credential( for item in tool.get("Envs", []) if item.get("Key") } - if str(envs.get("CODEX_API_KEY", "")).startswith("ck-") and envs.get( - "CODEX_BASE_URL" - ): - _validate_credential_relay_url(str(envs["CODEX_BASE_URL"])) - return - - gateway = next( - (item for item in list_gateways(api) if item["name"] == "agentkit-credhost-gw"), - None, - ) - provider_name = ( - f"veadk-skill-creator-{hashlib.sha256(tool_id.encode()).hexdigest()[:12]}" - ) - raw_model_key = get_ark_token( + model_api_key = get_ark_token( region=region, access_key=access_key, secret_key=secret_key, session_token=session_token, ) - try: - hosted = host_model_key( - key=raw_model_key, - gateway_id=gateway["id"] if gateway else None, - gateway_name=None if gateway else "agentkit-credhost-gw", - provider_name=provider_name, - upstream_url="https://ark.cn-beijing.volces.com", - model_path="/api/v3", - region=region, - api=api, - ) - finally: - raw_model_key = "" - if not hosted.ticket: - raise SkillCreatorError("AgentKit 凭据托管未返回 Sandbox 票据") - session_envs = build_exec_session_envs( model_name=_MODELS[0][1], - model_api_key=hosted.ticket, + model_api_key=model_api_key, model_provider=_MODEL_PROVIDER, - model_base_url=hosted.model_base_url, + model_base_url=_MODEL_BASE_URL, model_provider_was_provided=True, model_base_url_was_provided=True, include_codex_config=True, @@ -783,8 +738,19 @@ def ensure_skill_creator_model_credential( ) if not session_envs: raise SkillCreatorError("无法生成 Sandbox 模型环境变量") - _validate_credential_relay_url(hosted.model_base_url) - set_tool_env(api, tool_id, {item.key: item.value for item in session_envs}) + updates = {item.key: item.value for item in session_envs} + if all(envs.get(key) == value for key, value in updates.items()): + return + envs.update(updates) + api.call( + "agentkit", + "UpdateTool", + "2025-10-30", + { + "ToolId": tool_id, + "Envs": [{"Key": key, "Value": value} for key, value in envs.items()], + }, + ) class SkillCreatorService: @@ -805,17 +771,16 @@ def capabilities(self) -> dict[str, Any]: ) envs = {item.key: item.value for item in tool.envs or []} credential_ready = bool( - str(envs.get("CODEX_API_KEY") or "").startswith("ck-") - and envs.get("CODEX_BASE_URL") + envs.get("CODEX_API_KEY") and envs.get("CODEX_BASE_URL") ) if credential_ready: - _validate_credential_relay_url(str(envs["CODEX_BASE_URL"])) + _validate_model_base_url(str(envs["CODEX_BASE_URL"])) tool_ready = tool.tool_type == "CodeEnv" and tool.status == "Ready" enabled = tool_ready and credential_ready if not tool_ready: reason = "配置的 Sandbox 必须是 Ready CodeEnv" elif not credential_ready: - reason = "Sandbox 尚未绑定 AgentKit 托管模型凭证" + reason = "Sandbox 尚未绑定模型凭证" else: reason = "" except Exception: @@ -1282,12 +1247,9 @@ def _validate_tool(self, tool_id: str) -> str: if tool.tool_type != "CodeEnv" or tool.status != "Ready": raise SkillCreatorError("配置的 Sandbox 必须是 Ready CodeEnv") envs = {item.key: item.value for item in tool.envs or []} - if not ( - str(envs.get("CODEX_API_KEY") or "").startswith("ck-") - and envs.get("CODEX_BASE_URL") - ): - raise SkillCreatorError("Sandbox 尚未绑定 AgentKit 托管模型凭证") - return _validate_credential_relay_url(str(envs["CODEX_BASE_URL"])) + if not (envs.get("CODEX_API_KEY") and envs.get("CODEX_BASE_URL")): + raise SkillCreatorError("Sandbox 尚未绑定模型凭证") + return _validate_model_base_url(str(envs["CODEX_BASE_URL"])) def _tool_id(self, *, required: bool = True) -> str: if self._configured_tool_id: From e012b6c040f1aae13091a0d3e595f702844d9f5d Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 23 Jul 2026 14:47:16 +0800 Subject: [PATCH 2/3] fix(studio): use application control plane for Shanghai --- README.md | 2 ++ docs/content/docs/framework/frontend.en.mdx | 4 ++- tests/cli/test_studio_update.py | 34 +++++++++++++++++++-- veadk/integrations/ve_faas/ve_faas.py | 14 +++++---- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 89fd6b53..1710ad03 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,8 @@ VeADK provides several useful command line tools for faster deployment and optim policies; target `cn-beijing` (default) or `cn-shanghai` with `--region`, automatically locate the Identity user pool across Beijing and Shanghai, and select the VeFaaS project with `--project` (default `default`); + Shanghai Functions, gateways, and AgentKit resources stay in Shanghai while + VeFaaS Application operations use its Beijing control-plane endpoint; custom local or remote logo images are bundled into the deployment; the deployed client skips the second OAuth consent confirmation after login; two dedicated AgentKit CodeEnv Tools are created automatically for temporary diff --git a/docs/content/docs/framework/frontend.en.mdx b/docs/content/docs/framework/frontend.en.mdx index 2b635da9..f7a86d9a 100644 --- a/docs/content/docs/framework/frontend.en.mdx +++ b/docs/content/docs/framework/frontend.en.mdx @@ -276,7 +276,9 @@ veadk studio deploy \ also supports `cn-shanghai`. Deployment checks the deployment region first and then searches the Beijing and Shanghai Identity regions. A cross-region match emits a warning and continues. `--project` selects the VeFaaS function project -and defaults to `default`. +and defaults to `default`. For Shanghai deployments, the Function, APIG, and +AgentKit resources remain in Shanghai while VeFaaS Application operations use +the service's Beijing control-plane endpoint. To update an existing Studio deployment, run this command from a VeADK source checkout: diff --git a/tests/cli/test_studio_update.py b/tests/cli/test_studio_update.py index 48769834..08e7b887 100644 --- a/tests/cli/test_studio_update.py +++ b/tests/cli/test_studio_update.py @@ -147,7 +147,7 @@ def _get_function(self, _: object) -> SimpleNamespace: ] -def test_list_applications_uses_client_region( +def test_list_applications_uses_application_control_plane_region( monkeypatch: pytest.MonkeyPatch, ) -> None: requested_regions: list[str] = [] @@ -163,7 +163,7 @@ def _request(**kwargs: object) -> dict[str, object]: monkeypatch.setattr("veadk.integrations.ve_faas.ve_faas.ve_request", _request) assert service._list_application(app_name="studio-app") == [] - assert requested_regions == ["cn-shanghai"] + assert requested_regions == ["cn-beijing"] def test_load_deployed_site_logo_uses_current_branding_url( @@ -507,6 +507,36 @@ def test_update_application_code_bundle_merges_only_explicit_environment( } +def test_application_control_plane_uses_beijing_for_shanghai_deployment( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls: list[tuple[str, str]] = [] + service = object.__new__(VeFaaS) + cast(Any, service).ak = "ak" + cast(Any, service).sk = "sk" + cast(Any, service).region = "cn-shanghai" + + def _ve_request(**kwargs: object) -> dict[str, object]: + action = cast(str, kwargs["action"]) + region = cast(str, kwargs["region"]) + calls.append((action, region)) + if action == "GetApplication": + return {"Result": {"Status": "create_success"}} + return {"Result": {"Items": [], "Total": 0}} + + monkeypatch.setattr("veadk.integrations.ve_faas.ve_faas.ve_request", _ve_request) + + status, _ = service._get_application_status("application-id") + applications = service._list_application(app_name="studio-app") + + assert status == "create_success" + assert applications == [] + assert calls == [ + ("GetApplication", "cn-beijing"), + ("ListApplications", "cn-beijing"), + ] + + def test_update_application_code_bundle_preserves_unspecified_sandbox_tool( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, diff --git a/veadk/integrations/ve_faas/ve_faas.py b/veadk/integrations/ve_faas/ve_faas.py index d2c8301c..1beea731 100644 --- a/veadk/integrations/ve_faas/ve_faas.py +++ b/veadk/integrations/ve_faas/ve_faas.py @@ -43,6 +43,8 @@ logger = get_logger(__name__) +_APPLICATION_CONTROL_PLANE_REGION = "cn-beijing" + class VeFaaS: def __init__( @@ -179,7 +181,7 @@ def _create_application( sk=self.sk, service="vefaas", version="2021-03-03", - region="cn-beijing", + region=_APPLICATION_CONTROL_PLANE_REGION, host="open.volcengineapi.com", ) @@ -199,7 +201,7 @@ def _release_application(self, app_id: str): sk=self.sk, service="vefaas", version="2021-03-03", - region=self.region, + region=_APPLICATION_CONTROL_PLANE_REGION, host="open.volcengineapi.com", ) @@ -240,7 +242,7 @@ def _get_application_status(self, app_id: str): sk=self.sk, service="vefaas", version="2021-03-03", - region=self.region, + region=_APPLICATION_CONTROL_PLANE_REGION, host="open.volcengineapi.com", ) return response["Result"]["Status"], response @@ -273,7 +275,7 @@ def _list_application(self, app_id: str = None, app_name: str = None): sk=self.sk, service="vefaas", version="2021-03-03", - region=self.region, + region=_APPLICATION_CONTROL_PLANE_REGION, host="open.volcengineapi.com", ) result = response.get("Result", {}) @@ -486,7 +488,7 @@ def delete(self, app_id: str): sk=self.sk, service="vefaas", version="2021-03-03", - region="cn-beijing", + region=_APPLICATION_CONTROL_PLANE_REGION, host="open.volcengineapi.com", ) except Exception as e: @@ -870,7 +872,7 @@ def _get_application_logs(self, app_id: str) -> list[str]: sk=self.sk, service="vefaas", version="2021-03-03", - region="cn-beijing", + region=_APPLICATION_CONTROL_PLANE_REGION, host="open.volcengineapi.com", ) From 8b0d2731f14f02c20823c17fb52701f26bbf10d3 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 23 Jul 2026 15:07:39 +0800 Subject: [PATCH 3/3] fix(studio): use sandbox deployment region --- README.md | 3 ++- docs/content/docs/framework/frontend.en.mdx | 4 +++- tests/cli/test_frontend_skill_creator.py | 18 ++++++++++++++++++ tests/cli/test_studio_deploy_target.py | 1 + tests/cli/test_studio_update.py | 12 +++++++++--- veadk/cli/cli_frontend.py | 3 ++- veadk/cli/frontend_skill_creator.py | 21 ++++++++++++--------- 7 files changed, 47 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1710ad03..d09200bf 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,8 @@ VeADK provides several useful command line tools for faster deployment and optim `--region`, automatically locate the Identity user pool across Beijing and Shanghai, and select the VeFaaS project with `--project` (default `default`); Shanghai Functions, gateways, and AgentKit resources stay in Shanghai while - VeFaaS Application operations use its Beijing control-plane endpoint; + VeFaaS Application operations use its Beijing control-plane endpoint; the + selected region is also used for temporary-chat and Skill-creation sessions; custom local or remote logo images are bundled into the deployment; the deployed client skips the second OAuth consent confirmation after login; two dedicated AgentKit CodeEnv Tools are created automatically for temporary diff --git a/docs/content/docs/framework/frontend.en.mdx b/docs/content/docs/framework/frontend.en.mdx index f7a86d9a..bb8c8d57 100644 --- a/docs/content/docs/framework/frontend.en.mdx +++ b/docs/content/docs/framework/frontend.en.mdx @@ -278,7 +278,9 @@ then searches the Beijing and Shanghai Identity regions. A cross-region match emits a warning and continues. `--project` selects the VeFaaS function project and defaults to `default`. For Shanghai deployments, the Function, APIG, and AgentKit resources remain in Shanghai while VeFaaS Application operations use -the service's Beijing control-plane endpoint. +the service's Beijing control-plane endpoint. The deployed Function records the +selected AgentKit Sandbox region so temporary chat and Skill-creation sessions +query the same region as their CodeEnv Tools. To update an existing Studio deployment, run this command from a VeADK source checkout: diff --git a/tests/cli/test_frontend_skill_creator.py b/tests/cli/test_frontend_skill_creator.py index dd24e043..ce297a2c 100644 --- a/tests/cli/test_frontend_skill_creator.py +++ b/tests/cli/test_frontend_skill_creator.py @@ -465,3 +465,21 @@ def test_skill_creator_reads_only_dedicated_sandbox_tool_env(monkeypatch) -> Non monkeypatch.delenv("SANDBOX_SKILL_CREATOR") with pytest.raises(SkillCreatorError, match="管理员未配置"): SkillCreatorService()._tool_id() + + +def test_skill_creator_uses_configured_sandbox_region(monkeypatch) -> None: + monkeypatch.setenv("AGENTKIT_SANDBOX_REGION", "cn-shanghai") + tool = SimpleNamespace( + tool_type="CodeEnv", + status="Ready", + envs=[ + SimpleNamespace(key="CODEX_API_KEY", value=os.urandom(24).hex()), + SimpleNamespace(key="CODEX_BASE_URL", value=_MODEL_BASE_URL), + ], + ) + with patch("veadk.cli.frontend_skill_creator.AgentkitToolsClient") as client_class: + client_class.return_value.get_tool.return_value = tool + + SkillCreatorService(tool_id="tool-id")._validate_tool("tool-id") + + client_class.assert_called_once_with(region="cn-shanghai") diff --git a/tests/cli/test_studio_deploy_target.py b/tests/cli/test_studio_deploy_target.py index 348e60d0..46f60125 100644 --- a/tests/cli/test_studio_deploy_target.py +++ b/tests/cli/test_studio_deploy_target.py @@ -230,6 +230,7 @@ def register_callback_for_user_pool_client(self, **kwargs: object) -> None: assert "VEADK_STUDIO_DEVELOPERS" not in veadk_environments assert veadk_environments["SANDBOX_CHAT_CODEX"] == "chat-code-env-id" assert veadk_environments["SANDBOX_SKILL_CREATOR"] == "skill-code-env-id" + assert veadk_environments["AGENTKIT_SANDBOX_REGION"] == expected_region assert credential_tool_ids == [ "chat-code-env-id", "skill-code-env-id", diff --git a/tests/cli/test_studio_update.py b/tests/cli/test_studio_update.py index 08e7b887..fd82733b 100644 --- a/tests/cli/test_studio_update.py +++ b/tests/cli/test_studio_update.py @@ -269,7 +269,7 @@ def update_application_code_bundle(self, **kwargs: object) -> str: assert isinstance(update, dict) assert update["application_id"] == "app-id" assert update["function_id"] == "function-app-id" - assert update["environment_overrides"] is None + assert update["environment_overrides"] == {"AGENTKIT_SANDBOX_REGION": "cn-beijing"} def test_studio_update_rejects_ambiguous_name_before_build( @@ -416,7 +416,10 @@ def update_application_code_bundle(self, **kwargs: object) -> str: assert search["project"] == "default" update = captured["update"] assert isinstance(update, dict) - assert update["environment_overrides"] == {"VEADK_SITE_TITLE": "新标题"} + assert update["environment_overrides"] == { + "AGENTKIT_SANDBOX_REGION": "cn-beijing", + "VEADK_SITE_TITLE": "新标题", + } def test_studio_update_only_overrides_explicit_sandbox_tool_id( @@ -470,7 +473,10 @@ def update_application_code_bundle(self, **kwargs: object) -> str: ) assert result.exit_code == 0, result.output - assert captured["environment_overrides"] == {"SANDBOX_CHAT_CODEX": "chat-tool-new"} + assert captured["environment_overrides"] == { + "AGENTKIT_SANDBOX_REGION": "cn-beijing", + "SANDBOX_CHAT_CODEX": "chat-tool-new", + } def test_update_application_code_bundle_merges_only_explicit_environment( diff --git a/veadk/cli/cli_frontend.py b/veadk/cli/cli_frontend.py index 95d5a8c0..59b65679 100644 --- a/veadk/cli/cli_frontend.py +++ b/veadk/cli/cli_frontend.py @@ -3788,6 +3788,7 @@ def frontend_deploy( veadk_environments["VEADK_STUDIO_DEVELOPERS"] = studio_developers veadk_environments["SANDBOX_CHAT_CODEX"] = chat_codex_tool_id veadk_environments["SANDBOX_SKILL_CREATOR"] = skill_creator_tool_id + veadk_environments["AGENTKIT_SANDBOX_REGION"] = region if client_secret: veadk_environments["OAUTH2_CLIENT_SECRET"] = client_secret @@ -4061,7 +4062,7 @@ def frontend_update( region=target.region, project_name=target.project, ) - environment_overrides = {} + environment_overrides = {"AGENTKIT_SANDBOX_REGION": target.region} if branding_title is not None: environment_overrides["VEADK_SITE_TITLE"] = branding_title if sandbox_chat_codex_tool_id is not None: diff --git a/veadk/cli/frontend_skill_creator.py b/veadk/cli/frontend_skill_creator.py index 97927523..bf4e7f5e 100644 --- a/veadk/cli/frontend_skill_creator.py +++ b/veadk/cli/frontend_skill_creator.py @@ -756,8 +756,11 @@ def ensure_skill_creator_model_credential( class SkillCreatorService: """Coordinate A/B Skill generation in independent AgentKit sandboxes.""" - def __init__(self, tool_id: str | None = None) -> None: + def __init__(self, tool_id: str | None = None, region: str | None = None) -> None: self._configured_tool_id = (tool_id or "").strip() + self._region = ( + region or os.getenv("AGENTKIT_SANDBOX_REGION") or _REGION + ).strip() def capabilities(self) -> dict[str, Any]: """Return fixed model capabilities without exposing server credentials.""" @@ -766,7 +769,7 @@ def capabilities(self) -> dict[str, Any]: reason = "管理员未配置" if tool_id: try: - tool = AgentkitToolsClient(region=_REGION).get_tool( + tool = AgentkitToolsClient(region=self._region).get_tool( tools_types.GetToolRequest(ToolId=tool_id) ) envs = {item.key: item.value for item in tool.envs or []} @@ -964,7 +967,7 @@ def publish( _ensure_bucket_ready( bucket_name=bucket, prefix=prefix, - region=_REGION, + region=self._region, auto_bucket=not bool(configured_bucket), assume_yes=True, assume_no=False, @@ -977,10 +980,10 @@ def publish( str(archive_path), name, temp_dir ) tos_url = _tos_upload( - hashed_path, bucket, prefix, _REGION, verify_bucket=False + hashed_path, bucket, prefix, self._region, verify_bucket=False ) - client = AgentkitSkillsClient(region=_REGION) + client = AgentkitSkillsClient(region=self._region) effective_project = ( project_name or os.getenv("VEADK_SKILL_CREATOR_PROJECT_NAME") or None ) @@ -1076,7 +1079,7 @@ def _create_candidate( request: str, ) -> dict[str, str]: del label - client = AgentkitToolsClient(region=_REGION) + client = AgentkitToolsClient(region=self._region) session_id = self._session_id(job_id, candidate_id) session_envs = build_exec_session_envs( model_name=model, @@ -1198,7 +1201,7 @@ def _candidate_status( return result def _find_session(self, tool_id: str, user_session_id: str) -> dict[str, str]: - response = AgentkitToolsClient(region=_REGION).list_sessions( + response = AgentkitToolsClient(region=self._region).list_sessions( tools_types.ListSessionsRequest( ToolId=tool_id, MaxResults=10, @@ -1227,7 +1230,7 @@ def _delete_instances( failed = 0 for tool_id, instance_id in instances: try: - AgentkitToolsClient(region=_REGION).delete_session( + AgentkitToolsClient(region=self._region).delete_session( tools_types.DeleteSessionRequest( ToolId=tool_id, SessionId=instance_id ) @@ -1239,7 +1242,7 @@ def _delete_instances( def _validate_tool(self, tool_id: str) -> str: try: - tool = AgentkitToolsClient(region=_REGION).get_tool( + tool = AgentkitToolsClient(region=self._region).get_tool( tools_types.GetToolRequest(ToolId=tool_id) ) except Exception as error: