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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,15 @@ 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; 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
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 <app-name>`: build the frontend from a
local VeADK source checkout and release it through the existing VeFaaS
Expand Down
34 changes: 19 additions & 15 deletions docs/content/docs/framework/frontend.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -204,11 +204,10 @@ export SANDBOX_SKILL_CREATOR=<skill-code-env-tool-id>
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 |
| :-- | :-- | :-- |
Expand All @@ -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.
Expand Down Expand Up @@ -276,7 +276,11 @@ 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. 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:
Expand Down
104 changes: 65 additions & 39 deletions tests/cli/test_frontend_skill_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -101,15 +101,15 @@ 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"}

with (
patch.object(
service,
"_validate_tool",
return_value=_RELAY_URL,
return_value=_MODEL_BASE_URL,
),
patch.object(service, "_create_candidate", side_effect=create_candidate),
):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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] = {}

Expand Down Expand Up @@ -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",
)

Expand All @@ -412,21 +420,21 @@ 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"
),
],
)
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")
Expand Down Expand Up @@ -457,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")
5 changes: 3 additions & 2 deletions tests/cli/test_studio_deploy_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
],
)
Expand Down Expand Up @@ -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",
Expand Down
46 changes: 41 additions & 5 deletions tests/cli/test_studio_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -507,6 +513,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,
Expand Down
Loading
Loading