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
View File
@@ -125,6 +125,7 @@ describe("agent instruction wiring", () => {
});
const providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
providers.saveCredential({ orgId, providerId, secret: "sk-test", model: "gpt-5.4" });
providers.integrationKeys.write("composio", "ak_test");
agentId = organization.createAgent({
orgId, name: "Ada", role: "Engineer", department: "Engineering",
modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 1000, managerAgentId: null,
+8 -5
View File
@@ -3,7 +3,9 @@ import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import type { DatabaseSync } from "node:sqlite";
import { DatabaseConnection } from "../../src/main/database/connection";
import { IntegrationKeyStore } from "../../src/main/services/integration-key-store";
import { OrganizationService } from "../../src/main/services/organization-service";
import { ChatService } from "../../src/main/services/chat-service";
import { runAgentLoop } from "../../src/main/services/agent-run-loop";
@@ -16,9 +18,10 @@ interface ScriptedTurn {
toolCalls: ToolCall[];
}
function fakeProvider(script: ScriptedTurn[]) {
function fakeProvider(script: ScriptedTurn[], db: DatabaseSync) {
let index = 0;
return {
integrationKeys: new IntegrationKeyStore(db),
runToolTurn: async (): Promise<AgentTurnResult & { requestId: string }> => {
const turn = script[index] ?? { text: "done", toolCalls: [] };
index += 1;
@@ -85,7 +88,7 @@ describe("agent run loop (real tool dispatch)", () => {
let toolResultNotifications = 0;
const result = await runAgentLoop(
{
providers: fakeProvider(script),
providers: fakeProvider(script, connection.db),
tools: { db: connection.db, organization, chat: new ChatService(connection.db) },
onToolResults: () => { toolResultNotifications += 1; },
},
@@ -143,7 +146,7 @@ describe("agent run loop (real tool dispatch)", () => {
{ text: "done", toolCalls: [] },
];
const result = await runAgentLoop(
{ providers: fakeProvider(script), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
{ providers: fakeProvider(script, connection.db), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
{ orgId, providerId, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx },
);
// The tool returns an error string to the model; nothing is written outside the root.
@@ -180,7 +183,7 @@ describe("agent run loop (real tool dispatch)", () => {
];
await runAgentLoop(
{ providers: fakeProvider(script), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
{ providers: fakeProvider(script, connection.db), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
{ orgId, providerId, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx },
);
@@ -226,7 +229,7 @@ describe("agent run loop (real tool dispatch)", () => {
taskProjectId: null, taskGoalId: null, workspace: null,
};
await expect(runAgentLoop(
{ providers: fakeProvider([{ text: "", toolCalls: [] }]), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
{ providers: fakeProvider([{ text: "", toolCalls: [] }], connection.db), tools: { db: connection.db, organization, chat: new ChatService(connection.db) } },
{ orgId, providerId, workspaceId: null, instructions: "i", initialUserTurn: "u", toolContext: ctx },
)).rejects.toThrow(/empty turn/);
connection.close();
-17
View File
@@ -1,17 +0,0 @@
import { describe, expect, test } from "vitest";
import { composioApiKey, normalizeComposioApiKey } from "../../src/main/services/composio/composio-credentials";
describe("composio credential resolution", () => {
test("trims the embedded key", () => {
expect(normalizeComposioApiKey(" ak_live ")).toBe("ak_live");
});
test("a blank key resolves to null so callers report the integration as unconfigured", () => {
expect(normalizeComposioApiKey("")).toBeNull();
expect(normalizeComposioApiKey(" ")).toBeNull();
});
test("the hardcoded key resolves in every build", () => {
expect(composioApiKey()).toBe("***REMOVED***");
});
});
+48
View File
@@ -0,0 +1,48 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { loadComposioTools } from "../../src/main/services/composio/composio-tool-bridge";
import { IntegrationKeyStore } from "../../src/main/services/integration-key-store";
const plainVault = {
encryptSecret: (secret: string) => Buffer.from(secret, "utf8"),
decryptSecret: (ciphertext: Buffer) => ciphertext.toString("utf8"),
};
function ceoHarness() {
const connection = new DatabaseConnection(join(tmpdir(), `ac-composio-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
let orgId = "";
let ceoId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "Acme", employeeName: "Dana", mission: "m", goal: "g" });
const providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
ceoId = organization.createAgent({
orgId, name: "CEO", role: "CEO", department: "Executive",
modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 25000, managerAgentId: null, roleLevel: "c_suite",
});
});
return { connection, orgId, ceoId };
}
describe("composio tool loading", () => {
test("an agent without connector access loads no tools and needs no key", async () => {
const { connection, orgId, ceoId } = ceoHarness();
await expect(loadComposioTools(connection.db, new IntegrationKeyStore(connection.db, plainVault), orgId, ceoId)).resolves.toEqual([]);
connection.close();
});
test("an agent with connector access fails loudly when the Composio key is missing", async () => {
const { connection, orgId, ceoId } = ceoHarness();
const now = new Date().toISOString();
connection.db.prepare(`
insert into connector_accounts(id, org_id, toolkit_slug, toolkit_name, auth_config_id, status, created_at, updated_at)
values ('ca_1', ?, 'gmail', 'Gmail', 'ac_1', 'ACTIVE', ?, ?)
`).run(orgId, now, now);
await expect(loadComposioTools(connection.db, new IntegrationKeyStore(connection.db, plainVault), orgId, ceoId)).rejects.toThrow("no Composio API key is saved");
connection.close();
});
});
+9 -5
View File
@@ -2,17 +2,21 @@ import { describe, expect, test } from "vitest";
import { connectorDisplayName, connectorLogoUrl } from "../../src/main/services/composio/connector-brands";
describe("connector brands", () => {
test("known toolkits resolve a display name and a logo.dev URL", () => {
test("known toolkits resolve a display name and a logo.dev URL with the saved token", () => {
expect(connectorDisplayName("google_analytics")).toBe("Google Analytics");
expect(connectorLogoUrl("google_analytics")).toBe(
"https://img.logo.dev/analytics.google.com?token=***REMOVED***",
expect(connectorLogoUrl("google_analytics", "pk_test")).toBe(
"https://img.logo.dev/analytics.google.com?token=pk_test",
);
expect(connectorDisplayName("share_point")).toBe("SharePoint");
expect(connectorLogoUrl("stripe")).toContain("img.logo.dev/stripe.com");
expect(connectorLogoUrl("stripe", "pk_a&b")).toBe("https://img.logo.dev/stripe.com?token=pk_a%26b");
});
test("no saved token means no logo URL", () => {
expect(connectorLogoUrl("stripe", null)).toBeNull();
});
test("unknown toolkits prettify the slug and carry no logo", () => {
expect(connectorDisplayName("acme_billing")).toBe("Acme Billing");
expect(connectorLogoUrl("acme_billing")).toBeNull();
expect(connectorLogoUrl("acme_billing", "pk_test")).toBeNull();
});
});
+1 -1
View File
@@ -24,7 +24,7 @@ describe("domain services", () => {
const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57]);
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58]);
connection.close();
});
+7 -3
View File
@@ -54,6 +54,10 @@ function addDoneTask(h: Harness, title: string, origin: string | null, updatedAt
return taskId;
}
function hoursFromNow(hours: number): string {
return new Date(Date.now() + hours * 3_600_000).toISOString();
}
function reviews(h: Harness): Array<{ id: string; status: string }> {
return h.connection.db.prepare("select id, status from tasks where goal_id = ? and origin = 'goal_review' order by created_at")
.all(h.goalId) as Array<{ id: string; status: string }>;
@@ -84,15 +88,15 @@ describe("goal review service", () => {
const [reviewId] = h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization));
// CEO closes the review without creating tasks or completing the goal: silence, not a loop.
setTaskStatus(h, reviewId, TASK_STATUS.DONE, "2026-08-01T10:00:00.000Z");
setTaskStatus(h, reviewId, TASK_STATUS.DONE, hoursFromNow(1));
expect(h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization))).toEqual([]);
// Chatter never counts as progress.
addDoneTask(h, "Reply to CEO", "chat_wake", "2026-08-01T12:00:00.000Z");
addDoneTask(h, "Reply to CEO", "chat_wake", hoursFromNow(2));
expect(h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization))).toEqual([]);
// A real completed task after the last review re-arms the checkpoint.
addDoneTask(h, "Implement tenant module", null, "2026-08-01T13:00:00.000Z");
addDoneTask(h, "Implement tenant module", null, hoursFromNow(3));
expect(h.connection.transaction(() => spawnGoalReviews(h.connection.db, h.organization))).toHaveLength(1);
expect(reviews(h)).toHaveLength(2);
h.connection.close();
+59
View File
@@ -0,0 +1,59 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { IntegrationKeyStore } from "../../src/main/services/integration-key-store";
import type { IntegrationKeyName } from "../../src/shared/commands";
const reversingVault = {
encryptSecret: (secret: string) => Buffer.from([...secret].reverse().join(""), "utf8"),
decryptSecret: (ciphertext: Buffer) => [...ciphertext.toString("utf8")].reverse().join(""),
};
function openStore() {
const connection = new DatabaseConnection(join(tmpdir(), `ac-keys-${randomUUID()}.db`));
return { connection, store: new IntegrationKeyStore(connection.db, reversingVault) };
}
describe("integration key store", () => {
test("saves trimmed keys through the vault and reads them back", () => {
const { connection, store } = openStore();
store.write("composio", " ak_123 ");
const row = connection.db.prepare("select ciphertext from integration_keys where name = 'composio'").get() as { ciphertext: Uint8Array };
expect(Buffer.from(row.ciphertext).toString("utf8")).toBe("321_ka");
expect(store.read("composio")).toBe("ak_123");
expect(store.read("logo_dev")).toBeNull();
connection.close();
});
test("statuses list every known key in order with configured flags", () => {
const { connection, store } = openStore();
store.write("logo_dev", "pk_1");
const statuses = store.statuses();
expect(statuses.map((status) => [status.name, status.configured])).toEqual([
["composio", false],
["logo_dev", true],
["google_antigravity_client_secret", false],
]);
expect(statuses[1].updatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
connection.close();
});
test("clear removes a key and overwrite replaces it", () => {
const { connection, store } = openStore();
store.write("composio", "ak_old");
store.write("composio", "ak_new");
expect(store.read("composio")).toBe("ak_new");
store.clear("composio");
expect(store.read("composio")).toBeNull();
connection.close();
});
test("rejects blank values and unknown names", () => {
const { connection, store } = openStore();
expect(() => store.write("composio", " ")).toThrow("cannot be blank");
expect(() => store.write("stripe" as IntegrationKeyName, "x")).toThrow('Unknown integration key "stripe"');
connection.close();
});
});
+5 -5
View File
@@ -3,7 +3,7 @@ import { describe, expect, test } from "vitest";
import { interpretPollResponse } from "../../src/main/services/oauth/github-copilot-flow";
import { interpretKimiPollResponse } from "../../src/main/services/oauth/kimi-code-flow";
import { extractAccountId } from "../../src/main/services/oauth/openai-codex-flow";
import { readProjectId, resolveAntigravityClientSecret } from "../../src/main/services/oauth/google-antigravity-flow";
import { readProjectId, requireAntigravityClientSecret } from "../../src/main/services/oauth/google-antigravity-flow";
import { generatePkce } from "../../src/main/services/oauth/pkce";
import { parseAccessToken, parseAccountToken } from "../../src/main/services/provider-auth-profiles";
@@ -93,12 +93,12 @@ describe("antigravity project discovery", () => {
});
describe("antigravity oauth configuration", () => {
test("reads the client secret from runtime environment", () => {
expect(resolveAntigravityClientSecret({ AGENTCOMPANY_GOOGLE_ANTIGRAVITY_CLIENT_SECRET: " live-secret " })).toBe("live-secret");
test("passes a saved client secret through", () => {
expect(requireAntigravityClientSecret("live-secret")).toBe("live-secret");
});
test("requires an explicit client secret", () => {
expect(() => resolveAntigravityClientSecret({})).toThrow("AGENTCOMPANY_GOOGLE_ANTIGRAVITY_CLIENT_SECRET");
test("requires a client secret saved in Settings", () => {
expect(() => requireAntigravityClientSecret(null)).toThrow("Settings > Integrations");
});
});
+22 -2
View File
@@ -19,6 +19,7 @@ import { ProviderService } from "../../src/main/services/provider-service";
import { providerAdapters } from "../../src/main/services/provider-adapters";
import { AuthBrokerService } from "../../src/main/services/auth-broker-service";
import { loadConfiguredProvider } from "../../src/main/services/provider-internal";
import { IntegrationKeyStore } from "../../src/main/services/integration-key-store";
const CONVO: AgentTurnMessage[] = [
{ role: "user", text: "do it" },
@@ -228,6 +229,7 @@ describe("credential resolver", () => {
const organization = new OrganizationService(connection.db);
const providers = new ProviderService(connection.db, providerAdapters, testVault);
const authBroker = new AuthBrokerService(connection.db, testVault);
const integrationKeys = new IntegrationKeyStore(connection.db, testVault);
let orgId = "";
let openaiId = "";
let gptAccountId = "";
@@ -241,14 +243,32 @@ describe("credential resolver", () => {
});
return (async () => {
const apiKeyCred = await resolveTurnCredential({ authBroker, orgId, provider: loadConfiguredProvider(connection.db, orgId, openaiId) });
const apiKeyCred = await resolveTurnCredential({ authBroker, integrationKeys, orgId, provider: loadConfiguredProvider(connection.db, orgId, openaiId) });
expect(apiKeyCred).toEqual({ apiKey: "sk-live" });
const oauthCred = await resolveTurnCredential({ authBroker, orgId, provider: loadConfiguredProvider(connection.db, orgId, gptAccountId) });
const oauthCred = await resolveTurnCredential({ authBroker, integrationKeys, orgId, provider: loadConfiguredProvider(connection.db, orgId, gptAccountId) });
expect(oauthCred.accessToken).toBe("tok");
expect(oauthCred.accountId).toBe("acct_1");
connection.close();
})();
});
test("an expired Antigravity credential without a saved client secret points to Settings", async () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-cred-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
const providers = new ProviderService(connection.db, providerAdapters, testVault);
const authBroker = new AuthBrokerService(connection.db, testVault);
const integrationKeys = new IntegrationKeyStore(connection.db, testVault);
let orgId = "";
let antigravityId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "Acme", employeeName: "Dana", mission: "m", goal: "g" });
antigravityId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'google-antigravity'").get(orgId) as { id: string }).id;
providers.saveCredential({ orgId, providerId: antigravityId, secret: JSON.stringify({ access: "old", refresh: "r", expires: 0, projectId: "p" }), model: "gemini-3-pro" });
});
await expect(resolveTurnCredential({ authBroker, integrationKeys, orgId, provider: loadConfiguredProvider(connection.db, orgId, antigravityId) }))
.rejects.toThrow("Settings > Integrations");
connection.close();
});
});
describe("responses tool definitions", () => {