Skip to content
Closed
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
35 changes: 34 additions & 1 deletion apps/web/e2e/first-run.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ test("lets a web-only console use an existing project connection without key man
return route.fulfill({ status: 503, json: { error: { message: "Key management is not paired." } } });
});
await page.goto("/");
await expect(page.getByRole("heading", { name: "Use an existing Agent API key.", exact: true })).toBeVisible();
await expect(page.getByRole("heading", { name: "API key management is not enabled", exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Create API key", exact: true })).toHaveCount(0);
await page.getByRole("button", { name: "I've saved it. Continue", exact: true }).click();
await expect(page.getByRole("heading", { name: "Connect your own machine.", exact: true })).toBeVisible();
Expand All @@ -353,3 +353,36 @@ test("lets a web-only console use an existing project connection without key man
expect(writes[0]?.body).toMatchObject({ model: "fixture/web-only-model" });
expect(keyRequests).toBe(0);
});

test("rechecks key management without sending key requests while disabled", async ({ page }) => {
await mockAccount(page, { mode: "authenticated", username });
let enabled = false;
let keyRequests = 0;
await page.route("**/console/config", (route) => route.fulfill({ json: { api_keys: enabled, sandbox_admin: enabled, node_installer: false } }));
await page.route("**/console/api-keys", (route) => {
keyRequests++;
return route.fulfill({ json: { data: [] } });
});
await page.goto("/");
await expect(page.getByRole("heading", { name: "API key management is not enabled", exact: true })).toBeVisible();
await page.getByRole("button", { name: "Check again", exact: true }).click();
await expect(page.getByRole("heading", { name: "API key management is not enabled", exact: true })).toBeVisible();
expect(keyRequests).toBe(0);
enabled = true;
await page.getByRole("button", { name: "Check again", exact: true }).click();
await expect(page.getByRole("button", { name: "Create API key", exact: true })).toBeEnabled();
await expect(page.getByRole("button", { name: "I've saved it. Continue", exact: true })).toBeDisabled();
expect(keyRequests).toBe(1);
});

test("keeps the API key management page separate from introduction guidance", async ({ page }) => {
await mockAccount(page, { mode: "authenticated", username });
await page.route("**/console/config", (route) => route.fulfill({ json: { api_keys: false, sandbox_admin: false, node_installer: false } }));
await page.goto("/#api-keys");
await expect(page.getByRole("heading", { name: "API key management is not enabled", exact: true })).toBeVisible();
await expect(page.getByText("You can continue the introduction and use your signed-in console connection to create an Agent.", { exact: true })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Check again", exact: true })).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
await expect(page.getByRole("button", { name: "Check again", exact: true })).toBeInViewport();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
});
8 changes: 5 additions & 3 deletions apps/web/src/features/api-keys/ApiKeyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ export function ApiKeyPanel({ onReady }: { onReady?: (ready: boolean) => void })
if (capability === "available") return <ManagedApiKeyPanel onReady={onReady} />;
return <section className="api-key-panel" aria-label={t("API keys")}>
{capability === "unavailable" ? <>
<header><div className="api-key-heading"><KeyRound size={18} strokeWidth={1.5} /><h3>{t("Use an existing Agent API key.")}</h3></div></header>
<p className="api-key-caption">{t("This console cannot create API keys. Use a key supplied by your Core administrator for requests from your machine or application.")}</p>
<p className="api-key-caption">{t("You can continue the introduction and use your signed-in console connection to create an Agent.")}</p>
<header><div className="api-key-heading"><KeyRound size={18} strokeWidth={1.5} /><h3>{t("API key management is not enabled")}</h3></div></header>
<p className="api-key-caption">{t("A deployment administrator needs to enable key management for this console. You can then create, view and revoke keys here.")}</p>
<p className="api-key-caption">{t("If you already have an Agent API key, you can keep using it for requests from your machine or application.")}</p>
{onReady ? <p className="api-key-caption">{t("You can continue the introduction and use your signed-in console connection to create an Agent.")}</p> : null}
<button type="button" className="button outline" onClick={() => setRevision((value) => value + 1)}><RefreshCw size={14} />{t("Check again")}</button>
</> : capability === "loading" ? <p role="status">{t("Checking API key management…")}</p> : <>
<p className="api-key-error" role="alert">{t("Could not check API key management. Try again.")}</p>
<button type="button" className="button outline" onClick={() => setRevision((value) => value + 1)}>{t("Try again")}</button>
Expand Down
16 changes: 11 additions & 5 deletions apps/web/src/features/api-keys/api-keys.css
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
.api-key-panel { padding: 26px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); }
.api-key-panel { min-width: 0; padding: 20px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); }
.api-key-heading { display: flex; align-items: center; gap: 10px; }
.api-key-heading h3 { font-size: 16px; font-weight: 550; letter-spacing: -.02em; margin: 0; }
.api-key-panel header p, .api-key-caption { font-size: 12px; line-height: 1.7; color: var(--fg-muted); }
.api-key-create { display: flex; gap: 12px; align-items: end; margin: 22px 0; }
.api-key-create .field { flex: 1; }
.api-key-create .field { flex: 1; min-width: 0; }
.api-key-create .field span { font-size: 12px; }
.api-key-create input { min-height: 36px; }
.api-key-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.api-key-secret { display: grid; gap: 14px; padding: 20px; margin: 20px 0; background: var(--surface-subtle); border: 1px solid var(--line-strong); border-radius: 10px; }
.api-key-secret { display: grid; min-width: 0; gap: 14px; padding: 16px; margin: 20px 0; background: var(--surface-subtle); border: 1px solid var(--line-strong); border-radius: 6px; }
.api-key-secret strong { font-size: 13px; font-weight: 500; }
.api-key-secret input { width: 100%; padding: 12px; border: 1px solid var(--line); border-radius: 6px; font-family: var(--font-mono); font-size: 12px; color: var(--fg); background: var(--surface); }
.api-key-list-heading { display: flex; align-items: center; justify-content: space-between; font-size: 12px; margin-top: 20px; color: var(--fg-muted); }
.api-key-list { padding: 0; margin: 0; list-style: none; }
.api-key-list > li { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 10px; padding: 14px 0; border-top: 1px solid var(--line); }
.api-key-list strong { font-size: 13px; display: block; font-weight: 500; }
.api-key-list strong { font-size: 13px; display: block; font-weight: 500; overflow-wrap: anywhere; }
.api-key-list span { display: block; font-size: 11px; color: var(--fg-muted); margin-top: 5px; }
.api-key-confirm { flex-basis: 100%; font-size: 12px; }
.api-key-error { color: var(--danger); font-size: 12px; line-height: 1.7; }
.api-keys-page { max-width: 920px; margin: 0 auto; }
.api-keys-page { width: 100%; overflow: auto; }
.api-keys-page > .api-key-panel { margin: 24px; }
@media (max-width: 640px) {
.api-key-panel { padding: 16px; }
.api-keys-page > .api-key-panel { margin: 16px; }
.api-key-create { flex-direction: column; align-items: stretch; }
}
6 changes: 4 additions & 2 deletions apps/web/src/lib/api-key-strings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export const apiKeyChinese = {
"My API key": "我的 API 密钥",
"Use an existing Agent API key.": "使用已有的 Agent API 密钥。",
"This console cannot create API keys. Use a key supplied by your Core administrator for requests from your machine or application.": "此控制台无法创建 API 密钥。从本机或应用发起请求时,请使用 Core 管理员提供的密钥。",
"API key management is not enabled": "API 密钥管理尚未启用",
"A deployment administrator needs to enable key management for this console. You can then create, view and revoke keys here.": "需要部署管理员为此控制台启用密钥管理,之后即可在这里创建、查看和撤销密钥。",
"If you already have an Agent API key, you can keep using it for requests from your machine or application.": "如果你已有 Agent API 密钥,仍可使用它从本机或应用发起请求。",
"Check again": "重新检查",
"You can continue the introduction and use your signed-in console connection to create an Agent.": "你可以继续导览,并通过已登录的控制台连接创建 Agent。",
"Checking API key management…": "正在检查 API 密钥管理功能…",
"Could not check API key management. Try again.": "暂时无法检查 API 密钥管理功能,请重试。",
Expand Down
5 changes: 5 additions & 0 deletions docs/getting-started/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ Neither is the administrator password. Examples read environment variables or as
for keys privately in the terminal, and reject HTTP redirects. Keep the generated Core API key for later requests from your own machine.
A Web-only installation without paired key management guides you to use an
existing Core API key and still allows the request workbench.
If **API key management is not enabled** appears, configure the paired
administrator access described below, restart the console, sign in again if
prompted, and select **Check again**. The administrator account alone does not
enable deployment management. Existing caller keys remain valid; the page never
asks you to paste a deployment administrator key into the browser.
Creating a saved Agent stores its configuration; it does not start a Session or
call the model. The full protocol surface and execution support are documented
in the [API guide](./quickstart.md).
Expand Down