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:
1 parent
1ea040c0f0
commit
56b0594f91
36 files changed
+430
-100
No files matched your search
@@ -28,6 +28,7 @@ import { AutomationService } from "../services/automation-service";
|
||||
import { MeetingService } from "../services/meeting-service";
|
||||
import { SprintService } from "../services/sprint-service";
|
||||
import { ComposioConnectionService } from "../services/composio/composio-connection-service";
|
||||
import { IntegrationKeyStore } from "../services/integration-key-store";
|
||||
import { importStagedSkills, stageBuiltinSkillsLibrary } from "../services/builtin-skills";
|
||||
import { coalesce } from "./coalesce";
|
||||
import { normalizeExternalBrowserUrl } from "./external-url";
|
||||
@@ -48,7 +49,8 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
const approvals = new ApprovalService(deps.connection.db);
|
||||
const chat = new ChatService(deps.connection.db);
|
||||
const providers = new ProviderService(deps.connection.db);
|
||||
const providerOAuth = new ProviderOAuthService();
|
||||
const integrationKeys = new IntegrationKeyStore(deps.connection.db);
|
||||
const providerOAuth = new ProviderOAuthService(integrationKeys);
|
||||
const skills = new SkillService(deps.connection.db, deps.artifactRoot);
|
||||
const notifyDataChanged = coalesce(() => {
|
||||
try {
|
||||
@@ -137,6 +139,8 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
ipcMain.handle(IPC_CHANNELS.updateAgent, (_event, input) => mutate(() => organization.updateAgent(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.updateEmployeeProfile, (_event, input) => mutate(() => organization.updateEmployeeProfile(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.setAutonomousPaused, (_event, input) => mutate(() => organization.setAutonomousPaused(Boolean(input.paused))));
|
||||
ipcMain.handle(IPC_CHANNELS.setIntegrationKey, (_event, input) => mutate(() => integrationKeys.write(input.name, input.value)));
|
||||
ipcMain.handle(IPC_CHANNELS.clearIntegrationKey, (_event, input) => mutate(() => integrationKeys.clear(input.name)));
|
||||
ipcMain.handle(IPC_CHANNELS.createTask, (_event, input) => mutate(() => organization.createTask(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.updateTask, (_event, input) => mutate(() => organization.updateTask(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.deleteTask, (_event, input) => mutate(() => organization.deleteTask(input)));
|
||||
@@ -344,7 +348,7 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
}
|
||||
});
|
||||
|
||||
const composioConnections = new ComposioConnectionService(deps.connection.db);
|
||||
const composioConnections = new ComposioConnectionService(deps.connection.db, integrationKeys);
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.listConnectorCatalog, async () => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const integrationKeysSchema = `
|
||||
create table if not exists integration_keys (
|
||||
name text primary key,
|
||||
ciphertext blob not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (58, 'integration_keys', datetime('now'));
|
||||
`;
|
||||
@@ -56,6 +56,7 @@ import { closeStaleWakeTasksSchema } from "./migrations/054-close-stale-wake-tas
|
||||
import { taskOriginSchema } from "./migrations/055-task-origin";
|
||||
import { kimiProvidersSchema } from "./migrations/056-kimi-providers";
|
||||
import { kimiCodeProviderSchema } from "./migrations/057-kimi-code-provider";
|
||||
import { integrationKeysSchema } from "./migrations/058-integration-keys";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -121,6 +122,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 55, name: "task_origin", sql: taskOriginSchema },
|
||||
{ version: 56, name: "kimi_providers", sql: kimiProvidersSchema },
|
||||
{ version: 57, name: "kimi_code_provider", sql: kimiCodeProviderSchema },
|
||||
{ version: 58, name: "integration_keys", sql: integrationKeysSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface AgentRunLoopResult {
|
||||
}
|
||||
|
||||
export interface AgentRunLoopDeps {
|
||||
providers: Pick<ProviderService, "runToolTurn">;
|
||||
providers: Pick<ProviderService, "runToolTurn" | "integrationKeys">;
|
||||
tools: AgentToolDeps;
|
||||
/** Called after each iteration's tool calls have written their effects. */
|
||||
onToolResults?: () => void;
|
||||
@@ -72,7 +72,7 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn
|
||||
toolCallCount += 1;
|
||||
let output: string;
|
||||
if (isComposioTool(call.name)) {
|
||||
output = await executeComposioTool(deps.tools.db, input.orgId, input.toolContext.agent.id, call);
|
||||
output = await executeComposioTool(deps.tools.db, deps.providers.integrationKeys, input.orgId, input.toolContext.agent.id, call);
|
||||
} else if (isWorkspaceTool(call.name)) {
|
||||
output = await executeWorkspaceTool(input.toolContext, call);
|
||||
} else if (isWebTool(call.name)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import {
|
||||
INTEGRATION_KEY_NAMES,
|
||||
type IntegrationKeyName,
|
||||
type IntegrationKeyStatus,
|
||||
} from "../../shared/commands";
|
||||
import type { ProviderSecretVault } from "./auth-broker-service";
|
||||
import { SecretVault } from "./secret-vault";
|
||||
|
||||
/** Operator-entered third-party keys, encrypted. */
|
||||
export class IntegrationKeyStore {
|
||||
constructor(
|
||||
private readonly db: DatabaseSync,
|
||||
private readonly vault: ProviderSecretVault = new SecretVault(),
|
||||
) {}
|
||||
|
||||
read(name: IntegrationKeyName): string | null {
|
||||
const row = this.db.prepare("select ciphertext from integration_keys where name = ?")
|
||||
.get(name) as { ciphertext: Uint8Array } | undefined;
|
||||
return row ? this.vault.decryptSecret(Buffer.from(row.ciphertext)) : null;
|
||||
}
|
||||
|
||||
write(name: IntegrationKeyName, value: string): void {
|
||||
requireKnownName(name);
|
||||
const key = value.trim();
|
||||
if (!key) throw new Error(`The ${name} key cannot be blank.`);
|
||||
this.db.prepare(`
|
||||
insert into integration_keys(name, ciphertext, updated_at) values (?, ?, ?)
|
||||
on conflict(name) do update set ciphertext = excluded.ciphertext, updated_at = excluded.updated_at
|
||||
`).run(name, this.vault.encryptSecret(key), new Date().toISOString());
|
||||
console.info(`[integration-keys] ${name} saved`);
|
||||
}
|
||||
|
||||
clear(name: IntegrationKeyName): void {
|
||||
requireKnownName(name);
|
||||
this.db.prepare("delete from integration_keys where name = ?").run(name);
|
||||
console.info(`[integration-keys] ${name} cleared`);
|
||||
}
|
||||
|
||||
statuses(): IntegrationKeyStatus[] {
|
||||
const rows = this.db.prepare("select name, updated_at from integration_keys")
|
||||
.all() as Array<{ name: string; updated_at: string }>;
|
||||
const saved = new Map(rows.map((row) => [row.name, row.updated_at]));
|
||||
return INTEGRATION_KEY_NAMES.map((name) => ({
|
||||
name,
|
||||
configured: saved.has(name),
|
||||
updatedAt: saved.get(name) ?? null,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function requireKnownName(name: string): asserts name is IntegrationKeyName {
|
||||
if (!(INTEGRATION_KEY_NAMES as readonly string[]).includes(name)) {
|
||||
throw new Error(`Unknown integration key "${name}".`);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { generatePkce } from "./pkce";
|
||||
import type { OAuthCredentials, OAuthFlow, OAuthLoginCallbacks } from "./oauth-types";
|
||||
|
||||
const CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
||||
const CLIENT_SECRET_ENV = "AGENTCOMPANY_GOOGLE_ANTIGRAVITY_CLIENT_SECRET";
|
||||
const CALLBACK_PORT = 51121;
|
||||
const CALLBACK_PATH = "/oauth-callback";
|
||||
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
||||
@@ -24,9 +23,8 @@ const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
||||
const CODE_ASSIST_ENDPOINTS = ["https://cloudcode-pa.googleapis.com", "https://daily-cloudcode-pa.sandbox.googleapis.com"];
|
||||
|
||||
export function resolveAntigravityClientSecret(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const secret = env[CLIENT_SECRET_ENV]?.trim();
|
||||
if (!secret) throw new Error(`${CLIENT_SECRET_ENV} is required for Google Antigravity OAuth.`);
|
||||
export function requireAntigravityClientSecret(secret: string | null | undefined): string {
|
||||
if (!secret) throw new Error("Google Antigravity sign-in needs its OAuth client secret. Add it in Settings > Integrations.");
|
||||
return secret;
|
||||
}
|
||||
|
||||
@@ -45,13 +43,13 @@ function buildAuthorizeUrl(challenge: string, state: string): string {
|
||||
return `${AUTH_URL}?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function exchangeCode(code: string, verifier: string): Promise<{ access: string; refresh: string; expires: number }> {
|
||||
async function exchangeCode(code: string, verifier: string, clientSecret: string): Promise<{ access: string; refresh: string; expires: number }> {
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: resolveAntigravityClientSecret(),
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: REDIRECT_URI,
|
||||
@@ -105,13 +103,13 @@ async function discoverProject(accessToken: string): Promise<string> {
|
||||
}
|
||||
|
||||
/** Refresh an expired Antigravity access token, retaining the project id. */
|
||||
export async function refreshAntigravity(refreshToken: string, projectId: string): Promise<OAuthCredentials> {
|
||||
export async function refreshAntigravity(refreshToken: string, projectId: string, clientSecret: string): Promise<OAuthCredentials> {
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: resolveAntigravityClientSecret(),
|
||||
client_secret: clientSecret,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
}),
|
||||
@@ -129,13 +127,14 @@ export async function refreshAntigravity(refreshToken: string, projectId: string
|
||||
export const antigravityFlow: OAuthFlow = {
|
||||
providerKey: "google-antigravity",
|
||||
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
const clientSecret = requireAntigravityClientSecret(callbacks.clientSecret);
|
||||
const { verifier, challenge } = generatePkce();
|
||||
const server = await startOAuthCallbackServer({ port: CALLBACK_PORT, path: CALLBACK_PATH, signal: callbacks.signal });
|
||||
try {
|
||||
callbacks.onAuth({ url: buildAuthorizeUrl(challenge, verifier), instructions: "Complete the Google sign-in in your browser." });
|
||||
const result = await server.waitForCode();
|
||||
if (result.state !== verifier) throw new Error("OAuth state mismatch.");
|
||||
const tokens = await exchangeCode(result.code, verifier);
|
||||
const tokens = await exchangeCode(result.code, verifier, clientSecret);
|
||||
callbacks.onProgress?.("Resolving project");
|
||||
const projectId = await discoverProject(tokens.access);
|
||||
return { access: tokens.access, refresh: tokens.refresh, expires: tokens.expires, projectId };
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface OAuthLoginCallbacks {
|
||||
signal?: AbortSignal;
|
||||
/** GitHub Enterprise domain for Copilot; null/empty means github.com. */
|
||||
enterpriseDomain?: string | null;
|
||||
/** Client secret for flows that need one. */
|
||||
clientSecret?: string | null;
|
||||
}
|
||||
|
||||
export interface OAuthFlow {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getOAuthFlow } from "./oauth/oauth-flows";
|
||||
import type { IntegrationKeyStore } from "./integration-key-store";
|
||||
|
||||
export interface ProviderOAuthHooks {
|
||||
onAuth: (info: { url: string; instructions?: string }) => void;
|
||||
@@ -14,6 +15,8 @@ export interface ProviderOAuthHooks {
|
||||
export class ProviderOAuthService {
|
||||
private readonly controllers = new Map<string, AbortController>();
|
||||
|
||||
constructor(private readonly keys: IntegrationKeyStore) {}
|
||||
|
||||
async run(input: { providerKey: string; enterpriseDomain?: string | null }, hooks: ProviderOAuthHooks): Promise<string> {
|
||||
const flow = getOAuthFlow(input.providerKey);
|
||||
if (this.controllers.has(input.providerKey)) {
|
||||
@@ -27,6 +30,7 @@ export class ProviderOAuthService {
|
||||
onProgress: hooks.onProgress,
|
||||
signal: controller.signal,
|
||||
enterpriseDomain: input.enterpriseDomain ?? null,
|
||||
clientSecret: input.providerKey === "google-antigravity" ? this.keys.read("google_antigravity_client_secret") : null,
|
||||
});
|
||||
return JSON.stringify(credentials);
|
||||
} finally {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { IntegrationKeyStore } from "./integration-key-store";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { CancelProviderResponseInput, CreateProviderResponseInput, StreamProviderResponseInput } from "../../shared/commands";
|
||||
import { FEATURE_FLAG_KEY, PROVIDER_AUTH_TYPE, PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, PROVIDER_REQUEST_STATE, type ProviderExecutionMode } from "../../shared/domain";
|
||||
@@ -35,6 +36,7 @@ export interface ProviderResponseRunnerDeps {
|
||||
adapters: ProviderAdapterRegistry;
|
||||
audit: AuditService;
|
||||
authBroker: AuthBrokerService;
|
||||
integrationKeys: IntegrationKeyStore;
|
||||
operations: OperationsService;
|
||||
rateLimitRegistry: ProviderRateLimitRegistry;
|
||||
responseObserver: ProviderResponseObserver;
|
||||
@@ -114,7 +116,7 @@ export class ProviderResponseRunner {
|
||||
if (!model) throw new Error("Provider model is required.");
|
||||
const owner: ProviderRow & { model: string } = { ...provider, model };
|
||||
const turn = this.deps.toolTurn ?? selectToolTurn(provider.provider_key);
|
||||
const credential = await resolveTurnCredential({ authBroker: this.deps.authBroker, orgId: input.orgId, provider });
|
||||
const credential = await resolveTurnCredential({ authBroker: this.deps.authBroker, integrationKeys: this.deps.integrationKeys, orgId: input.orgId, provider });
|
||||
const promptChars = JSON.stringify(input.messages).length;
|
||||
const requestId = this.deps.requestStore.create({
|
||||
orgId: input.orgId,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { ProviderStreamRendererEvent } from "../../shared/provider-request-
|
||||
import { AuditService } from "./audit-service";
|
||||
import { AuthBrokerService, type ProviderSecretVault } from "./auth-broker-service";
|
||||
import { OperationsService } from "./operations-service";
|
||||
import { IntegrationKeyStore } from "./integration-key-store";
|
||||
import { SecretVault } from "./secret-vault";
|
||||
import { providerAdapters, providerRateLimitStateFromError, type ProviderAdapterRegistry } from "./provider-adapters";
|
||||
import { ProviderBackgroundService } from "./provider-background-service";
|
||||
@@ -44,6 +45,7 @@ export class ProviderService {
|
||||
private readonly requestStore: ProviderRequestStore;
|
||||
private readonly background: ProviderBackgroundService;
|
||||
private readonly runner: ProviderResponseRunner;
|
||||
readonly integrationKeys: IntegrationKeyStore;
|
||||
|
||||
constructor(
|
||||
private readonly db: DatabaseSync,
|
||||
@@ -54,6 +56,7 @@ export class ProviderService {
|
||||
) {
|
||||
this.audit = new AuditService(db);
|
||||
this.authBroker = new AuthBrokerService(db, vault);
|
||||
this.integrationKeys = new IntegrationKeyStore(db, vault);
|
||||
this.operations = new OperationsService(db);
|
||||
this.rateLimitRegistry = new ProviderRateLimitRegistry(db);
|
||||
this.requestStore = new ProviderRequestStore(db);
|
||||
@@ -64,6 +67,7 @@ export class ProviderService {
|
||||
adapters,
|
||||
audit: this.audit,
|
||||
authBroker: this.authBroker,
|
||||
integrationKeys: this.integrationKeys,
|
||||
operations: this.operations,
|
||||
rateLimitRegistry: this.rateLimitRegistry,
|
||||
responseObserver: this.responseObserver,
|
||||
|
||||
@@ -26,7 +26,8 @@ import type { TurnCredential } from "../provider-tool-turn";
|
||||
import type { OAuthCredentials } from "../oauth/oauth-types";
|
||||
import { refreshCodex } from "../oauth/openai-codex-flow";
|
||||
import { refreshClaudePro } from "../oauth/anthropic-claude-flow";
|
||||
import { refreshAntigravity } from "../oauth/google-antigravity-flow";
|
||||
import { refreshAntigravity, requireAntigravityClientSecret } from "../oauth/google-antigravity-flow";
|
||||
import type { IntegrationKeyStore } from "../integration-key-store";
|
||||
import { refreshGitHubCopilotToken } from "../oauth/github-copilot-flow";
|
||||
import { refreshKimiCode } from "../oauth/kimi-code-flow";
|
||||
|
||||
@@ -53,7 +54,7 @@ function parseStoredOAuth(secret: string): StoredOAuth {
|
||||
return creds;
|
||||
}
|
||||
|
||||
function refreshOAuth(providerKey: string, creds: StoredOAuth): Promise<OAuthCredentials> {
|
||||
function refreshOAuth(providerKey: string, creds: StoredOAuth, keys: IntegrationKeyStore): Promise<OAuthCredentials> {
|
||||
switch (providerKey) {
|
||||
case "gpt-account":
|
||||
return refreshCodex(creds.refresh);
|
||||
@@ -61,7 +62,7 @@ function refreshOAuth(providerKey: string, creds: StoredOAuth): Promise<OAuthCre
|
||||
return refreshClaudePro(creds.refresh);
|
||||
case "google-antigravity":
|
||||
if (!creds.projectId) throw new Error("Antigravity credential is missing its project id; reconnect the provider.");
|
||||
return refreshAntigravity(creds.refresh, creds.projectId);
|
||||
return refreshAntigravity(creds.refresh, creds.projectId, requireAntigravityClientSecret(keys.read("google_antigravity_client_secret")));
|
||||
case "github-copilot":
|
||||
return refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl ?? undefined);
|
||||
case "kimi-code":
|
||||
@@ -110,6 +111,7 @@ function toTurnCredential(creds: StoredOAuth | OAuthCredentials): TurnCredential
|
||||
|
||||
export async function resolveTurnCredential(deps: {
|
||||
authBroker: AuthBrokerService;
|
||||
integrationKeys: IntegrationKeyStore;
|
||||
orgId: string;
|
||||
provider: ProviderRow;
|
||||
}): Promise<TurnCredential> {
|
||||
@@ -123,7 +125,7 @@ export async function resolveTurnCredential(deps: {
|
||||
const creds = parseStoredOAuth(secret);
|
||||
if (Date.now() < creds.expires) return toTurnCredential(creds);
|
||||
if (!deps.provider.credential_id) throw new Error("Provider credential id is missing; reconnect the provider.");
|
||||
const refreshed = await refreshOAuth(deps.provider.provider_key, creds);
|
||||
const refreshed = await refreshOAuth(deps.provider.provider_key, creds, deps.integrationKeys);
|
||||
deps.authBroker.updateProviderSecret({
|
||||
orgId: deps.orgId,
|
||||
providerKey: deps.provider.provider_key,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DatabaseSync, StatementSync } from "node:sqlite";
|
||||
import type { AppSnapshot } from "../../shared/domain";
|
||||
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
|
||||
import { IntegrationKeyStore } from "./integration-key-store";
|
||||
import { currentProviderUsageWindow } from "../../shared/provider-runtime-domain";
|
||||
|
||||
type Row = Record<string, unknown>;
|
||||
@@ -87,6 +88,7 @@ export class SnapshotService {
|
||||
activeOrgId,
|
||||
uiPreferences: this.resolveUiPreferences(),
|
||||
autonomousPaused: this.resolveAutonomousPaused(),
|
||||
integrationKeys: new IntegrationKeyStore(this.db).statuses(),
|
||||
organizations,
|
||||
employees: list(this.db, "employees", "created_at asc"),
|
||||
agents,
|
||||
|
||||
@@ -151,7 +151,7 @@ export class TaskExecutionService {
|
||||
.toolkitsForAgent(orgId, agent.id)
|
||||
.map(connectorDisplayName);
|
||||
const language = this.appLanguage();
|
||||
const externalTools = await loadComposioTools(this.db, orgId, agent.id);
|
||||
const externalTools = await loadComposioTools(this.db, this.providers.integrationKeys, orgId, agent.id);
|
||||
// Watermark for the reply-delivery check below: anything the agent posts
|
||||
// into the source thread from here on counts as its reply.
|
||||
const runStartedAt = new Date().toISOString();
|
||||
|
||||
Reference in new issue
Block a user