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
5 changes: 5 additions & 0 deletions .changeset/agent-key-bearer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudhal/api": minor
---

Agent keys are also accepted as `Authorization: Bearer <key>` (what most MCP clients and Codex's `bearer_token_env_var` send), on `/mcp` and wherever `x-api-key` was. `/api/admin/*` keeps its deployment-token bearer and never reads it as an agent key.
36 changes: 6 additions & 30 deletions apps/studio/src/components/CreateAgentKeyDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,23 @@ import {
Input,
InsetPanel,
LayerDialog,
Tabs,
Text,
} from "@cloudhal/ui";
import { CodeHighlighted } from "@cloudhal/ui/code";
import { ArrowCounterClockwise, Info, Warning } from "@phosphor-icons/react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { listOf, useT } from "../i18n/index.tsx";
import {
type GrantableScope,
mcpSnippet,
SCOPE_GROUPS,
type ScopeGroup,
type SnippetKind,
sameScopes,
scopeOptions,
} from "../lib/agents.ts";
import { api, type CreatedAgentKey, errorCode, type KeyMode } from "../lib/api.ts";
import { apiErrorText } from "../lib/api-errors.ts";
import { AutoModeConfirm, BypassHint, ModeBadge, ModePicker } from "./AgentModes.tsx";
import { McpClientSetup } from "./McpClientSetup.tsx";
import { useAgentModeRules } from "./useAgentModeRules.ts";

type Step = "mode" | "details";
Expand Down Expand Up @@ -61,7 +58,6 @@ export function CreateAgentKeyDialog({
const [advancedOpen, setAdvancedOpen] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [created, setCreated] = useState<CreatedAgentKey | undefined>();
const [snippet, setSnippet] = useState<SnippetKind>("json");

const modeScopes = rules.scopesFor(mode);
const customised = !sameScopes(scopes, modeScopes);
Expand Down Expand Up @@ -89,7 +85,6 @@ export function CreateAgentKeyDialog({
setScopes(rules.scopesFor(rules.defaultMode));
setAdvancedOpen(false);
setSubmitted(false);
setSnippet("json");
create.reset();
} else {
setCreated(undefined);
Expand Down Expand Up @@ -160,30 +155,11 @@ export function CreateAgentKeyDialog({
tooltip={{ text: t("common.copy"), copiedText: t("common.copied") }}
/>
</div>
<div className="grid min-w-0 gap-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<Text size="sm" bold as="span">
{t("agents.reveal.config")}
</Text>
<Tabs
variant="segmented"
size="sm"
value={snippet}
onValueChange={(v) => setSnippet(v as SnippetKind)}
tabs={(["json", "claude"] as const).map((k) => ({
value: k,
label: t(`agents.connect.tabs.${k}`),
}))}
/>
</div>
<div className="min-w-0 overflow-x-auto">
<CodeHighlighted
code={mcpSnippet(snippet, endpoint, created.key)}
lang={snippet === "json" ? "json" : "bash"}
showCopyButton
/>
</div>
</div>
<McpClientSetup
endpoint={endpoint}
agentKey={created.key}
label={t("agents.reveal.config")}
/>
</div>
) : step === "mode" ? (
<div className="grid gap-5" data-testid="create-step-mode">
Expand Down
48 changes: 48 additions & 0 deletions apps/studio/src/components/McpClientSetup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Select, Text } from "@cloudhal/ui";
import { CodeHighlighted } from "@cloudhal/ui/code";
import { useState } from "react";
import { useT } from "../i18n/index.tsx";
import { type McpClient, mcpSnippet, REMOTE_CLIENTS, snippetLang } from "../lib/agents.ts";

/**
* Ready-to-copy setup for one MCP client at a time (docs/agents/clients.md): a client picker, the
* snippet for this deployment's `/mcp`, and where it goes. With `agentKey` (the one-time reveal)
* the key is filled in; `local` adds the stdio server for the person's own machine.
*/
export function McpClientSetup({
endpoint,
agentKey,
local = false,
label,
}: {
endpoint: string;
agentKey?: string;
local?: boolean;
/** The picker's label; "Client" by default. */
label?: string;
}) {
const t = useT();
const [client, setClient] = useState<McpClient>("claude");
const clients: McpClient[] = local ? [...REMOTE_CLIENTS, "stdio"] : [...REMOTE_CLIENTS];
return (
<div className="grid min-w-0 gap-3" data-testid="mcp-client-setup" data-client={client}>
<Select
className="w-full"
label={label ?? t("agents.connect.client")}
value={client}
onValueChange={(v) => setClient(v as McpClient)}
items={Object.fromEntries(clients.map((c) => [c, t(`agents.connect.clients.${c}`)]))}
/>
<div className="min-w-0 overflow-x-auto">
<CodeHighlighted
code={mcpSnippet(client, endpoint, agentKey)}
lang={snippetLang(client)}
showCopyButton
/>
</div>
<Text variant="secondary" size="sm">
{t(`agents.connect.hints.${client}`)}
</Text>
</div>
);
}
29 changes: 22 additions & 7 deletions apps/studio/src/i18n/ar.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Generated by `pnpm i18n:import` (ADR-0034): ar core messages.
// Do not edit: change the translation file and import it; only English is edited here.
// hash: 586c8d46bd63e844
// hash: 46a568922c0a4e81
import type { Messages } from "./en.ts";

export const messages: Messages = {
Expand Down Expand Up @@ -930,7 +930,7 @@ export const messages: Messages = {
},
reveal: {
title: "انسخ مفتاحك الجديد",
description: "«{name}» جاهز. يرسله وكيلك في الترويسة x-api-key.",
description: "«{name}» جاهز. يرسله وكيلك كرمز Bearer أو في الترويسة x-api-key.",
warningTitle: "لن ترى هذا المفتاح مجددًا",
warning:
"احفظه الآن في مدير كلمات المرور لديك أو في مخزن أسرار الوكيل. إذا فُقد، فألغِه وأنشئ مفتاحًا جديدًا.",
Expand All @@ -944,13 +944,28 @@ export const messages: Messages = {
"يصل الوكلاء إلى Studio عبر MCP على هذا العنوان ويرسلون مفتاحهم في ترويسة. عند الاتصال يُبلَّغ الوكيل بوضع مفتاحه، فيعرف ما ينتظرك.",
endpoint: "نقطة نهاية MCP",
header: "الترويسة",
tabs: {
json: "JSON",
client: "العميل",
clients: {
claude: "Claude Code",
stdio: "محلي",
codex: "Codex",
cursor: "Cursor",
vscode: "VS Code",
generic: "عميل آخر",
stdio: "محلي (stdio)",
},
hints: {
claude: "شغّله في الطرفية، ثم تحقّق من الاتصال بالأمر ‎/mcp داخل Claude Code.",
codex:
"يقرأ Codex المفتاح من CLOUDHAL_API_KEY ويرسله كرمز Bearer. أضف سطر export إلى ملف تعريف الصدفة لديك ليكون المفتاح معيّنًا كلما بدأ Codex.",
cursor:
"الصقه في ‎~/.cursor/mcp.json أو في ‎.cursor/mcp.json داخل مشروع. يحتوي الملف على المفتاح، فأبقِه خارج نظام التحكم في الإصدارات.",
vscode:
"احفظه باسم ‎.vscode/mcp.json في مشروعك. يطلب VS Code المفتاح مرة واحدة ويحفظه في مخزن الأسرار لديه.",
generic:
"أي عميل يدعم Streamable HTTP: هذا العنوان، مع المفتاح في الترويسة Authorization كرمز Bearer. تعمل الترويسة x-api-key أيضًا.",
stdio:
"على جهازك الخاص، شغّل خادم stdio داخل مجلد الموقع؛ لا حاجة إلى مفتاح. يحدّد --mode طريقة عمله، وهنا فقط يمكن أن يكون bypass.",
},
stdioHint:
"على جهازك الخاص، شغّل خادم stdio داخل مجلد الموقع؛ لا حاجة إلى مفتاح. يحدّد --mode طريقة عمله، وهنا فقط يمكن أن يكون bypass.",
},
},
activity: {
Expand Down
31 changes: 24 additions & 7 deletions apps/studio/src/i18n/de.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Generated by `pnpm i18n:import` (ADR-0034): de core messages.
// Do not edit: change the translation file and import it; only English is edited here.
// hash: 8a6fa48d080c8dcb
// hash: d5dca37f709f1733
import type { Messages } from "./en.ts";

export const messages: Messages = {
Expand Down Expand Up @@ -964,7 +964,8 @@ export const messages: Messages = {
},
reveal: {
title: "Kopiere deinen neuen Schlüssel",
description: "„{name}“ ist bereit. Dein Agent sendet ihn im Header x-api-key.",
description:
"„{name}“ ist bereit. Dein Agent sendet ihn als Bearer-Token oder im Header x-api-key.",
warningTitle: "Du wirst diesen Schlüssel nicht wieder sehen",
warning:
"Speichere ihn jetzt in deinem Passwortmanager oder im Secret Store des Agents. Geht er verloren, widerrufe ihn und erstelle einen neuen.",
Expand All @@ -978,13 +979,29 @@ export const messages: Messages = {
"Agents erreichen Studio über MCP unter dieser Adresse und senden ihren Schlüssel in einem Header. Beim Verbinden erfährt der Agent den Modus seines Schlüssels und weiß so, was auf dich wartet.",
endpoint: "MCP-Endpunkt",
header: "Header",
tabs: {
json: "JSON",
client: "Client",
clients: {
claude: "Claude Code",
stdio: "Lokal",
codex: "Codex",
cursor: "Cursor",
vscode: "VS Code",
generic: "Anderer Client",
stdio: "Lokal (stdio)",
},
hints: {
claude:
"Führe es in einem Terminal aus und prüfe die Verbindung dann mit /mcp in Claude Code.",
codex:
"Codex liest den Schlüssel aus CLOUDHAL_API_KEY und sendet ihn als Bearer-Token. Trag die export-Zeile in dein Shell-Profil ein, damit der Schlüssel bei jedem Start von Codex gesetzt ist.",
cursor:
"Füge es in ~/.cursor/mcp.json ein oder in .cursor/mcp.json in einem Projekt. Die Datei enthält den Schlüssel, halte sie also aus der Versionsverwaltung heraus.",
vscode:
"Speichere es als .vscode/mcp.json in deinem Projekt. VS Code fragt einmal nach dem Schlüssel und bewahrt ihn in seinem Geheimnisspeicher auf.",
generic:
"Jeder Client, der Streamable HTTP spricht: diese URL, mit dem Schlüssel als Bearer-Token im Authorization-Header. Der Header x-api-key funktioniert auch.",
stdio:
"Starte auf deinem eigenen Rechner den stdio-Server im Website-Ordner; kein Schlüssel nötig. --mode legt fest, wie er arbeitet, und nur hier kann er bypass sein.",
},
stdioHint:
"Starte auf deinem eigenen Rechner den stdio-Server im Website-Ordner; kein Schlüssel nötig. --mode legt fest, wie er arbeitet, und nur hier kann er bypass sein.",
},
},
activity: {
Expand Down
28 changes: 24 additions & 4 deletions apps/studio/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,8 @@ export const en = {
},
reveal: {
title: "Copy your new key",
description: "“{name}” is ready. Your agent sends it in the x-api-key header.",
description:
"“{name}” is ready. Your agent sends it as a bearer token or in the x-api-key header.",
warningTitle: "You won't see this key again",
warning:
"Store it now in your password manager or the agent's secret store. If it's lost, revoke it and create a new one.",
Expand All @@ -913,9 +914,28 @@ export const en = {
"Agents reach Studio over MCP at this address and send their key in a header. When it connects, the agent is told its key's mode, so it knows what waits for you.",
endpoint: "MCP endpoint",
header: "Header",
tabs: { json: "JSON", claude: "Claude Code", stdio: "Local" },
stdioHint:
"On your own machine, run the stdio server inside the site folder; no key needed. --mode sets how it works, and only here can it be bypass.",
client: "Client",
clients: {
claude: "Claude Code",
codex: "Codex",
cursor: "Cursor",
vscode: "VS Code",
generic: "Other client",
stdio: "Local (stdio)",
},
hints: {
claude: "Run it in a terminal, then check the connection with /mcp inside Claude Code.",
codex:
"Codex reads the key from CLOUDHAL_API_KEY and sends it as a bearer token. Put the export line in your shell profile so the key is set whenever Codex starts.",
cursor:
"Paste it into ~/.cursor/mcp.json, or .cursor/mcp.json in a project. The file holds the key, so keep it out of version control.",
vscode:
"Save it as .vscode/mcp.json in your project. VS Code asks for the key once and keeps it in its secret storage.",
generic:
"Any client that speaks streamable HTTP: this URL, with the key in the Authorization header as a bearer token. The x-api-key header works too.",
stdio:
"On your own machine, run the stdio server inside the site folder; no key needed. --mode sets how it works, and only here can it be bypass.",
},
},
},
activity: {
Expand Down
30 changes: 23 additions & 7 deletions apps/studio/src/i18n/es.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Generated by `pnpm i18n:import` (ADR-0034): es core messages.
// Do not edit: change the translation file and import it; only English is edited here.
// hash: b279cd0e5ba1d754
// hash: d29c27e165ec3375
import type { Messages } from "./en.ts";

export const messages: Messages = {
Expand Down Expand Up @@ -962,7 +962,8 @@ export const messages: Messages = {
},
reveal: {
title: "Copia tu nueva clave",
description: "“{name}” está lista. Tu agente la envía en el encabezado x-api-key.",
description:
"“{name}” está lista. Tu agente la envía como token bearer o en el encabezado x-api-key.",
warningTitle: "No volverás a ver esta clave",
warning:
"Guárdala ahora en tu gestor de contraseñas o en el almacén de secretos del agente. Si se pierde, revócala y crea una nueva.",
Expand All @@ -976,13 +977,28 @@ export const messages: Messages = {
"Los agentes acceden a Studio por MCP en esta dirección y envían su clave en un encabezado. Al conectarse, se le indica al agente el modo de su clave, para que sepa qué espera por ti.",
endpoint: "Endpoint MCP",
header: "Encabezado",
tabs: {
json: "JSON",
client: "Cliente",
clients: {
claude: "Claude Code",
stdio: "Local",
codex: "Codex",
cursor: "Cursor",
vscode: "VS Code",
generic: "Otro cliente",
stdio: "Local (stdio)",
},
hints: {
claude: "Ejecútalo en una terminal y comprueba la conexión con /mcp dentro de Claude Code.",
codex:
"Codex lee la clave de CLOUDHAL_API_KEY y la envía como token bearer. Pon la línea export en el perfil de tu shell para que la clave esté definida cada vez que se inicie Codex.",
cursor:
"Pégalo en ~/.cursor/mcp.json, o en .cursor/mcp.json dentro de un proyecto. El archivo contiene la clave, así que mantenlo fuera del control de versiones.",
vscode:
"Guárdalo como .vscode/mcp.json en tu proyecto. VS Code pide la clave una vez y la guarda en su almacenamiento de secretos.",
generic:
"Cualquier cliente que hable HTTP transmisible: esta URL, con la clave en el encabezado Authorization como token bearer. El encabezado x-api-key también funciona.",
stdio:
"En tu propio equipo, ejecuta el servidor stdio dentro de la carpeta del sitio; no hace falta clave. --mode define cómo funciona, y solo aquí puede ser bypass.",
},
stdioHint:
"En tu propio equipo, ejecuta el servidor stdio dentro de la carpeta del sitio; no hace falta clave. --mode define cómo funciona, y solo aquí puede ser bypass.",
},
},
activity: {
Expand Down
30 changes: 23 additions & 7 deletions apps/studio/src/i18n/fa.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Generated by `pnpm i18n:import` (ADR-0034): fa core messages.
// Do not edit: change the translation file and import it; only English is edited here.
// hash: 090d022080bd43f1
// hash: cb94d2968657f128
import type { Messages } from "./en.ts";

export const messages: Messages = {
Expand Down Expand Up @@ -944,7 +944,8 @@ export const messages: Messages = {
},
reveal: {
title: "کلید جدید خود را کپی کنید",
description: "«{name}» آماده است. عامل شما آن را در سرآیند x-api-key می‌فرستد.",
description:
"«{name}» آماده است. عامل شما آن را به‌صورت توکن Bearer یا در سرآیند x-api-key می‌فرستد.",
warningTitle: "این کلید را دوباره نخواهید دید",
warning:
"همین حالا آن را در مدیر رمز عبور یا انبار اسرار عامل ذخیره کنید. اگر گم شد، آن را باطل کنید و کلید جدیدی بسازید.",
Expand All @@ -958,13 +959,28 @@ export const messages: Messages = {
"عامل‌ها از طریق MCP در این نشانی به Studio می‌رسند و کلید خود را در یک سرآیند می‌فرستند. هنگام اتصال، حالت کلید به عامل گفته می‌شود تا بداند چه چیزی منتظر شما می‌ماند.",
endpoint: "نقطهٔ پایانی MCP",
header: "سرآیند",
tabs: {
json: "JSON",
client: "کلاینت",
clients: {
claude: "Claude Code",
stdio: "محلی",
codex: "Codex",
cursor: "Cursor",
vscode: "VS Code",
generic: "کلاینت دیگر",
stdio: "محلی (stdio)",
},
hints: {
claude: "آن را در ترمینال اجرا کنید، سپس اتصال را با ‎/mcp در Claude Code بررسی کنید.",
codex:
"‏Codex کلید را از CLOUDHAL_API_KEY می‌خواند و به‌صورت توکن Bearer می‌فرستد. خط export را به پروفایل پوستهٔ خود اضافه کنید تا هر بار که Codex شروع می‌شود کلید تنظیم باشد.",
cursor:
"آن را در ‎~/.cursor/mcp.json یا در ‎.cursor/mcp.json یک پروژه بچسبانید. این فایل کلید را دارد، پس آن را از کنترل نسخه بیرون نگه دارید.",
vscode:
"آن را با نام ‎.vscode/mcp.json در پروژه‌تان ذخیره کنید. ‏VS Code یک بار کلید را می‌پرسد و در فضای ذخیرهٔ رازهایش نگه می‌دارد.",
generic:
"هر کلاینتی که Streamable HTTP را پشتیبانی می‌کند: این نشانی، با کلید در سرآیند Authorization به‌صورت توکن Bearer. سرآیند x-api-key هم کار می‌کند.",
stdio:
"روی دستگاه خودتان، سرور stdio را درون پوشهٔ سایت اجرا کنید؛ کلیدی لازم نیست. --mode نحوهٔ کار آن را تعیین می‌کند و فقط اینجا می‌تواند bypass باشد.",
},
stdioHint:
"روی دستگاه خودتان، سرور stdio را درون پوشهٔ سایت اجرا کنید؛ کلیدی لازم نیست. --mode نحوهٔ کار آن را تعیین می‌کند و فقط اینجا می‌تواند bypass باشد.",
},
},
activity: {
Expand Down
Loading
Loading