Skip to content

Commit 216405d

Browse files
fix(studio): bind sandbox model credential directly (#720)
* fix(studio): bind sandbox model credential directly * fix(studio): use application control plane for Shanghai * fix(studio): use sandbox deployment region
1 parent 3f75ce9 commit 216405d

8 files changed

Lines changed: 187 additions & 149 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,15 @@ VeADK provides several useful command line tools for faster deployment and optim
205205
policies; target `cn-beijing` (default) or `cn-shanghai` with
206206
`--region`, automatically locate the Identity user pool across Beijing and
207207
Shanghai, and select the VeFaaS project with `--project` (default `default`);
208+
Shanghai Functions, gateways, and AgentKit resources stay in Shanghai while
209+
VeFaaS Application operations use its Beijing control-plane endpoint; the
210+
selected region is also used for temporary-chat and Skill-creation sessions;
208211
custom local or remote logo images are bundled into the deployment; the
209212
deployed client skips the second OAuth consent confirmation after login;
210213
two dedicated AgentKit CodeEnv Tools are created automatically for temporary
211214
chats and Skill creation unless their IDs are supplied with
212215
`--sandbox-chat-codex-tool-id` and `--sandbox-skill-creator-tool-id`; Tool or
213-
credential-relay provisioning failures print the underlying error verbatim
216+
model-credential provisioning failures print the underlying error verbatim
214217
after credential values are redacted
215218
- `veadk studio update --vefaas-app-name <app-name>`: build the frontend from a
216219
local VeADK source checkout and release it through the existing VeFaaS

‎docs/content/docs/framework/frontend.en.mdx‎

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,11 @@ The new-chat **Skill creation** mode runs
180180
Ark Responses. Each candidate gets an independent AgentKit CodeEnv session.
181181
The server validates the generated directory name, `SKILL.md` frontmatter,
182182
file count, size, and safe paths before packaging a ZIP. AgentKit credential
183-
hosting keeps the real Ark API key; Studio and Sandbox receive only its
184-
revocable gateway ticket. Per-job Session variables cannot override that
185-
hosted ticket, and Studio accepts only Volcengine HTTPS credential relay URLs.
186-
Model credentials are never returned to the browser, and the Skill creation
187-
APIs require the Studio developer or admin role.
183+
configuration resolves the Ark API key with the deployer's AK/SK and writes it
184+
directly to each dedicated CodeEnv Tool. Per-job Session variables cannot
185+
override the Tool's model credential. Model credentials are never returned to
186+
the browser, and the Skill creation APIs require the Studio developer or admin
187+
role.
188188

189189
After submission, both candidate conversations appear immediately and
190190
independently render public reasoning summaries, tool calls, and assistant
@@ -204,11 +204,10 @@ export SANDBOX_SKILL_CREATOR=<skill-code-env-tool-id>
204204
veadk frontend --agents-dir examples
205205
```
206206

207-
For local Studio, run the AgentKit `credential-hosting` command and choose to
208-
write its result into both dedicated CodeEnv Tools. Adding a
209-
candidate to AgentKit uploads its ZIP to TOS, then creates or updates the Skill
210-
through the Skills API. It is published to a SkillSpace only when the user
211-
provides SkillSpace IDs.
207+
For local Studio, configure the Ark model variables on both dedicated CodeEnv
208+
Tools. Adding a candidate to AgentKit uploads its ZIP to TOS, then creates or
209+
updates the Skill through the Skills API. It is published to a SkillSpace only
210+
when the user provides SkillSpace IDs.
212211

213212
| Environment variable | Default | Description |
214213
| :-- | :-- | :-- |
@@ -226,11 +225,12 @@ requests reach different instances.
226225
On the first cloud deployment, the command creates two independent CodeEnv
227226
Tools with the deployer's AK/SK when Tool IDs are omitted. Existing Tools can be
228227
selected with `--sandbox-chat-codex-tool-id` and
229-
`--sandbox-skill-creator-tool-id`. AgentKit credential hosting stores the Ark
230-
credential in KMS and binds only the relay URL and revocable ticket to both
231-
Tools. The VeFaaS Function receives only Tool IDs, never model credentials:
228+
`--sandbox-skill-creator-tool-id`. The Ark credential is resolved with the
229+
deployer's AK/SK and written directly to both Tools as model environment
230+
variables. Sandbox sessions inherit those Tool variables. The browser and
231+
VeFaaS Function receive only Tool IDs, never model credentials.
232232

233-
If Tool or credential-relay provisioning fails, the CLI prints the underlying
233+
If Tool or model-credential provisioning fails, the CLI prints the underlying
234234
error verbatim, including its original line breaks, after redacting credential
235235
values. Use the service action, error code, request ID, and request details to
236236
identify the missing permission or account resource.
@@ -276,7 +276,11 @@ veadk studio deploy \
276276
also supports `cn-shanghai`. Deployment checks the deployment region first and
277277
then searches the Beijing and Shanghai Identity regions. A cross-region match
278278
emits a warning and continues. `--project` selects the VeFaaS function project
279-
and defaults to `default`.
279+
and defaults to `default`. For Shanghai deployments, the Function, APIG, and
280+
AgentKit resources remain in Shanghai while VeFaaS Application operations use
281+
the service's Beijing control-plane endpoint. The deployed Function records the
282+
selected AgentKit Sandbox region so temporary chat and Skill-creation sessions
283+
query the same region as their CodeEnv Tools.
280284

281285
To update an existing Studio deployment, run this command from a VeADK source
282286
checkout:

‎tests/cli/test_frontend_skill_creator.py‎

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
mount_skill_creator_routes,
4141
)
4242

43-
_RELAY_URL = "https://test.apigateway-cn-beijing.volceapi.com/api/v3"
43+
_MODEL_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
4444

4545

4646
def _skill_zip(name: str = "weather-report") -> bytes:
@@ -101,15 +101,15 @@ def create_candidate(
101101
request: str,
102102
) -> dict[str, str]:
103103
del tool_id, job_id, label, request
104-
assert model_base_url == _RELAY_URL
104+
assert model_base_url == _MODEL_BASE_URL
105105
calls.append((candidate_id, model))
106106
return {"instanceId": f"instance-{candidate_id}", "endpoint": "endpoint"}
107107

108108
with (
109109
patch.object(
110110
service,
111111
"_validate_tool",
112-
return_value=_RELAY_URL,
112+
return_value=_MODEL_BASE_URL,
113113
),
114114
patch.object(service, "_create_candidate", side_effect=create_candidate),
115115
):
@@ -275,7 +275,7 @@ def create_candidate(*args: object, **kwargs: object) -> dict[str, str]:
275275
patch.object(
276276
service,
277277
"_validate_tool",
278-
return_value=_RELAY_URL,
278+
return_value=_MODEL_BASE_URL,
279279
),
280280
patch.object(service, "_create_candidate", side_effect=create_candidate),
281281
patch.object(service, "_delete_instances") as delete_instances,
@@ -329,50 +329,58 @@ def test_archive_metadata_rejects_symlink_entry() -> None:
329329
SkillCreatorService(tool_id="tool-id")._archive_metadata(output.getvalue())
330330

331331

332-
def test_credential_hosting_is_bound_to_tool_without_raw_key() -> None:
332+
def test_model_credential_is_bound_directly_to_tool() -> None:
333+
access_key = os.urandom(16).hex()
334+
model_api_key = os.urandom(24).hex()
335+
secret_key = os.urandom(24).hex()
336+
calls: list[tuple[str, dict[str, object]]] = []
337+
333338
class FakeApi:
334-
def call(self, *_args: object, **_kwargs: object) -> dict[str, object]:
335-
return {
336-
"Tool": {
337-
"Envs": [
338-
{"Key": "CODEX_API_KEY", "Value": "raw-key"},
339-
{"Key": "CODEX_BASE_URL", "Value": "https://ark.example"},
340-
]
339+
def call(
340+
self,
341+
_service: str,
342+
action: str,
343+
_version: str,
344+
body: dict[str, object],
345+
) -> dict[str, object]:
346+
calls.append((action, body))
347+
if action == "GetTool":
348+
return {
349+
"Tool": {"Envs": [{"Key": "EXISTING_ENV", "Value": "preserved"}]}
341350
}
342-
}
351+
return {}
343352

344-
updates: dict[str, str] = {}
345353
with (
346354
patch("agentkit.auth._openapi.OpenApiClient", return_value=FakeApi()),
347355
patch(
348-
"agentkit.auth.credential_hosting.list_gateways",
349-
return_value=[{"id": "gateway-id", "name": "agentkit-credhost-gw"}],
350-
),
351-
patch("veadk.auth.veauth.ark_veauth.get_ark_token", return_value="raw-key"),
352-
patch(
353-
"agentkit.auth.credential_hosting.host_model_key",
354-
return_value=SimpleNamespace(
355-
ticket="ck-hosted-ticket",
356-
model_base_url=_RELAY_URL,
357-
),
358-
),
359-
patch(
360-
"agentkit.auth.credential_hosting.set_tool_env",
361-
side_effect=lambda _api, _tool_id, values: updates.update(values),
362-
),
356+
"veadk.auth.veauth.ark_veauth.get_ark_token",
357+
return_value=model_api_key,
358+
) as get_ark_token,
363359
):
364360
ensure_skill_creator_model_credential(
365361
tool_id="tool-id",
366-
access_key="access-key",
367-
secret_key="secret-key",
362+
access_key=access_key,
363+
secret_key=secret_key,
368364
)
369365

370-
assert updates["CODEX_API_KEY"] == "ck-hosted-ticket"
371-
assert updates["CODEX_BASE_URL"] == _RELAY_URL
372-
assert "raw-key" not in updates.values()
366+
get_ark_token.assert_called_once_with(
367+
region="cn-beijing",
368+
access_key=access_key,
369+
secret_key=secret_key,
370+
session_token=None,
371+
)
372+
assert [action for action, _ in calls] == ["GetTool", "UpdateTool"]
373+
update_body = calls[1][1]
374+
envs = {
375+
item["Key"]: item["Value"]
376+
for item in cast(list[dict[str, str]], update_body["Envs"])
377+
}
378+
assert envs["EXISTING_ENV"] == "preserved"
379+
assert envs["CODEX_API_KEY"] == model_api_key
380+
assert envs["CODEX_BASE_URL"] == _MODEL_BASE_URL
373381

374382

375-
def test_candidate_session_never_overrides_hosted_tool_ticket(monkeypatch) -> None:
383+
def test_candidate_session_never_overrides_tool_model_credential(monkeypatch) -> None:
376384
service = SkillCreatorService(tool_id="tool-id")
377385
captured: dict[str, object] = {}
378386

@@ -400,7 +408,7 @@ def delete_session(self, _request: object) -> None:
400408
"a",
401409
"doubao-seed-2-0-pro-260215",
402410
"豆包 Seed 2.0 Pro",
403-
_RELAY_URL,
411+
_MODEL_BASE_URL,
404412
"Create a release notes Skill",
405413
)
406414

@@ -412,21 +420,21 @@ def delete_session(self, _request: object) -> None:
412420
)
413421

414422

415-
def test_tool_rejects_untrusted_credential_relay_url() -> None:
423+
def test_tool_rejects_untrusted_model_base_url() -> None:
416424
service = SkillCreatorService(tool_id="tool-id")
417425
tool = SimpleNamespace(
418426
tool_type="CodeEnv",
419427
status="Ready",
420428
envs=[
421-
SimpleNamespace(key="CODEX_API_KEY", value="ck-hosted-ticket"),
429+
SimpleNamespace(key="CODEX_API_KEY", value=os.urandom(24).hex()),
422430
SimpleNamespace(
423431
key="CODEX_BASE_URL", value="http://attacker.invalid/api/v3"
424432
),
425433
],
426434
)
427435
with (
428436
patch("veadk.cli.frontend_skill_creator.AgentkitToolsClient") as client_class,
429-
pytest.raises(SkillCreatorError, match="中继地址无效"),
437+
pytest.raises(SkillCreatorError, match="模型服务地址无效"),
430438
):
431439
client_class.return_value.get_tool.return_value = tool
432440
service._validate_tool("tool-id")
@@ -457,3 +465,21 @@ def test_skill_creator_reads_only_dedicated_sandbox_tool_env(monkeypatch) -> Non
457465
monkeypatch.delenv("SANDBOX_SKILL_CREATOR")
458466
with pytest.raises(SkillCreatorError, match="管理员未配置"):
459467
SkillCreatorService()._tool_id()
468+
469+
470+
def test_skill_creator_uses_configured_sandbox_region(monkeypatch) -> None:
471+
monkeypatch.setenv("AGENTKIT_SANDBOX_REGION", "cn-shanghai")
472+
tool = SimpleNamespace(
473+
tool_type="CodeEnv",
474+
status="Ready",
475+
envs=[
476+
SimpleNamespace(key="CODEX_API_KEY", value=os.urandom(24).hex()),
477+
SimpleNamespace(key="CODEX_BASE_URL", value=_MODEL_BASE_URL),
478+
],
479+
)
480+
with patch("veadk.cli.frontend_skill_creator.AgentkitToolsClient") as client_class:
481+
client_class.return_value.get_tool.return_value = tool
482+
483+
SkillCreatorService(tool_id="tool-id")._validate_tool("tool-id")
484+
485+
client_class.assert_called_once_with(region="cn-shanghai")

‎tests/cli/test_studio_deploy_target.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ def _skip_serverless_role_setup(monkeypatch: pytest.MonkeyPatch) -> None:
5050
[
5151
("tool", "Failed to provision the AgentKit chat CodeEnv Tool"),
5252
(
53-
"relay",
54-
"Failed to provision the AgentKit chat model credential relay",
53+
"credential",
54+
"Failed to provision the AgentKit chat model credential",
5555
),
5656
],
5757
)
@@ -230,6 +230,7 @@ def register_callback_for_user_pool_client(self, **kwargs: object) -> None:
230230
assert "VEADK_STUDIO_DEVELOPERS" not in veadk_environments
231231
assert veadk_environments["SANDBOX_CHAT_CODEX"] == "chat-code-env-id"
232232
assert veadk_environments["SANDBOX_SKILL_CREATOR"] == "skill-code-env-id"
233+
assert veadk_environments["AGENTKIT_SANDBOX_REGION"] == expected_region
233234
assert credential_tool_ids == [
234235
"chat-code-env-id",
235236
"skill-code-env-id",

‎tests/cli/test_studio_update.py‎

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def _get_function(self, _: object) -> SimpleNamespace:
147147
]
148148

149149

150-
def test_list_applications_uses_client_region(
150+
def test_list_applications_uses_application_control_plane_region(
151151
monkeypatch: pytest.MonkeyPatch,
152152
) -> None:
153153
requested_regions: list[str] = []
@@ -163,7 +163,7 @@ def _request(**kwargs: object) -> dict[str, object]:
163163
monkeypatch.setattr("veadk.integrations.ve_faas.ve_faas.ve_request", _request)
164164

165165
assert service._list_application(app_name="studio-app") == []
166-
assert requested_regions == ["cn-shanghai"]
166+
assert requested_regions == ["cn-beijing"]
167167

168168

169169
def test_load_deployed_site_logo_uses_current_branding_url(
@@ -269,7 +269,7 @@ def update_application_code_bundle(self, **kwargs: object) -> str:
269269
assert isinstance(update, dict)
270270
assert update["application_id"] == "app-id"
271271
assert update["function_id"] == "function-app-id"
272-
assert update["environment_overrides"] is None
272+
assert update["environment_overrides"] == {"AGENTKIT_SANDBOX_REGION": "cn-beijing"}
273273

274274

275275
def test_studio_update_rejects_ambiguous_name_before_build(
@@ -416,7 +416,10 @@ def update_application_code_bundle(self, **kwargs: object) -> str:
416416
assert search["project"] == "default"
417417
update = captured["update"]
418418
assert isinstance(update, dict)
419-
assert update["environment_overrides"] == {"VEADK_SITE_TITLE": "新标题"}
419+
assert update["environment_overrides"] == {
420+
"AGENTKIT_SANDBOX_REGION": "cn-beijing",
421+
"VEADK_SITE_TITLE": "新标题",
422+
}
420423

421424

422425
def test_studio_update_only_overrides_explicit_sandbox_tool_id(
@@ -470,7 +473,10 @@ def update_application_code_bundle(self, **kwargs: object) -> str:
470473
)
471474

472475
assert result.exit_code == 0, result.output
473-
assert captured["environment_overrides"] == {"SANDBOX_CHAT_CODEX": "chat-tool-new"}
476+
assert captured["environment_overrides"] == {
477+
"AGENTKIT_SANDBOX_REGION": "cn-beijing",
478+
"SANDBOX_CHAT_CODEX": "chat-tool-new",
479+
}
474480

475481

476482
def test_update_application_code_bundle_merges_only_explicit_environment(
@@ -507,6 +513,36 @@ def test_update_application_code_bundle_merges_only_explicit_environment(
507513
}
508514

509515

516+
def test_application_control_plane_uses_beijing_for_shanghai_deployment(
517+
monkeypatch: pytest.MonkeyPatch,
518+
) -> None:
519+
calls: list[tuple[str, str]] = []
520+
service = object.__new__(VeFaaS)
521+
cast(Any, service).ak = "ak"
522+
cast(Any, service).sk = "sk"
523+
cast(Any, service).region = "cn-shanghai"
524+
525+
def _ve_request(**kwargs: object) -> dict[str, object]:
526+
action = cast(str, kwargs["action"])
527+
region = cast(str, kwargs["region"])
528+
calls.append((action, region))
529+
if action == "GetApplication":
530+
return {"Result": {"Status": "create_success"}}
531+
return {"Result": {"Items": [], "Total": 0}}
532+
533+
monkeypatch.setattr("veadk.integrations.ve_faas.ve_faas.ve_request", _ve_request)
534+
535+
status, _ = service._get_application_status("application-id")
536+
applications = service._list_application(app_name="studio-app")
537+
538+
assert status == "create_success"
539+
assert applications == []
540+
assert calls == [
541+
("GetApplication", "cn-beijing"),
542+
("ListApplications", "cn-beijing"),
543+
]
544+
545+
510546
def test_update_application_code_bundle_preserves_unspecified_sandbox_tool(
511547
monkeypatch: pytest.MonkeyPatch,
512548
tmp_path: Path,

0 commit comments

Comments
 (0)