From e72cc9e22fa835417cb6e673191e1ae4094483e1 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Fri, 25 Sep 2026 19:49:59 +0000 Subject: [PATCH 1/3] Show a saved Agent's model provider and a dash for a missing harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An Agent's detail page now has a Model provider section when the Agent saved x_agents_core.model_provider: its protocol (Anthropic Messages or OpenAI Responses), base URL, whether an API key is configured, and the context window and max output tokens when Core reports them. The read view has no key and the page never shows one. A saved Agent can have a provider and no harness, so the Agents list and the detail show "—" for a missing harness instead of "Core default". The e2e fixture adds two Production Agents, one with only a provider and one with a harness and a provider, appended after the seeded data so the rest of the demo is unchanged. One acceptance test checks that the project's Agents load, the provider-only Agent's harness reads "—", its provider section shows every field, and no key appears. --- apps/web/PRODUCT.md | 4 +++- apps/web/e2e/data/agents.mjs | 24 +++++++++++++++++-- apps/web/e2e/data/routes.mjs | 8 ++++++- apps/web/e2e/resources.spec.ts | 16 +++++++++++++ apps/web/src/features/agents/AgentCatalog.css | 5 ++++ apps/web/src/features/agents/AgentsPage.tsx | 23 +++++++++++++++--- apps/web/src/i18n/locales/en/agents.ts | 11 +++++++++ apps/web/src/i18n/locales/zh-CN/agents.ts | 11 +++++++++ 8 files changed, 95 insertions(+), 7 deletions(-) diff --git a/apps/web/PRODUCT.md b/apps/web/PRODUCT.md index ec979796d..d2838c838 100644 --- a/apps/web/PRODUCT.md +++ b/apps/web/PRODUCT.md @@ -122,7 +122,9 @@ workbench. Sessions, send input or cancel work. Deletion follows the public deletion rules; a busy Session is not deletable and the console never cancels work to make it so. - **Secrets stay write-only.** Credential tokens, Template environment variables and - setup commands are never returned, to the administrator included. + setup commands are never returned, to the administrator included. An Agent's saved + model provider shows its protocol, base URL, limits and whether a key is configured, + never the key. - **Creators.** Core records the key behind every write. The console shows the creating key of each asset and a project's write history; an asset an administrator copied in an earlier release shows as Admin copy and an asset diff --git a/apps/web/e2e/data/agents.mjs b/apps/web/e2e/data/agents.mjs index 14f77edc9..db6b40bff 100644 --- a/apps/web/e2e/data/agents.mjs +++ b/apps/web/e2e/data/agents.mjs @@ -84,8 +84,28 @@ export const agentDefinitions = [ }, ]; -/** Which project owns each definition (index into the demo projects). */ -export const agentProject = [0, 1, 2, 0, 1, 0, 1, 1, 2, 3]; +/** + * Saved Agents with a model provider, as Core reads them: whether a key is saved, + * never the key. One saved only a provider, so it has no harness. No Session uses + * them, so the demo Sessions stay the same. + */ +export const providerAgentDefinitions = [ + { + id: "agent_11c4f2a8", name: "Spec drafter", model: "MiniMax-M2", reasoning: {}, + instructions: "Draft a product spec from the linked discussion: problem, users, scope, open questions.", + tools: [], metadata: { team: "product" }, + x_agents_core: { model_provider: { protocol: "anthropic", base_url: "https://api.minimaxi.com/anthropic", api_key_configured: true, context_window: 204800, max_output_tokens: 65536 } }, + }, + { + id: "agent_12d7e3b9", name: "Research assistant", model: "kimi-k2", reasoning: {}, + instructions: "Answer research questions with cited sources and a one-paragraph summary.", + tools: [], metadata: { team: "research" }, + x_agents_core: { harness: "codex", model_provider: { protocol: "responses", base_url: "https://api.moonshot.cn/v1", api_key_configured: true } }, + }, +]; + +/** Which project owns each definition (index into the demo projects); the provider Agents come last. */ +export const agentProject = [0, 1, 2, 0, 1, 0, 1, 1, 2, 3, 0, 0]; /** Prompt and reply pairs per Agent so Session transcripts read like real work. */ export const agentConversations = { diff --git a/apps/web/e2e/data/routes.mjs b/apps/web/e2e/data/routes.mjs index e031b2c17..6f540bf13 100644 --- a/apps/web/e2e/data/routes.mjs +++ b/apps/web/e2e/data/routes.mjs @@ -1,5 +1,5 @@ // Synthetic deployment data for the browser acceptance fixture: Sessions, Turns, Items, nodes. -import { agentConversations, agentDefinitions } from "./agents.mjs"; +import { agentConversations, agentDefinitions, providerAgentDefinitions } from "./agents.mjs"; let seed = 42; const rand = () => ((seed = (seed * 1664525 + 1013904223) % 4294967296) / 4294967296); const pick = (list) => list[Math.floor(rand() * list.length)]; @@ -99,5 +99,11 @@ export function buildDemo(now = Math.floor(Date.now() / 1000)) { const sleeping = allocation.compute_phase === "suspended"; return { ...base, environment_id: session.environment.id, mode: "openai_hosted", provider_type: "docker", instance: { kind: "managed_allocation", allocation_id: allocation.id, device_id: null, connection_generation: null }, lifecycle_state: sleeping ? "sleeping" : "active", status: "observed", reason: null, allocation_created_at: session.created_at, observed_at: now - 2, started_at: session.created_at, cpu: sleeping ? null : { usage_seconds_total: 600 + Math.floor(rand() * 4000), capacity_cores: 2, usage_cores: Number((rand() * 1.6).toFixed(2)), utilization_ratio: null }, memory: sleeping ? null : { usage_bytes: Math.floor((0.4 + rand() * 1.4) * 2 ** 30), limit_bytes: 2 * 2 ** 30 } }; }); + // Added after everything else is generated, so the seeded data above does not change. + agents.push(...providerAgentDefinitions.map((definition, index) => ({ + ...definition, object: "agent", multi_agent: { enabled: false, max_concurrent_subagents: null }, service_tier: "auto", + text: { format: { type: "text" }, verbosity: "medium" }, + created_at: now - 86400 * (3 + index), updated_at: now - 1800 * (index + 1), + }))); return { agents, sessions, turns, items, nodes, allocations, observations }; } diff --git a/apps/web/e2e/resources.spec.ts b/apps/web/e2e/resources.spec.ts index 7a79621bf..02b605741 100644 --- a/apps/web/e2e/resources.spec.ts +++ b/apps/web/e2e/resources.spec.ts @@ -26,3 +26,19 @@ test("goes back to the page a resource was opened from", async ({ page, request await page.getByRole("button", { name: "Back", exact: true }).first().click(); await expect(page.getByRole("heading", { name: "Report builder", level: 1 })).toBeVisible(); }); + +test("reads an Agent saved with a model provider and no harness, without its key", async ({ page, request }) => { + // Production holds Spec drafter, saved with only an x_agents_core.model_provider. + await openConsole(page, request, "agents?project=proj_7f3a91c2"); + const open = page.getByRole("button", { name: "Open Spec drafter" }); + await expect(open).toBeVisible(); + await expect(page.getByText(/Could not load/)).toHaveCount(0); + // With one project shown, the cells are model, harness, … + await expect(page.getByRole("row").filter({ has: open }).getByRole("cell").nth(1)).toHaveText("—"); + + await open.click(); + await expect(page.getByRole("heading", { name: "Spec drafter", level: 1 })).toBeVisible(); + const provider = page.getByRole("region", { name: "Model provider" }); + for (const text of ["Anthropic Messages", "https://api.minimaxi.com/anthropic", "Configured", "204,800", "65,536"]) await expect(provider).toContainText(text); + await expect(page.locator("body")).not.toContainText(/api_key|\bsk-/); +}); diff --git a/apps/web/src/features/agents/AgentCatalog.css b/apps/web/src/features/agents/AgentCatalog.css index 84f70b95e..09c84c86a 100644 --- a/apps/web/src/features/agents/AgentCatalog.css +++ b/apps/web/src/features/agents/AgentCatalog.css @@ -12,3 +12,8 @@ text-overflow: ellipsis; white-space: nowrap; } + +/* A provider's base URL takes two grid columns so it stays on one line, like an ID. */ +.resource-facts > .agent-provider-url { + grid-column: span 2; +} diff --git a/apps/web/src/features/agents/AgentsPage.tsx b/apps/web/src/features/agents/AgentsPage.tsx index dba41b4c0..bf17e9297 100644 --- a/apps/web/src/features/agents/AgentsPage.tsx +++ b/apps/web/src/features/agents/AgentsPage.tsx @@ -1,4 +1,4 @@ -import type { CoreHarnessKind, SavedAgent } from "@agents-core-web/agents-client"; +import type { CoreHarnessKind, ModelProviderView, SavedAgent } from "@agents-core-web/agents-client"; import { ArrowLeft, Bot, ListTree, Trash2 } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -24,6 +24,9 @@ export function harnessLabel(harness: CoreHarnessKind): string { return "Codex"; } +/** Provider protocols by their product names, the same in every language. */ +const protocolNames: Record = { anthropic: "Anthropic Messages", responses: "OpenAI Responses" }; + function coverage(summary: ProjectSummary | undefined, locale?: string): string { if (!summary || summary.coverage.sessions === 0) return MISSING; return formatPercent(summary.coverage.reported / summary.coverage.sessions, locale); @@ -113,7 +116,7 @@ function AgentsList() { {showProject ? : null} {agent.model} - {harness ? harnessLabel(harness) : t("view.coreDefault")} + {harness ? harnessLabel(harness) : MISSING} {formatInteger(agent.tools.length, locale)} {summary?.last_active_at ? formatRelative(summary.last_active_at, now, locale) : MISSING} @@ -201,6 +204,7 @@ function AgentDetail({ projectId, agentId }: { projectId: string; agentId: strin const name = agent ? agent.name || t("catalog.untitled") : agentId; const tools = agent ? toolRows(agent) : []; const metadata = agent ? Object.entries(agent.metadata ?? {}) : []; + const provider = agent?.x_agents_core?.model_provider; return (
{t("view.id")}
{tCommon("project.column")}
{t("view.columns.model")}
{agent.model}
-
{t("view.columns.harness")}
{agent.x_agents_core?.harness ? harnessLabel(agent.x_agents_core.harness) : t("view.coreDefault")}
+
{t("view.columns.harness")}
{agent.x_agents_core?.harness ? harnessLabel(agent.x_agents_core.harness) : MISSING}
{t("view.created")}
{formatDateTime(agent.created_at, locale)}
{t("view.updated")}
{formatDateTime(agent.updated_at, locale)}
@@ -268,6 +272,19 @@ function AgentDetail({ projectId, agentId }: { projectId: string; agentId: strin ) :

{t("view.noTools")}

}
+ {provider ? ( +
+ {/* The read view has no key, only whether one is saved. */} +
+
{t("view.provider.protocol")}
{protocolNames[provider.protocol]}
+
{t("view.provider.baseUrl")}
{provider.base_url}
+
{t("view.provider.apiKey")}
{provider.api_key_configured ? t("view.provider.keyConfigured") : t("view.provider.keyNotConfigured")}
+ {provider.context_window !== undefined ?
{t("view.provider.contextWindow")}
{formatInteger(provider.context_window, locale)}
: null} + {provider.max_output_tokens !== undefined ?
{t("view.provider.maxOutputTokens")}
{formatInteger(provider.max_output_tokens, locale)}
: null} +
+
+ ) : null} +
{t("view.reasoningEffort")}
{agent.reasoning?.effort ?? t("view.coreDefault")}
diff --git a/apps/web/src/i18n/locales/en/agents.ts b/apps/web/src/i18n/locales/en/agents.ts index d673dfb0e..1d7a8af1d 100644 --- a/apps/web/src/i18n/locales/en/agents.ts +++ b/apps/web/src/i18n/locales/en/agents.ts @@ -37,6 +37,17 @@ export const agents = { textFormat: "Text format", verbosity: "Verbosity", serviceTier: "Service tier", + provider: { + title: "Model provider", + help: "Saved on this Agent. Its Sessions use it unless their request sets a provider. Core never returns the API key.", + protocol: "Protocol", + baseUrl: "Base URL", + apiKey: "API key", + keyConfigured: "Configured", + keyNotConfigured: "Not configured", + contextWindow: "Context window", + maxOutputTokens: "Max output tokens", + }, metadata: "Metadata", noMetadata: "No metadata", usage: "Usage", diff --git a/apps/web/src/i18n/locales/zh-CN/agents.ts b/apps/web/src/i18n/locales/zh-CN/agents.ts index 349779c07..11b402c81 100644 --- a/apps/web/src/i18n/locales/zh-CN/agents.ts +++ b/apps/web/src/i18n/locales/zh-CN/agents.ts @@ -30,6 +30,17 @@ export const agents: TranslationShape = { textFormat: "文本格式", verbosity: "详细程度", serviceTier: "服务层级", + provider: { + title: "模型服务", + help: "保存在此 Agent 上。它的 Session 使用这里的模型服务,除非请求中另外指定。Core 从不返回 API key。", + protocol: "协议", + baseUrl: "Base URL", + apiKey: "API key", + keyConfigured: "已配置", + keyNotConfigured: "未配置", + contextWindow: "上下文窗口", + maxOutputTokens: "最大输出 token", + }, metadata: "元数据", noMetadata: "没有元数据", usage: "用量", From f803bc49db528bd4b0e2bc6e6b3385ee05e77c08 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Fri, 25 Sep 2026 19:49:59 +0000 Subject: [PATCH 2/3] Mention the Agent page's model provider in the Web protocol coverage --- docs/web/protocol-coverage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/web/protocol-coverage.md b/docs/web/protocol-coverage.md index 4fc7c1c20..166607a12 100644 --- a/docs/web/protocol-coverage.md +++ b/docs/web/protocol-coverage.md @@ -52,7 +52,7 @@ here. Archived projects remain readable. | Resource | Reads used | Deletion | Creator | Console surface | | --- | --- | --- | --- | --- | -| Agents | `/agents`, `/agents/{agent_id}` | Agent | `agent` | Agents list with usage per Agent; Agent page with instructions, tools, generation settings and metadata | +| Agents | `/agents`, `/agents/{agent_id}` | Agent | `agent` | Agents list with usage per Agent; Agent page with instructions, tools, the saved model provider (never its key), generation settings and metadata | | Environment templates | `/environment-templates`, `/environment-templates/{id}` | Template | `environment_template` | Templates list; Template page with every safe section | | Skills | `/skills`, `/skills/{skill_id}`, `/skills/{skill_id}/versions`, Skill and version `/content` | Skill and Skill version | `skill` (list) | Skills list; Skill page with versions and archive downloads | | Files | `/files` | File | `file` | Files list (metadata only) | From 950196903fb6997d07f464b8c3d2d3e0ff814250 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Fri, 25 Sep 2026 19:51:46 +0000 Subject: [PATCH 3/3] Show Core default again for an Agent without a saved harness An Agent that saved no harness runs on the deployment's default harness, so "Core default" is accurate, and it matches how the page shows unset generation settings. The acceptance test now expects it for the provider-only Agent. --- apps/web/e2e/resources.spec.ts | 2 +- apps/web/src/features/agents/AgentsPage.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/e2e/resources.spec.ts b/apps/web/e2e/resources.spec.ts index 02b605741..0de4af8a1 100644 --- a/apps/web/e2e/resources.spec.ts +++ b/apps/web/e2e/resources.spec.ts @@ -34,7 +34,7 @@ test("reads an Agent saved with a model provider and no harness, without its key await expect(open).toBeVisible(); await expect(page.getByText(/Could not load/)).toHaveCount(0); // With one project shown, the cells are model, harness, … - await expect(page.getByRole("row").filter({ has: open }).getByRole("cell").nth(1)).toHaveText("—"); + await expect(page.getByRole("row").filter({ has: open }).getByRole("cell").nth(1)).toHaveText("Core default"); await open.click(); await expect(page.getByRole("heading", { name: "Spec drafter", level: 1 })).toBeVisible(); diff --git a/apps/web/src/features/agents/AgentsPage.tsx b/apps/web/src/features/agents/AgentsPage.tsx index bf17e9297..44be05396 100644 --- a/apps/web/src/features/agents/AgentsPage.tsx +++ b/apps/web/src/features/agents/AgentsPage.tsx @@ -116,7 +116,7 @@ function AgentsList() { {showProject ? : null} {agent.model} - {harness ? harnessLabel(harness) : MISSING} + {harness ? harnessLabel(harness) : t("view.coreDefault")} {formatInteger(agent.tools.length, locale)} {summary?.last_active_at ? formatRelative(summary.last_active_at, now, locale) : MISSING} @@ -238,7 +238,7 @@ function AgentDetail({ projectId, agentId }: { projectId: string; agentId: strin
{t("view.id")}
{tCommon("project.column")}
{t("view.columns.model")}
{agent.model}
-
{t("view.columns.harness")}
{agent.x_agents_core?.harness ? harnessLabel(agent.x_agents_core.harness) : MISSING}
+
{t("view.columns.harness")}
{agent.x_agents_core?.harness ? harnessLabel(agent.x_agents_core.harness) : t("view.coreDefault")}
{t("view.created")}
{formatDateTime(agent.created_at, locale)}
{t("view.updated")}
{formatDateTime(agent.updated_at, locale)}