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
4 changes: 3 additions & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ server that `veadk frontend` launches — no separate backend.
- **Runtime management**: inspect or delete deployed runtimes, or connect one
directly so the global Agent selector switches to that Runtime. The cloud
selector gives each two-line Runtime row explicit connect and info actions;
the info action opens a tabbed Agent/Runtime panel. Long descriptions, names,
the info action opens a tabbed Agent/Runtime panel. Studio distinguishes its
own ownership checks from Agent Server compatibility and authentication
failures when a connection cannot be established. Long descriptions, names,
component summaries, IDs, and environment values stay inside the scrollable panel.
- **Custom-agent workbench**: configure an agent with a rich Markdown
system-prompt editor (including heading and list shortcuts), then debug with
Expand Down
47 changes: 44 additions & 3 deletions frontend/src/adk/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,33 @@ export async function listApps(): Promise<string[]> {
/** A runtime exists but the current identity is not allowed to use it. */
export class RuntimeAccessDeniedError extends Error {
constructor() {
super("你没有权限访问该 Runtime,请刷新列表后重试。");
super("当前账号无权访问该 Runtime,请刷新列表或重新登录后重试。");
this.name = "RuntimeAccessDeniedError";
}
}

/** The Runtime is visible to Studio, but its Agent Server cannot be probed. */
export class RuntimeProbeError extends Error {
constructor(
message: string,
readonly unsupported = false,
) {
super(message);
this.name = "RuntimeProbeError";
}
}

async function runtimeProxyErrorCode(response: Response): Promise<string> {
try {
const payload = (await response.clone().json()) as {
detail?: unknown;
};
return typeof payload.detail === "string" ? payload.detail : "";
} catch {
return "";
}
}

/** List the apps a remote AgentKit server exposes (also validates URL + key).
* Pass `ep` to probe via the runtime proxy instead of a raw base+key. */
export async function fetchRemoteApps(
Expand All @@ -251,9 +273,23 @@ export async function fetchRemoteApps(
ep?: AdkEndpoint,
): Promise<string[]> {
const res = await apiFetch(`/list-apps`, {}, ep ?? { base, apiKey });
if ((res.status === 403 || res.status === 404) && ep?.runtimeId) {
const runtimeErrorCode = ep?.runtimeId
? await runtimeProxyErrorCode(res)
: "";
if (ep?.runtimeId && runtimeErrorCode === "runtime_access_denied") {
throw new RuntimeAccessDeniedError();
}
if (ep?.runtimeId && res.status === 404) {
throw new RuntimeProbeError(
"该 Runtime 的 Agent Server 未提供连接接口,请确认 Runtime 已就绪且版本兼容。",
true,
);
}
if (ep?.runtimeId && (res.status === 401 || res.status === 403)) {
throw new RuntimeProbeError(
"Runtime 服务拒绝了连接请求,请检查 Runtime 的鉴权配置。",
);
}
if (!res.ok) {
throw new Error(await httpErrorMessage(res, "读取 Agent 列表失败"));
}
Expand Down Expand Up @@ -1028,7 +1064,12 @@ export async function probeRuntimeApps(
const res = await fetchRemoteApps("", "", { runtimeId, region });
return res;
} catch (error) {
if (error instanceof RuntimeAccessDeniedError) throw error;
if (
error instanceof RuntimeAccessDeniedError ||
error instanceof RuntimeProbeError
) {
throw error;
}
return null;
}
}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ body {
border-color: hsl(8 46% 70%);
background: hsl(8 55% 96%);
}
.agent-row-lead { width: 16px; height: 16px; color: hsl(var(--muted-foreground)); flex-shrink: 0; }
.agent-row-lead { width: 16px; height: 16px; color: hsl(var(--muted-foreground)); flex-shrink: 0; transform: translateY(3px); }
.agent-row--connected .agent-row-lead { color: hsl(142 48% 38%); }
.agent-row-name {
flex: 1;
Expand All @@ -268,8 +268,8 @@ body {
font-weight: 550;
line-height: 1.5;
}
.agent-row-chev { width: 15px; height: 15px; color: hsl(var(--muted-foreground)); transition: transform 0.15s; flex-shrink: 0; }
.agent-row-chev.open { transform: rotate(90deg); }
.agent-row-chev { width: 15px; height: 15px; color: hsl(var(--muted-foreground)); transition: transform 0.15s; flex-shrink: 0; transform: translateY(1px); }
.agent-row-chev.open { transform: translateY(1px) rotate(90deg); }

/* The drawer positions two independent floating cards: the Runtime list and,
* when requested, a tabbed information panel. */
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/ui/AgentSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getRuntimeDetail,
getRuntimes,
RuntimeAccessDeniedError,
RuntimeProbeError,
type AgentInfo,
type CloudRuntime,
type RuntimeScope,
Expand Down Expand Up @@ -308,6 +309,13 @@ export function AgentSelector({
setError(error.message);
return;
}
if (error instanceof RuntimeProbeError) {
if (error.unsupported) {
setUnsupported((current) => new Set(current).add(rt.runtimeId));
}
setError(error.message);
return;
}
setUnsupported((s) => new Set(s).add(rt.runtimeId));
})
.finally(() => setConnecting(null));
Expand Down
13 changes: 11 additions & 2 deletions frontend/tests/studioAccess.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,21 @@ test("runtime selection obeys the server-granted scope", () => {
});

test("runtime authorization failures are not reported as unsupported", () => {
assert.match(clientSource, /res\.status === 403 \|\| res\.status === 404/);
assert.match(clientSource, /if \(error instanceof RuntimeAccessDeniedError\) throw error/);
assert.match(clientSource, /response\.clone\(\)\.json\(\)/);
assert.match(clientSource, /runtime_access_denied/);
assert.match(clientSource, /res\.status === 404[\s\S]*?RuntimeProbeError/);
assert.match(clientSource, /res\.status === 401 \|\| res\.status === 403/);
assert.match(clientSource, /error instanceof RuntimeAccessDeniedError \|\|[\s\S]*?error instanceof RuntimeProbeError/);
assert.match(selectorSource, /error instanceof RuntimeAccessDeniedError[\s\S]*?setError\(error\.message\)/);
assert.match(selectorSource, /error instanceof RuntimeProbeError[\s\S]*?setError\(error\.message\)/);
assert.match(connectionsSource, /removeRuntimeConnection\(runtimeId\)/);
});

test("selected Agent icons are optically aligned with the label", () => {
assert.match(stylesSource, /\.agent-row-lead[^}]*transform: translateY\(3px\)/);
assert.match(stylesSource, /\.agent-row-chev\.open[^}]*translateY\(1px\) rotate\(90deg\)/);
});

test("deployment and management requests rely on server identity, not author input", () => {
assert.doesNotMatch(clientSource, /author: opts\?\.author/);
assert.doesNotMatch(clientSource, /my-runtimes\?author=/);
Expand Down
11 changes: 5 additions & 6 deletions tests/cli/test_studio_rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,13 +388,12 @@ def delete_runtime(_self: Any, request: Any) -> None:
).status_code
== 404
)
assert (
client.get(
"/web/runtime-proxy/runtime-other/list-apps?region=cn-beijing",
headers=developer_headers,
).status_code
== 404
proxy_forbidden = client.get(
"/web/runtime-proxy/runtime-other/list-apps?region=cn-beijing",
headers=developer_headers,
)
assert proxy_forbidden.status_code == 404
assert proxy_forbidden.json()["detail"] == "runtime_access_denied"
assert (
client.post(
"/web/delete-runtime",
Expand Down
17 changes: 15 additions & 2 deletions veadk/cli/cli_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2507,13 +2507,21 @@ def _authorized_runtime(
region: str,
*,
managed_only: bool = False,
coded_access_error: bool = False,
) -> Any:
principal = _current_principal(request)
role = _request_role(request)
runtime = _get_runtime(runtime_id, region)
tags = _runtime_tags(runtime)
if role != StudioRole.ADMIN and not runtime_belongs_to(tags, principal):
raise HTTPException(status_code=404, detail="Runtime not found")
raise HTTPException(
status_code=404,
detail=(
"runtime_access_denied"
if coded_access_error
else "Runtime not found"
),
)
if managed_only and tags.get("veadk:managed") != "true":
raise HTTPException(status_code=404, detail="Runtime not found")
return runtime
Expand Down Expand Up @@ -2858,7 +2866,12 @@ async def _runtime_proxy(runtime_id: str, path: str, request: Request):
"""
region = request.query_params.get("region", "cn-beijing")
try:
runtime = _authorized_runtime(request, runtime_id, region)
runtime = _authorized_runtime(
request,
runtime_id,
region,
coded_access_error=True,
)
endpoint, apikey, auth_type = _resolve_runtime_conn(
runtime_id,
region,
Expand Down

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading