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

@@ -1,14 +1,14 @@
import type { DatabaseSync } from "node:sqlite";
import type { ConnectorCatalogEntry, ConnectComposioToolkitResult } from "../../../shared/commands";
import { ComposioClient } from "./composio-client";
import { composioApiKey } from "./composio-credentials";
import { IntegrationKeyStore } from "../integration-key-store";
import { getComposioUserId } from "./composio-tool-bridge";
import { connectorDisplayName, connectorLogoUrl } from "./connector-brands";
import { normalizeSlug } from "./connector-grants";
const CATALOG_TTL_MS = 10 * 60 * 1000;
let catalogCache: { fetchedAt: number; entries: ConnectorCatalogEntry[] } | null = null;
let catalogCache: { fetchedAt: number; keys: string; entries: ConnectorCatalogEntry[] } | null = null;
/**
* Company-level Composio connections. The catalog mirrors the auth configs
@@ -25,7 +25,10 @@ let catalogCache: { fetchedAt: number; entries: ConnectorCatalogEntry[] } | null
export class ComposioConnectionService {
private static queues = new Map<string, Promise<unknown>>();
constructor(private readonly db: DatabaseSync) {}
constructor(
private readonly db: DatabaseSync,
private readonly keys: IntegrationKeyStore = new IntegrationKeyStore(db),
) {}
listCatalog(): Promise<ConnectorCatalogEntry[]> {
return this.catalog(this.requireClient());
@@ -149,13 +152,16 @@ export class ComposioConnectionService {
}
private requireClient(): ComposioClient {
const apiKey = composioApiKey();
if (!apiKey) throw new Error("The Composio integration is not configured in this build.");
const apiKey = this.keys.read("composio");
if (!apiKey) throw new Error("No Composio API key is saved. Add one in Settings > Integrations.");
return new ComposioClient(apiKey);
}
private async catalog(client: ComposioClient): Promise<ConnectorCatalogEntry[]> {
if (catalogCache && Date.now() - catalogCache.fetchedAt < CATALOG_TTL_MS) {
const logoDevToken = this.keys.read("logo_dev");
const keys = `${this.keys.read("composio")}
${logoDevToken}`;
if (catalogCache && catalogCache.keys === keys && Date.now() - catalogCache.fetchedAt < CATALOG_TTL_MS) {
return catalogCache.entries;
}
const configs = await client.listAuthConfigs();
@@ -167,11 +173,11 @@ export class ComposioConnectionService {
slug: config.toolkitSlug,
name: connectorDisplayName(config.toolkitSlug),
authScheme: config.authScheme,
logoUrl: connectorLogoUrl(config.toolkitSlug),
logoUrl: connectorLogoUrl(config.toolkitSlug, logoDevToken),
});
}
const entries = [...bySlug.values()].sort((a, b) => a.name.localeCompare(b.name));
catalogCache = { fetchedAt: Date.now(), entries };
catalogCache = { fetchedAt: Date.now(), keys, entries };
return entries;
}
}
@@ -1,16 +0,0 @@
/**
* The Composio integration key is hardcoded on purpose: the repo is private
* and every checkout must build a configured app, with no credential file or
* build-time injection involved. Rotating the key = editing this constant.
*/
const COMPOSIO_API_KEY = "***REMOVED***";
export function composioApiKey(): string | null {
return normalizeComposioApiKey(COMPOSIO_API_KEY);
}
/** Null for blank input so callers surface "not configured" explicitly. */
export function normalizeComposioApiKey(raw: string): string | null {
const key = raw.trim();
return key ? key : null;
}
@@ -1,7 +1,7 @@
import type { DatabaseSync } from "node:sqlite";
import type { ToolDefinition, ToolCall } from "../provider-tool-turn";
import { ComposioClient, type ComposioToolSchema } from "./composio-client";
import { composioApiKey } from "./composio-credentials";
import type { IntegrationKeyStore } from "../integration-key-store";
import { ConnectorGrantsService } from "./connector-grants";
const COMPOSIO_TOOL_PREFIX = "composio_";
@@ -52,14 +52,16 @@ export function composioSchemaToToolDefinition(schema: ComposioToolSchema): Tool
* intersection of the company's active connections and the toolkits the CEO
* granted this agent.
*/
export async function loadComposioTools(db: DatabaseSync, orgId: string, agentId: string): Promise<ToolDefinition[]> {
const apiKey = composioApiKey();
if (!apiKey) return [];
export async function loadComposioTools(db: DatabaseSync, keys: IntegrationKeyStore, orgId: string, agentId: string): Promise<ToolDefinition[]> {
const grants = new ConnectorGrantsService(db);
const toolkits = grants.toolkitsForAgent(orgId, agentId);
if (toolkits.length === 0) return [];
const apiKey = keys.read("composio");
if (!apiKey) {
throw new Error(`Agent ${agentId} holds connector grants (${toolkits.join(", ")}) but no Composio API key is saved in Settings > Integrations.`);
}
const toolkitsKey = toolkits.join(",");
const cacheKey = `${orgId}:${toolkitsKey}`;
const cacheKey = `${apiKey}:${orgId}:${toolkitsKey}`;
const cached = toolCache.get(cacheKey);
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
return cached.tools;
@@ -80,12 +82,13 @@ export async function loadComposioTools(db: DatabaseSync, orgId: string, agentId
export async function executeComposioTool(
db: DatabaseSync,
keys: IntegrationKeyStore,
orgId: string,
agentId: string,
call: ToolCall,
): Promise<string> {
const apiKey = composioApiKey();
if (!apiKey) return "ERROR: The Composio integration is not configured in this build.";
const apiKey = keys.read("composio");
if (!apiKey) return "ERROR: No Composio API key is saved. Ask the board to add one in Settings > Integrations.";
const toolSlug = originalToolName(call.name);
const toolkit = toolkitForTool(toolSlug);
const grants = new ConnectorGrantsService(db);
+4 -11
View File
@@ -1,11 +1,4 @@
/**
* Display names and logo domains for the toolkits configured in the Composio
* project. Logos come from img.logo.dev keyed by company domain (publishable
* token — safe to embed). A slug outside this table renders with a prettified
* name and the letter tile until its brand row is added here.
*/
const LOGO_DEV_TOKEN = "***REMOVED***";
/** Toolkit display names and logo.dev domains. */
const BRANDS: Record<string, { name: string; domain: string }> = {
airtable: { name: "Airtable", domain: "airtable.com" },
attio: { name: "Attio", domain: "attio.com" },
@@ -60,8 +53,8 @@ export function connectorDisplayName(slug: string): string {
.join(" ");
}
export function connectorLogoUrl(slug: string): string | null {
export function connectorLogoUrl(slug: string, logoDevToken: string | null): string | null {
const brand = BRANDS[slug];
if (!brand) return null;
return `https://img.logo.dev/${brand.domain}?token=${LOGO_DEV_TOKEN}`;
if (!brand || !logoDevToken) return null;
return `https://img.logo.dev/${brand.domain}?token=${encodeURIComponent(logoDevToken)}`;
}