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 apps/web/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 22 additions & 2 deletions apps/web/e2e/data/agents.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
8 changes: 7 additions & 1 deletion apps/web/e2e/data/routes.mjs
Original file line number Diff line number Diff line change
@@ -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)];
Expand Down Expand Up @@ -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 };
}
16 changes: 16 additions & 0 deletions apps/web/e2e/resources.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("Core default");

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-/);
});
5 changes: 5 additions & 0 deletions apps/web/src/features/agents/AgentCatalog.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
19 changes: 18 additions & 1 deletion apps/web/src/features/agents/AgentsPage.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<ModelProviderView["protocol"], string> = { 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);
Expand Down Expand Up @@ -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 (
<section className="page-section console-page agents-list-page" aria-labelledby="agent-detail-heading">
<PageHeader
Expand Down Expand Up @@ -268,6 +272,19 @@ function AgentDetail({ projectId, agentId }: { projectId: string; agentId: strin
) : <p className="table-muted">{t("view.noTools")}</p>}
</Section>

{provider ? (
<Section headingId="agent-provider-heading" title={t("view.provider.title")} help={t("view.provider.help")}>
{/* The read view has no key, only whether one is saved. */}
<dl className="resource-facts">
<div><dt>{t("view.provider.protocol")}</dt><dd>{protocolNames[provider.protocol]}</dd></div>
<div className="agent-provider-url"><dt>{t("view.provider.baseUrl")}</dt><dd><code>{provider.base_url}</code></dd></div>
<div><dt>{t("view.provider.apiKey")}</dt><dd>{provider.api_key_configured ? t("view.provider.keyConfigured") : t("view.provider.keyNotConfigured")}</dd></div>
{provider.context_window !== undefined ? <div><dt>{t("view.provider.contextWindow")}</dt><dd>{formatInteger(provider.context_window, locale)}</dd></div> : null}
{provider.max_output_tokens !== undefined ? <div><dt>{t("view.provider.maxOutputTokens")}</dt><dd>{formatInteger(provider.max_output_tokens, locale)}</dd></div> : null}
</dl>
</Section>
) : null}

<Section headingId="agent-generation-heading" title={t("view.generation")}>
<dl className="resource-facts">
<div><dt>{t("view.reasoningEffort")}</dt><dd>{agent.reasoning?.effort ?? t("view.coreDefault")}</dd></div>
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/i18n/locales/en/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/i18n/locales/zh-CN/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ export const agents: TranslationShape<typeof english> = {
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: "用量",
Expand Down
2 changes: 1 addition & 1 deletion docs/web/protocol-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down