Move Composio, logo.dev and Antigravity keys into Settings

The three third-party keys were hardcoded or read from the environment.
Operators now enter them in Settings > Integrations; they are stored
encrypted in the app-wide integration_keys table (migration 058).
An agent holding connector grants fails loudly when no Composio key is
saved. The goal-review test no longer depends on absolute dates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 01:34:14 -04:00
1 parent 1ea040c0f0
commit 56b0594f91
36 files changed
+430 -100

No files matched your search

+13
View File
@@ -26,6 +26,19 @@ export const zh: Record<string, string> = {
"Add workspace": "添加工作区",
"Advance": "推进",
"Advanced": "高级",
"Integrations": "集成密钥",
"Keys are stored on this device, encrypted by the operating system, and shared by every company.": "密钥保存在本机,由操作系统加密,所有公司共用。",
"Composio API key": "Composio API 密钥",
"Powers the Connectors page and agent connector tools. Create one at platform.composio.dev.": "用于连接器页面和 Agent 的连接器工具。在 platform.composio.dev 创建。",
"logo.dev publishable key": "logo.dev 公开密钥",
"Shows brand logos for connectors. Create one at logo.dev.": "为连接器显示品牌图标。在 logo.dev 创建。",
"Google Antigravity OAuth client secret": "Google Antigravity OAuth 客户端密钥",
"Needed to sign in to and refresh the Google Antigravity provider.": "登录和续期 Google Antigravity 时需要。",
"Saved": "已保存",
"Not set": "未设置",
"Replace key": "替换密钥",
"Key": "密钥",
"Remove": "移除",
"Agent": "智能体",
"Agent could not be reached": "无法连接到智能体",
"Agent runs": "智能体运行",
+4 -1
View File
@@ -2,11 +2,12 @@ import { useMemo, useState } from "react";
import { ProvidersPanel } from "./settings/ProvidersPanel";
import { WorkspacePanel } from "./settings/WorkspacePanel";
import { AdvancedPanel } from "./settings/AdvancedPanel";
import { IntegrationKeysPanel } from "./settings/IntegrationKeysPanel";
import { t } from "../i18n/i18n";
import type { PageProps } from "./types";
import { activeOrgId, requireActiveEmployee, requireActiveOrganization } from "./types";
type SettingsTab = "general" | "providers" | "advanced";
type SettingsTab = "general" | "providers" | "integrations" | "advanced";
interface NavSpec {
id: SettingsTab;
@@ -16,6 +17,7 @@ interface NavSpec {
const TABS: NavSpec[] = [
{ id: "general", label: t("General") },
{ id: "providers", label: t("Providers") },
{ id: "integrations", label: t("Integrations") },
{ id: "advanced", label: t("Advanced") },
];
@@ -56,6 +58,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
actions={actions}
/>
)}
{tab === "integrations" && <IntegrationKeysPanel keys={snapshot.integrationKeys} actions={actions} />}
{tab === "advanced" && (
<AdvancedPanel
orgId={orgId}
@@ -0,0 +1,90 @@
import { useState, type FormEvent } from "react";
import type { IntegrationKeyName, IntegrationKeyStatus } from "../../../../shared/commands";
import { Button, Field, Pill } from "../../shared/ui";
import { t } from "../../i18n/i18n";
import type { SnapshotActions } from "../../app/useSnapshot";
const KEY_COPY: Record<IntegrationKeyName, { label: string; hint: string; placeholder: string }> = {
composio: {
label: t("Composio API key"),
hint: t("Powers the Connectors page and agent connector tools. Create one at platform.composio.dev."),
placeholder: "ak_…",
},
logo_dev: {
label: t("logo.dev publishable key"),
hint: t("Shows brand logos for connectors. Create one at logo.dev."),
placeholder: "pk_…",
},
google_antigravity_client_secret: {
label: t("Google Antigravity OAuth client secret"),
hint: t("Needed to sign in to and refresh the Google Antigravity provider."),
placeholder: "GOCSPX-…",
},
};
export function IntegrationKeysPanel({ keys, actions }: { keys: IntegrationKeyStatus[]; actions: SnapshotActions }) {
return (
<div className="page">
<h1 className="page-h1">{t("Integrations")}</h1>
<p style={{ margin: "8px 0 24px", color: "var(--text-2)" }}>
{t("Keys are stored on this device, encrypted by the operating system, and shared by every company.")}
</p>
{keys.map((status) => <KeyRow key={status.name} status={status} actions={actions} />)}
</div>
);
}
function KeyRow({ status, actions }: { status: IntegrationKeyStatus; actions: SnapshotActions }) {
const copy = KEY_COPY[status.name];
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const perform = async (operation: () => Promise<unknown>) => {
setBusy(true);
setError(null);
try {
await operation();
} catch (failure) {
setError(failure instanceof Error ? failure.message : String(failure));
} finally {
setBusy(false);
}
};
const save = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = event.currentTarget;
const value = String(new FormData(form).get("value") ?? "");
void perform(async () => {
await actions.run(() => window.agentCompany.setIntegrationKey({ name: status.name, value }));
form.reset();
});
};
return (
<form onSubmit={save} style={{ marginBottom: 24 }}>
<div className="row" style={{ gap: 8, marginBottom: 6 }}>
<span className="section-label">{copy.label}</span>
{status.configured ? <Pill tone="green" dot>{t("Saved")}</Pill> : <Pill>{t("Not set")}</Pill>}
</div>
<p style={{ margin: "0 0 8px", color: "var(--text-2)" }}>{copy.hint}</p>
<div className="row" style={{ gap: 8, alignItems: "flex-end" }}>
<div style={{ flex: 1, maxWidth: 420 }}>
<Field label={status.configured ? t("Replace key") : t("Key")}>
<input className="input" name="value" type="password" autoComplete="off" placeholder={copy.placeholder} required />
</Field>
</div>
<Button type="submit" tone="primary" disabled={busy}>{t("Save")}</Button>
{status.configured && (
<Button
disabled={busy}
onClick={() => void perform(() => actions.run(() => window.agentCompany.clearIntegrationKey({ name: status.name })))}
>
{t("Remove")}
</Button>
)}
</div>
{error && <div className="form-error" role="alert">{error}</div>}
</form>
);
}