Skip to content

Commit 9167129

Browse files
authored
feat(studio): add Agent publication review with Runtime tags (#1104)
* feat(studio): add Agent publication review with runtime tags * test(studio): avoid Agent review test module name collisions * fix(studio): simplify Agent review tags and repair card layout
1 parent c166b59 commit 9167129

96 files changed

Lines changed: 3104 additions & 754 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,44 @@ See [deployment and operation](service/studio_release_notifier/README.md).
1212

1313
## Features
1414

15+
- **Agent publication review**: Developers deploy privately and apply from an
16+
Agent card. The review center's Agent tab lets administrators inspect the
17+
submitted Runtime metadata, approve with an optional comment, or return with
18+
a required reason. Administrators can also publish directly. The applicant
19+
sees the reviewer name/avatar, decision time, comment and return reason
20+
21+
Agent review is independent of SkillSpaces. Runtime `TagResources` writes
22+
`veadk:visibility` and explicit `veadk:review:*` fields for application ID,
23+
status, submission time/message, reviewer ID/name, decision time/reason/comment,
24+
and withdrawal/unpublication actors and times. Agent details are read live from
25+
the Runtime; applicant identity reuses its `veadk:owner` and `veadk:author` tags
26+
instead of storing another snapshot. Display profiles resolve through the
27+
configured Identity user pool. Cloud error bodies and request IDs are
28+
returned intact. The repository uses provider-scoped clients for Volcengine
29+
and BytePlus
30+
31+
Only an approved Runtime tagged enterprise-visible is shared. Other users can
32+
use it through the server proxy and access their own conversations; management,
33+
logs, credentials and other users' sessions remain restricted. Pending Agents
34+
must be withdrawn before editing/deleting; published Agents must be unpublished
35+
first. Unpublishing revokes subsequent shared proxy requests, including when
36+
connection credentials were cached. An already running stream is not terminated
37+
38+
This first iteration stores the latest application on each Runtime and
39+
replaces it on resubmission. Review covers name, description, model and Runtime
40+
configuration metadata, not source files or automatic scoring. A configuration
41+
fingerprint rejects approval if the submitted Runtime has changed. Version
42+
upgrades, public-version selection and archived application history are deferred.
43+
Studio guards do not prevent direct cloud changes; concurrent decisions are
44+
serialized within one process, without a cross-replica transaction. The record
45+
limits application messages to 20 characters and decision reasons/comments to
46+
256 characters. Text unsupported by cloud tags is encoded per field, splitting
47+
long values into numbered continuations. Every tag value fits within 256 bytes;
48+
continuation tags are written first, then field heads and visibility together
49+
within the 20-tag call limit. Writes are read back before reporting success
50+
and reject exceeding the 50-tag Runtime quota. Existing packed applications
51+
remain readable; new writes use explicit fields
52+
1553
- **Skill publication requests**: Each personal Skill version can be submitted
1654
from its action row. Studio copies its archive into an independent Skill in
1755
`studio_review_space`, preserving the original name and writing the signed-in
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Agent visibility requests, independently stored on Runtime resources."""
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Limit shared Agent access to conversation routes and the caller's sessions."""
16+
17+
import re
18+
from typing import Any
19+
from urllib.parse import unquote
20+
21+
from fastapi import HTTPException, Request
22+
23+
24+
async def authorize_shared_proxy(
25+
request: Request, path: str, method: str, principal: Any
26+
) -> None:
27+
if principal is None:
28+
raise HTTPException(401, "Studio identity is required")
29+
normalized = unquote(path).strip("/")
30+
if (
31+
"%" in normalized
32+
or "\\" in normalized
33+
or any(part in {".", ".."} for part in normalized.split("/"))
34+
):
35+
raise HTTPException(403, "Shared Agent route is not allowed")
36+
if method in {"GET", "HEAD"} and (
37+
normalized == "list-apps" or re.fullmatch(r"web/agent-info/[^/]+", normalized)
38+
):
39+
return
40+
session = re.fullmatch(
41+
r"apps/[^/]+/users/([^/]+)/sessions(?:/[^/]+(?:/.*)?)?", normalized
42+
)
43+
if session and session[1].casefold() in principal.identifiers:
44+
return
45+
if normalized in {"run", "run_sse"} and method == "POST":
46+
try:
47+
payload = await request.json()
48+
except ValueError as error:
49+
raise HTTPException(400, "Invalid conversation request") from error
50+
if isinstance(payload, dict):
51+
user_ids = [payload[key] for key in ("user_id", "userId") if key in payload]
52+
if user_ids and all(
53+
isinstance(value, str) and value.casefold() in principal.identifiers
54+
for value in user_ids
55+
):
56+
return
57+
raise HTTPException(403, "Shared Agent access is limited to your own conversations")
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Resolve review actors through the deployment's existing Identity directory."""
16+
17+
from typing import Any
18+
from urllib.parse import urlsplit
19+
20+
from frontend.server.user_management.errors import UserManagementError
21+
22+
23+
def resolve_profile(directory: Any, person: dict[str, str]) -> dict[str, str]:
24+
uid = person.get("identityUid", "")
25+
if not directory:
26+
return person
27+
import volcenginesdkid as sdk
28+
29+
try:
30+
if not uid:
31+
# Deployment tags contain the trusted subject, not necessarily GetUser's UID
32+
matches = [
33+
user.uid
34+
for user in directory.users()
35+
if person.get("id") in {user.subject, user.uid}
36+
]
37+
if len(matches) != 1:
38+
return person
39+
uid = matches[0]
40+
user = directory._call(
41+
"get_user",
42+
sdk.GetUserRequest(user_pool_uid=directory.pool_uid, user_uid=uid),
43+
)
44+
except UserManagementError:
45+
# A removed directory user must not erase the persisted audit identity
46+
return person
47+
if str(getattr(user, "uid", "")) != uid:
48+
return person
49+
picture = str(getattr(user, "picture", "") or "")
50+
try:
51+
parsed = urlsplit(picture)
52+
if (
53+
parsed.scheme not in {"http", "https"}
54+
or not parsed.hostname
55+
or parsed.username
56+
or parsed.password
57+
):
58+
picture = ""
59+
except ValueError:
60+
picture = ""
61+
name = next(
62+
(
63+
str(getattr(user, key))
64+
for key in ("name", "preferred_username", "nickname", "email")
65+
if getattr(user, key, None)
66+
),
67+
person["name"],
68+
)
69+
return {
70+
**person,
71+
"name": name,
72+
"email": str(getattr(user, "email", "") or ""),
73+
"avatarUrl": picture,
74+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Provider-scoped Runtime transport for Agent review metadata."""
16+
17+
from __future__ import annotations
18+
19+
from collections.abc import Callable, Iterator
20+
from typing import Any
21+
22+
from agentkit.sdk.runtime import types as sdk
23+
from agentkit.sdk.runtime.client import AgentkitRuntimeClient
24+
25+
from frontend.server.agentkit_clients import create_agentkit_client
26+
from frontend.server.storage import StudioProvider
27+
28+
from .tags import write_runtime_tags
29+
30+
31+
class AgentReviewRepository:
32+
def __init__(
33+
self,
34+
provider: StudioProvider,
35+
credentials: Callable[[], tuple[str, str, str | None]],
36+
):
37+
self.provider: StudioProvider = provider
38+
self.credentials = credentials
39+
40+
def client(self, region: str) -> Any:
41+
access_key, secret_key, token = self.credentials()
42+
return create_agentkit_client(
43+
AgentkitRuntimeClient,
44+
provider=self.provider,
45+
access_key=access_key,
46+
secret_key=secret_key,
47+
session_token=token or "",
48+
region=region,
49+
)
50+
51+
def get(self, region: str, runtime_id: str) -> Any:
52+
return self.client(region).get_runtime(
53+
sdk.GetRuntimeRequest.model_validate({"RuntimeId": runtime_id})
54+
)
55+
56+
def list(self, region: str) -> Iterator[Any]:
57+
client = self.client(region)
58+
token = ""
59+
seen: set[str] = set()
60+
while True:
61+
response = client.list_runtimes(
62+
sdk.ListRuntimesRequest.model_validate(
63+
{"MaxResults": 100, **({"NextToken": token} if token else {})}
64+
)
65+
)
66+
yield from response.agent_kit_runtimes or []
67+
token = str(response.next_token or "")
68+
if not token:
69+
return
70+
if token in seen:
71+
raise RuntimeError("AgentKit returned a repeated Runtime page token")
72+
seen.add(token)
73+
74+
def write(self, region: str, runtime_id: str, values: dict[str, str]) -> None:
75+
write_runtime_tags(self.client(region), runtime_id, values)

0 commit comments

Comments
 (0)