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
6 changes: 6 additions & 0 deletions .github/workflows/harness-sidecar-release-gate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ on:
- 'tests/cli/test_generated_agent_sidecar_runtime.py'
- 'tests/cli/test_generated_agent_mcp.py'
- 'tests/cli/test_studio_rbac.py'
- 'tests/cli/test_studio_a2a_deployment.py'
- 'tests/cli/test_studio_release.py'
- 'tests/cli/test_studio_update.py'
- 'tests/cli/test_studio_startup_imports.py'
Expand Down Expand Up @@ -50,6 +51,7 @@ on:
- 'tests/cli/test_generated_agent_sidecar_runtime.py'
- 'tests/cli/test_generated_agent_mcp.py'
- 'tests/cli/test_studio_rbac.py'
- 'tests/cli/test_studio_a2a_deployment.py'
- 'tests/cli/test_studio_release.py'
- 'tests/cli/test_studio_update.py'
- 'tests/cli/test_studio_startup_imports.py'
Expand Down Expand Up @@ -134,6 +136,10 @@ jobs:
python -m pytest -q \
tests/frontend/server/test_runtime_iam.py \
tests/cli/test_generated_agent_mcp.py \
tests/cli/test_studio_a2a_deployment.py \
tests/cli/test_studio_rbac.py::test_mcp_deployment_error_details_are_actionable \
tests/cli/test_studio_rbac.py::test_managed_sidecar_runtime_envs_reject_invalid_mcp_state \
tests/cli/test_studio_rbac.py::test_managed_sidecar_runtime_envs_allow_zero_mcp_without_legacy_fallback \
tests/cli/test_generated_agent_backend_codegen_extended.py::test_generated_debug_applies_published_mcp_credential_contract_before_discovery \
tests/cli/test_studio_rbac.py::test_deployment_status_recovers_completed_update_from_fresh_instance \
tests/cli/test_studio_rbac.py::test_new_deployment_creates_requested_instance_range_without_republishing \
Expand Down
9 changes: 8 additions & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,14 @@ See [deployment and operation](service/studio_release_notifier/README.md).
`优化` → `调试` → `环境` → `发布`. On the optimization page, `自定义`
appears first and starts with no components, while `运维场景` applies the
`ops` component combination. Component checkboxes remain editable and an empty
selection keeps Sidecar disabled. The Environment step can optionally add the
selection keeps Sidecar disabled. BytePlus does not show a default warning;
selecting an unsupported Sidecar option still explains the restriction.
A2A center selections configure discovery on their parent Agent and do not
require a local Agent name. They can be retained or added during a standard
Runtime update without enabling Sidecar or configuring MCP. Source-preserving
updates still require the original Agent structure. Deployment validation
reports Agent, Skill, MCP, and source-image problems separately.
The Environment step can optionally add the
official Lark CLI, GitHub CLI, and Pandoc to the cloud runtime. Selecting a
tool generates an inspectable provider-specific Dockerfile with pinned
releases, amd64/arm64 assets, and SHA-256 verification. Advanced configuration
Expand Down
9 changes: 0 additions & 9 deletions frontend/src/create/CustomCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3701,13 +3701,11 @@ function DebugComparisonWorkspace({
function HarnessOptimizationWorkspace({
profile,
optimizations,
unavailableMessage,
onProfileChange,
onOptimizationChange,
}: {
profile: HarnessSidecarProfileId;
optimizations: HarnessSidecarOptionId[];
unavailableMessage?: string | null;
onProfileChange: (profile: HarnessSidecarProfileId) => void;
onOptimizationChange: (
optionId: HarnessSidecarOptionId,
Expand All @@ -3721,12 +3719,6 @@ function HarnessOptimizationWorkspace({
aria-label={t("traditional.optimization.ariaLabel")}
>
<div className="cw-optimize-panel">
{unavailableMessage ? (
<div className="cw-banner" role="alert">
<Info className="cw-i" />
<span>{unavailableMessage}</span>
</div>
) : null}
<fieldset className="cw-optimize-section">
<legend>{t("traditional.optimization.scenario")}</legend>
<RadioGroup<HarnessSidecarProfileId>
Expand Down Expand Up @@ -6604,7 +6596,6 @@ export function CustomCreate({
<HarnessOptimizationWorkspace
profile={harnessOptimizationProfile}
optimizations={harnessOptimizations}
unavailableMessage={harnessProviderNotice}
onProfileChange={updateHarnessOptimizationProfile}
onOptimizationChange={updateHarnessOptimization}
/>
Expand Down
2 changes: 1 addition & 1 deletion frontend/tests/harnessSidecarOptions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ test("fails fast with a clear BytePlus Sidecar notice without blocking ordinary
customCreateSource,
/selected && harnessProviderNotice[\s\S]*?setBuildErr\(harnessProviderNotice\)/,
);
assert.match(
assert.doesNotMatch(
customCreateSource,
/unavailableMessage=\{harnessProviderNotice\}/,
);
Expand Down
230 changes: 230 additions & 0 deletions tests/cli/test_studio_a2a_deployment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Offline regressions for registry-backed A2A deployment with Sidecar off."""

from __future__ import annotations

import copy
import json
from pathlib import Path
from types import SimpleNamespace
from typing import Any

import pytest
import yaml
from fastapi.testclient import TestClient

from tests.cli.test_studio_rbac import (
_create_studio_app,
_runtime,
_runtime_with_public_endpoint,
_RuntimeJsonResponse,
)
from veadk.cli.generated_agent_codegen import AgentDraft, generate_project_from_draft
from veadk.cli.legacy_runtime_recovery import (
LegacyRecoveryError,
apply_source_preserving_edits,
build_sidecar_mcp_servers_json,
mcp_editor_draft_without_credentials,
mcp_reuse_supplied_credentials,
)


def _registry_entry() -> dict[str, Any]:
# Studio's A2A selector configures discovery on its parent, not a named Agent.
return {
"name": "",
"agentType": "a2a",
"a2aRegistry": {"enabled": True, "registrySpaceId": "offline-space"},
"mcpTools": [],
}


def test_a2a_registry_entries_are_not_mcp_agent_identities() -> None:
draft = {"name": "root_agent", "subAgents": [_registry_entry()]}
assert (
mcp_reuse_supplied_credentials(
published_draft=draft,
edited_draft=draft,
published_reference_values={},
reuse_requests=(),
)
== ()
)
assert (
json.loads(build_sidecar_mcp_servers_json(draft=draft, secret_values={})) == []
)


def test_a2a_registry_inactive_mcp_fields_are_still_sanitized() -> None:
entry = _registry_entry()
entry["mcpTools"] = [{"authToken": "offline-inactive-secret"}]
draft = {"name": "root_agent", "subAgents": [entry]}
sanitized = mcp_editor_draft_without_credentials(draft)
assert "offline-inactive-secret" not in json.dumps(sanitized)
assert entry["mcpTools"][0]["authToken"] == "offline-inactive-secret"


def test_a2a_registry_does_not_relax_local_agent_identity_or_source_preservation() -> (
None
):
for child in ({"name": "", "agentType": "llm"}, {"name": "root_agent"}):
with pytest.raises(LegacyRecoveryError, match="agent_identity_invalid"):
mcp_reuse_supplied_credentials(
published_draft={"name": "root_agent"},
edited_draft={"name": "root_agent", "subAgents": [child]},
published_reference_values={},
reuse_requests=(),
)
with pytest.raises(LegacyRecoveryError):
apply_source_preserving_edits(
{"name": "root_agent"},
{"name": "root_agent", "subAgents": [_registry_entry()]},
)


@pytest.mark.parametrize("provider", ["byteplus", "volcengine"])
@pytest.mark.parametrize("operation", ["create", "add-registry", "retain-registry"])
def test_a2a_registry_deployment_without_sidecar(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
provider: str,
operation: str,
) -> None:
from agentkit.sdk.runtime.client import AgentkitRuntimeClient

region = "ap-southeast-1" if provider == "byteplus" else "cn-shanghai"
monkeypatch.setenv("BYTEPLUS_ACCESS_KEY", "test-ak")
monkeypatch.setenv("BYTEPLUS_SECRET_KEY", "test-sk")
monkeypatch.setenv("MODEL_AGENT_API_KEY", "offline-model-key")
monkeypatch.setattr(
"agentkit.utils.template_utils.render_template", lambda _: "offline"
)
monkeypatch.setattr(
"veadk.auth.veauth.ark_veauth.get_ark_token", lambda **_: "offline-model-key"
)
monkeypatch.setattr(
"veadk.cli.cli_frontend._sync_volcengine_runtime_tags", lambda **_: None
)
runtime = _runtime_with_public_endpoint(_runtime("offline-runtime", "developer"))
runtime.current_version_number = 3
runtime.status = "Ready"
runtime.role_name = "runtime-role"
runtime.artifact_url = ""
runtime.envs = []
published: dict[str, Any] = {
"name": "root_agent",
"description": "Offline A2A regression",
"instruction": "Use agent discovery.",
"cloudProvider": provider,
"subAgents": [
{"name": "worker", "instruction": "Help the root.", "subAgents": []}
],
}
if operation == "retain-registry":
published["subAgents"][0]["subAgents"] = [_registry_entry()]
requested = copy.deepcopy(published)
requested["subAgents"][0]["subAgents"] = [_registry_entry()]
captured: list[dict[str, Any]] = []

def get_runtime(_self: Any, request: Any) -> Any:
assert request.runtime_id == runtime.runtime_id
return runtime

monkeypatch.setattr(AgentkitRuntimeClient, "get_runtime", get_runtime)

class RuntimeClient:
def __init__(self, **_kwargs: Any) -> None:
pass

async def __aenter__(self) -> "RuntimeClient":
return self

async def __aexit__(self, *_args: Any) -> None:
pass

async def request(self, _method: str, url: str, **_kwargs: Any) -> Any:
if url.endswith("/list-apps"):
return _RuntimeJsonResponse(["root_agent"])
assert url.endswith("/web/agent-info/root_agent")
return _RuntimeJsonResponse({"name": "root_agent", "draft": published})

monkeypatch.setattr("httpx.AsyncClient", RuntimeClient)

def launch(*, config_file: str, **_kwargs: Any) -> Any:
captured.append(yaml.safe_load(Path(config_file).read_text()))
runtime.current_version_number = 4
return SimpleNamespace(
success=True,
error=None,
deploy_result=SimpleNamespace(
endpoint_url="https://runtime.example.test",
metadata={
"runtime_id": runtime.runtime_id,
"runtime_name": runtime.name,
},
),
)

monkeypatch.setattr("agentkit.toolkit.sdk.launch", launch)
project = generate_project_from_draft(AgentDraft.model_validate(requested))
python_files = [item.content for item in project.files if item.path.endswith(".py")]
assert any("build_a2a_registry_tools(" in content for content in python_files)
for content in python_files:
compile(content, "generated.py", "exec")
app = _create_studio_app(
monkeypatch, tmp_path, developers="developer", provider=provider
)
headers = {"X-VeADK-Local-User": "developer"}
payload: dict[str, Any] = {
"name": "root_agent",
"draft": requested,
"files": [item.model_dump() for item in project.files],
"createEvaluationSets": False,
"config": {"region": region},
"harnessSidecar": {"enabled": False, "componentOverrides": {}},
}
with TestClient(app) as client:
if operation != "create":
capability = client.get(
"/web/runtime-update-capability",
headers=headers,
params={
"runtimeId": runtime.runtime_id,
"region": region,
"appName": "root_agent",
},
)
assert capability.status_code == 200
assert capability.json()["canUpdate"] is True
payload.update(
runtimeId=runtime.runtime_id,
appName="root_agent",
editMode="regenerate",
updateEtag=capability.json()["etag"],
baseRuntimeVersion=3,
)
response = client.post("/web/deploy-agentkit", headers=headers, json=payload)
assert response.status_code == 200, response.text
frames = [
json.loads(line[6:])
for line in response.text.splitlines()
if line.startswith("data: ")
]
assert frames[-1]["success"] is True, frames[-1]
assert len(captured) == 1
runtime_envs = captured[0]["launch_types"]["cloud"]["runtime_envs"]
assert runtime_envs.get("HARNESS_SIDECAR_ENABLED", "false") == "false"
assert "MCP_SERVERS_JSON" not in runtime_envs
8 changes: 7 additions & 1 deletion tests/cli/test_studio_rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ def test_runtime_role_lookup_failure_finishes_deployment_and_allows_retry(
("legacy_mcp_credential_missing", "缺少可用凭证"),
("legacy_mcp_reuse_source_missing", "无法沿用原 MCP 凭证"),
("legacy_platform_mcp_read_only", "Skill 或 MCP 配置已变化"),
("legacy_overlay_agent_identity_invalid", "Agent 名称为空或重复"),
("legacy_overlay_agent_graph_changed", "不支持修改子 Agent 或 A2A 结构"),
("legacy_overlay_skill_materialization_invalid", "Skill 文件"),
("legacy_image_registry_unsupported", "源镜像仓库"),
("unknown_validation_error", "智能体部署配置校验失败"),
],
)
def test_mcp_deployment_error_details_are_actionable(
Expand Down Expand Up @@ -708,7 +713,8 @@ def test_managed_sidecar_runtime_envs_reject_invalid_mcp_state(
{"effectiveComponents": ["mcp_resilience"]},
)

assert error == "Harness Sidecar MCP 配置无效,请检查名称、地址与认证后重试。"
assert error == "MCP 配置无效,请检查名称、地址与认证后重试。"
assert runtime_envs["MCP_SERVERS_JSON"] == raw_mcp
assert "AGENTKIT_HARNESS_RUNTIME_COMMAND" not in runtime_envs


Expand Down
7 changes: 7 additions & 0 deletions tests/test_harness_sidecar_release_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ def test_sidecar_release_gate_runs_backend_and_frontend_in_parallel() -> None:
in backend_run
)
assert "tests/frontend/server/test_runtime_iam.py" in backend_run
assert "tests/cli/test_studio_a2a_deployment.py" in backend_run
assert "test_mcp_deployment_error_details_are_actionable" in backend_run
assert "test_managed_sidecar_runtime_envs_reject_invalid_mcp_state" in backend_run
assert (
"test_managed_sidecar_runtime_envs_allow_zero_mcp_without_legacy_fallback"
in backend_run
)
assert (
"test_new_deployment_creates_requested_instance_range_without_republishing"
in backend_run
Expand Down
24 changes: 22 additions & 2 deletions veadk/cli/cli_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,25 @@


def _mcp_deployment_error_detail(code: str) -> str:
"""Map server-only MCP validation codes to safe, actionable HTTP 409s."""
"""Map deployment validation codes without exposing configuration values."""

if code == "legacy_overlay_agent_identity_invalid":
return "Agent 名称为空或重复,请为每个本地 Agent 填写唯一名称后重试。"
if code == "legacy_overlay_agent_graph_changed":
return (
"当前 Runtime 使用保留源码更新,不支持修改子 Agent 或 A2A 结构。"
"请恢复原结构,或使用完整项目源码重新部署。"
)
if code.startswith("legacy_overlay_agent_graph_"):
return "Agent 结构无效或超出层级、数量限制,请检查子 Agent 配置后重试。"
if code.startswith(
("legacy_overlay_skill", "legacy_skill_", "legacy_runtime_skill")
):
return "Skill 文件或配置无效,请重新选择包含完整文件的 Skill 后重试。"
if code == "legacy_image_registry_unsupported":
return "当前源镜像仓库不支持保留源码更新,请使用完整项目源码重新部署。"
if code.startswith("legacy_image_"):
return "无法确认更新所需的源镜像,请重新打开智能体详情或使用完整项目源码部署。"

if code == "legacy_mcp_name_duplicate":
return "MCP 名称重复,请为每个 HTTP MCP 服务使用唯一名称。"
Expand All @@ -157,7 +175,9 @@ def _mcp_deployment_error_detail(code: str) -> str:
return "运行版本中的 Skill 或 MCP 配置已变化,请重新打开详情并确认最新配置后再更新。"
if code == "legacy_overlay_sidecar_intent_changed":
return "Harness Sidecar 组件选择已变化,请重新打开详情并确认后再更新。"
return "Harness Sidecar MCP 配置无效,请检查名称、地址与认证后重试。"
if code.startswith(("legacy_mcp_", "legacy_overlay_mcp")):
return "MCP 配置无效,请检查名称、地址与认证后重试。"
return "智能体部署配置校验失败,请检查配置后重试。"


def _capture_process_env(keys: Iterable[str]) -> Callable[[], None]:
Expand Down
Loading
Loading