Ship Composio as a built-in integration and trim Settings

- Resolve the API key from cred.txt at the project root in development
  and the packaged resources directory in production; the file is the
  source of truth, so a rotated key replaces the stored one. Fixes the
  401 packaged builds hit when a stale database key won.
- Remove the user-facing Composio key editor; the key ships with the
  product and is not configurable.
- Remove the Migration console from Settings.
- Remove the Connectors and Home refresh buttons; connector statuses
  sync when the page opens.
This commit is contained in:
ZacharyZhang-NY committed 2026-07-07 01:20:21 -04:00
1 parent 947cd8605b
commit 256e0680c6
16 files changed
+53 -396

No files matched your search

+3
View File
@@ -8,6 +8,9 @@ files:
- out/**/*
- package.json
- resources/icon.png
extraResources:
- from: cred.txt
to: cred.txt
asar: true
mac:
category: public.app-category.business
+1 -14
View File
@@ -321,7 +321,7 @@ export function registerIpcHandlers(deps: IpcDeps): void {
}
});
const composioConnections = new ComposioConnectionService(deps.connection.db, process.cwd());
const composioConnections = new ComposioConnectionService(deps.connection.db);
const connectorGrants = new ConnectorGrantsService(deps.connection.db);
ipcMain.handle(IPC_CHANNELS.listComposioToolkits, async (_event, input) => {
@@ -362,19 +362,6 @@ export function registerIpcHandlers(deps: IpcDeps): void {
toolkitSlug: input.toolkitSlug,
})));
ipcMain.handle(IPC_CHANNELS.saveComposioSettings, (_event, input) => mutate(() => {
const now = new Date().toISOString();
const db = deps.connection.db;
const upsert = db.prepare(`
insert into composio_settings(org_id, key, value, updated_at) values (?, ?, ?, ?)
on conflict(org_id, key) do update set value = excluded.value, updated_at = excluded.updated_at
`);
upsert.run(input.orgId, "api_key", input.apiKey, now);
upsert.run(input.orgId, "toolkits", JSON.stringify(input.toolkits), now);
upsert.run(input.orgId, "entity_id", input.orgId, now);
return null;
}));
ipcMain.handle(IPC_CHANNELS.windowMinimize, () => deps.mainWindow.minimize());
ipcMain.handle(IPC_CHANNELS.windowMaximize, () => deps.mainWindow.isMaximized() ? deps.mainWindow.unmaximize() : deps.mainWindow.maximize());
ipcMain.handle(IPC_CHANNELS.windowClose, () => deps.mainWindow.close());
@@ -16,7 +16,7 @@ let catalogCache: { fetchedAt: number; toolkits: ComposioToolkitSummary[] } | nu
* Connectors page read local state instead of calling out on every frame.
*/
export class ComposioConnectionService {
constructor(private readonly db: DatabaseSync, private readonly credentialFileDir: string) {}
constructor(private readonly db: DatabaseSync) {}
listToolkits(orgId: string): Promise<ComposioToolkitSummaryDto[]> {
const client = this.requireClient(orgId);
@@ -81,7 +81,7 @@ export class ComposioConnectionService {
}
private requireClient(orgId: string): ComposioClient {
const apiKey = resolveComposioApiKey(this.db, orgId, this.credentialFileDir);
const apiKey = resolveComposioApiKey(this.db, orgId);
if (!apiKey) throw new Error("Composio API key is not configured for this company.");
return new ComposioClient(apiKey);
}
@@ -3,18 +3,19 @@ import { join } from "node:path";
import type { DatabaseSync } from "node:sqlite";
/**
* The org's Composio API key. A developer credential file next to the app
* (cred.txt, first line "Composio apikey: ak_...", gitignored) is the source
* of truth whenever it exists: rotating the key in the file takes effect on
* the next resolution, replacing whatever the database held. Packaged
* installs have no credential file and manage the key in Settings instead.
* The Composio integration key ships with the product: cred.txt (first line
* "Composio apikey: ak_...", gitignored) sits at the project root in
* development and inside the packaged app's resources directory in
* production. Wherever the file exists it is the source of truth — rotating
* the key in the file replaces whatever the database held on the next
* resolution. It is not user-configurable.
*/
export function resolveComposioApiKey(db: DatabaseSync, orgId: string, credentialFileDir: string = process.cwd()): string | null {
export function resolveComposioApiKey(db: DatabaseSync, orgId: string, credentialFileDirs: string[] = defaultCredentialDirs()): string | null {
const stored = db.prepare(
"select value from composio_settings where org_id = ? and key = 'api_key' limit 1",
).get(orgId) as { value: string } | undefined;
const fileKey = readCredentialFile(credentialFileDir);
const fileKey = readCredentialFile(credentialFileDirs);
if (!fileKey) return stored?.value ?? null;
if (stored?.value === fileKey) return fileKey;
@@ -28,10 +29,20 @@ export function resolveComposioApiKey(db: DatabaseSync, orgId: string, credentia
return fileKey;
}
function readCredentialFile(dir: string): string | null {
const path = join(dir, "cred.txt");
if (!existsSync(path)) return null;
const firstLine = readFileSync(path, "utf8").split("\n")[0] ?? "";
const key = firstLine.split(":").slice(1).join(":").trim();
return key || null;
/** Project root in dev; the app's resources directory when packaged. */
export function defaultCredentialDirs(): string[] {
const dirs = [process.cwd()];
if (process.resourcesPath) dirs.push(process.resourcesPath);
return dirs;
}
function readCredentialFile(dirs: string[]): string | null {
for (const dir of dirs) {
const path = join(dir, "cred.txt");
if (!existsSync(path)) continue;
const firstLine = readFileSync(path, "utf8").split("\n")[0] ?? "";
const key = firstLine.split(":").slice(1).join(":").trim();
if (key) return key;
}
return null;
}
-1
View File
@@ -82,7 +82,6 @@ const api: DesktopApi = {
setLegalHoldState: (input) => invoke(IPC_CHANNELS.setLegalHoldState, input),
createRetentionFreeze: (input) => invoke(IPC_CHANNELS.createRetentionFreeze, input),
setRetentionFreezeState: (input) => invoke(IPC_CHANNELS.setRetentionFreezeState, input),
saveComposioSettings: (input) => invoke(IPC_CHANNELS.saveComposioSettings, input),
listComposioToolkits: (input) => invoke(IPC_CHANNELS.listComposioToolkits, input),
connectComposioToolkit: (input) => invoke(IPC_CHANNELS.connectComposioToolkit, input),
refreshComposioAccounts: (input) => invoke(IPC_CHANNELS.refreshComposioAccounts, input),
+7 -11
View File
@@ -66,6 +66,12 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
if (pollTimer.current) clearTimeout(pollTimer.current);
}, []);
// Account statuses sync from Composio when the page opens; a failure here
// is already expressed by the catalog error state.
useEffect(() => {
void window.agentCompany.refreshComposioAccounts({ orgId });
}, [orgId]);
const refreshAccounts = () =>
actions.run(() => window.agentCompany.refreshComposioAccounts({ orgId }));
@@ -104,17 +110,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {
return (
<div style={{ maxWidth: 1060 }}>
<div className="row" style={{ marginBottom: 18 }}>
<div>
<h2 style={{ margin: 0, fontSize: 16, fontWeight: 600 }}>Connectors</h2>
<div style={{ fontSize: 12.5, color: "var(--text-3)", marginTop: 2 }}>
Company connections your agents can be granted to use.
</div>
</div>
<div style={{ marginLeft: "auto" }}>
<Button size="small" onClick={() => void refreshAccounts()}>Refresh</Button>
</div>
</div>
<h2 style={{ margin: "0 0 18px", fontSize: 16, fontWeight: 600 }}>Connectors</h2>
<ConnectedList accounts={accounts} grants={grants} agents={agents} onGrant={grant} onRevoke={revoke} />
-1
View File
@@ -49,7 +49,6 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
return (
<div className="page-wide" style={{ maxWidth: 1240 }}>
<TopbarActions>
<Button tone="ghost" onClick={() => void actions.reload()} title="Refresh"><Icon name="refresh" size={16} /></Button>
<Button tone="primary" onClick={() => setNewTaskOpen(true)}><Icon name="plus" size={16} /> New task</Button>
</TopbarActions>
<h1 className="page-h1">{greeting}, {firstName}</h1>
-7
View File
@@ -43,8 +43,6 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
const workspaces = useMemo(() => snapshot.workspaces.filter((item) => item.orgId === orgId), [snapshot.workspaces, orgId]);
const providerRequests = useMemo(() => snapshot.providerRequests.filter((item) => item.orgId === orgId), [snapshot.providerRequests, orgId]);
const hiringPolicies = useMemo(() => snapshot.hiringPolicies.filter((item) => item.orgId === orgId), [snapshot.hiringPolicies, orgId]);
const migrationReports = useMemo(() => snapshot.migrationReports.filter((report) => report.orgId === orgId), [snapshot.migrationReports, orgId]);
const rollbackSnapshots = useMemo(() => snapshot.rollbackSnapshots.filter((item) => item.orgId === orgId), [snapshot.rollbackSnapshots, orgId]);
return (
<div className="split-main">
@@ -83,17 +81,12 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
{tab === "advanced" && (
<AdvancedPanel
orgId={orgId}
employeeId={employee.id}
agents={agents}
providers={providers}
catalog={snapshot.providerCatalog}
workspaces={workspaces}
providerRequests={providerRequests}
hiringPolicies={hiringPolicies}
composioSettings={snapshot.composioSettings}
migrationReports={migrationReports}
rollbackSnapshots={rollbackSnapshots}
reconciliationItems={snapshot.migrationReconciliationItems.filter((item) => item.orgId === orgId)}
actions={actions}
/>
)}
@@ -1,49 +1,33 @@
import type {
AgentRecord,
HiringPolicyRecord,
MigrationReportRecord,
ProviderCatalogEntry,
ProviderRecord,
ProviderRequestRecord,
RollbackSnapshotRecord,
WorkspaceRecord,
} from "../../../../shared/domain";
import type { MigrationReconciliationItemRecord } from "../../../../shared/rollback-domain";
import type { ComposioSettingRecord } from "../../../../shared/snapshot-domain";
import type { SnapshotActions } from "../../app/useSnapshot";
import { BillingPanel } from "./BillingPanel";
import { ComposioPanel } from "./ComposioPanel";
import { MigrationPanel } from "./MigrationPanel";
import { ProviderRequestPanel } from "./ProviderRequestPanel";
/** The low-frequency operator surfaces, one section each. */
export function AdvancedPanel({
orgId,
employeeId,
agents,
providers,
catalog,
workspaces,
providerRequests,
hiringPolicies,
composioSettings,
migrationReports,
rollbackSnapshots,
reconciliationItems,
actions,
}: {
orgId: string;
employeeId: string;
agents: AgentRecord[];
providers: ProviderRecord[];
catalog: ProviderCatalogEntry[];
workspaces: WorkspaceRecord[];
providerRequests: ProviderRequestRecord[];
hiringPolicies: HiringPolicyRecord[];
composioSettings: ComposioSettingRecord[];
migrationReports: MigrationReportRecord[];
rollbackSnapshots: RollbackSnapshotRecord[];
reconciliationItems: MigrationReconciliationItemRecord[];
actions: SnapshotActions;
}) {
const connected = providers.filter((provider) => provider.enabled);
@@ -51,11 +35,8 @@ export function AdvancedPanel({
<div className="page">
<h1 className="page-h1">Advanced</h1>
<div className="section-label" style={{ margin: "16px 0 10px" }}>Composio</div>
<ComposioPanel orgId={orgId} settings={composioSettings} actions={actions} />
{connected.length > 0 && (
<div style={{ marginTop: 28 }}>
<div style={{ marginTop: 16 }}>
<ProviderRequestPanel
orgId={orgId}
providers={connected}
@@ -70,17 +51,6 @@ export function AdvancedPanel({
<div style={{ marginTop: 28 }}>
<BillingPanel agents={agents} providers={providers} providerRequests={providerRequests} hiringPolicies={hiringPolicies} />
</div>
<div style={{ marginTop: 28 }}>
<MigrationPanel
orgId={orgId}
employeeId={employeeId}
reports={migrationReports}
rollbackSnapshots={rollbackSnapshots}
reconciliationItems={reconciliationItems}
actions={actions}
/>
</div>
</div>
);
}
@@ -1,52 +0,0 @@
import { useMemo, useState, type FormEvent } from "react";
import { Button } from "../../shared/ui";
import type { SnapshotActions } from "../../app/useSnapshot";
import type { ComposioSettingRecord } from "../../../../shared/snapshot-domain";
function hasSavedKey(settings: ComposioSettingRecord[], orgId: string): boolean {
return settings.some((s) => s.orgId === orgId && s.key === "api_key" && s.value.length > 0);
}
/**
* The Composio project API key. Which toolkits agents may use is decided by
* connections and per-agent grants on the Connectors page, not here.
*/
export function ComposioPanel({
orgId,
settings,
actions,
}: {
orgId: string;
settings: ComposioSettingRecord[];
actions: SnapshotActions;
}) {
const keyConfigured = useMemo(() => hasSavedKey(settings, orgId), [settings, orgId]);
const [apiKey, setApiKey] = useState("");
const handleSave = (event: FormEvent) => {
event.preventDefault();
const key = apiKey.trim();
if (!key) return;
void actions.run(() => window.agentCompany.saveComposioSettings({
orgId,
apiKey: key,
toolkits: [],
})).then(() => setApiKey(""));
};
return (
<form onSubmit={handleSave} className="row" style={{ gap: 8, maxWidth: 480 }}>
<input
type="password"
className="input"
aria-label="Composio API key"
value={apiKey}
onChange={(event) => setApiKey(event.target.value)}
placeholder={keyConfigured ? "••••••••" : "ak_..."}
autoComplete="off"
style={{ flex: 1 }}
/>
<Button tone="primary" type="submit" disabled={!apiKey.trim()}>Save</Button>
</form>
);
}
@@ -1,228 +0,0 @@
import { useState, type FormEvent } from "react";
import { Button, Field, Icon, Pill } from "../../shared/ui";
import { Dialog } from "../../shared/Dialog";
import type { MigrationReconciliationItemRecord, MigrationReportRecord, RollbackSnapshotRecord } from "../../../../shared/domain";
import type { SnapshotActions } from "../../app/useSnapshot";
import { formatRelative } from "../types";
interface ParsedReport {
tableCounts: Record<string, number>;
tableChecksums: Record<string, string>;
idMaps: Record<string, string[]>;
failures: Array<{ table: string; reason: string }>;
rollbackPack?: { sourceDatabasePath: string; capturedAt: string; restoreStrategy: string };
}
export function MigrationPanel({
orgId,
employeeId,
reports,
rollbackSnapshots,
reconciliationItems,
actions,
}: {
orgId: string;
employeeId: string;
reports: MigrationReportRecord[];
rollbackSnapshots: RollbackSnapshotRecord[];
reconciliationItems: MigrationReconciliationItemRecord[];
actions: SnapshotActions;
}) {
const [open, setOpen] = useState(false);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.createMigrationReport({
orgId,
sourceDatabasePath: String(form.get("sourceDatabasePath") ?? "").trim(),
})).then((result) => { if (result) setOpen(false); });
};
return (
<div className="reveal-row">
<div className="row" style={{ marginBottom: 10 }}>
<div className="section-label">Migration</div>
<div className="reveal" style={{ marginLeft: "auto" }}>
<Button tone="primary" onClick={() => setOpen(true)}><Icon name="plus" size={16} /> Report</Button>
</div>
</div>
{reports.length > 0 && (
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{reports.map((report, index) => (
<ReportRow
key={report.id}
orgId={orgId}
employeeId={employeeId}
report={report}
actions={actions}
last={index === reports.length - 1}
/>
))}
</div>
)}
<section className="card" style={{ marginTop: 16, padding: 0, overflow: "hidden" }}>
<div className="row" style={{ padding: "14px 16px", borderBottom: "1px solid var(--border-soft)" }}>
<Icon name="refresh" size={14} color="var(--text-3)" />
<div style={{ fontSize: 14, fontWeight: 600 }}>Rollback snapshots</div>
</div>
{rollbackSnapshots.length > 0 ? rollbackSnapshots.map((snapshot, index) => (
<RollbackRow
key={snapshot.id}
orgId={orgId}
employeeId={employeeId}
snapshot={snapshot}
actions={actions}
last={index === rollbackSnapshots.length - 1}
/>
)) : <div style={{ padding: 16 }}><Pill>0 rollback snapshots</Pill></div>}
</section>
<section className="card" style={{ marginTop: 16, padding: 0, overflow: "hidden" }}>
<div className="row" style={{ padding: "14px 16px", borderBottom: "1px solid var(--border-soft)" }}>
<Icon name="list" size={14} color="var(--text-3)" />
<div style={{ fontSize: 14, fontWeight: 600 }}>Reconciliation</div>
</div>
{reconciliationItems.length > 0 ? reconciliationItems.map((item, index) => (
<ReconciliationRow
key={item.id}
orgId={orgId}
employeeId={employeeId}
item={item}
actions={actions}
last={index === reconciliationItems.length - 1}
/>
)) : <div style={{ padding: 16 }}><Pill>0 reconciliation items</Pill></div>}
</section>
<Dialog title="Migration report" open={open} submitLabel="Create" onClose={() => setOpen(false)} onSubmit={submit}>
<Field label="Source database path"><input className="input" name="sourceDatabasePath" required /></Field>
</Dialog>
</div>
);
}
function ReportRow({
orgId,
employeeId,
report,
actions,
last,
}: {
orgId: string;
employeeId: string;
report: MigrationReportRecord;
actions: SnapshotActions;
last: boolean;
}) {
const body = parseReport(report.reportJson);
const idCount = Object.values(body.idMaps).reduce((sum, ids) => sum + ids.length, 0);
return (
<div
className="table-row"
style={{ gridTemplateColumns: "minmax(0, 1.35fr) minmax(0, 1fr) 120px 88px 96px", borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={report.sourceRef}>{report.sourceRef}</div>
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{formatRelative(report.createdAt)}</div>
</div>
<div className="row" style={{ flexWrap: "wrap", gap: 4 }}>
{Object.entries(body.tableCounts).map(([table, count]) => (
<Pill key={table}>{table}: {count}</Pill>
))}
</div>
<Pill>{Object.keys(body.tableChecksums).length} checksums</Pill>
<div className="row" style={{ flexWrap: "wrap", gap: 4 }}>
<Pill>{idCount} ids</Pill>
{body.failures.length > 0 ? <Pill tone="amber">{body.failures.length} failures</Pill> : null}
</div>
<Button
size="small"
disabled={!body.rollbackPack}
onClick={() => void actions.run(() => window.agentCompany.createRollbackSnapshot({ orgId, migrationReportId: report.id, createdByEmployeeId: employeeId }))}
>
Snapshot
</Button>
</div>
);
}
function RollbackRow({
orgId,
employeeId,
snapshot,
actions,
last,
}: {
orgId: string;
employeeId: string;
snapshot: RollbackSnapshotRecord;
actions: SnapshotActions;
last: boolean;
}) {
const manifest = JSON.parse(snapshot.workspaceManifestJson) as { workspaces: unknown[]; files: unknown[] };
return (
<div
className="table-row"
style={{ gridTemplateColumns: "minmax(0, 1.2fr) minmax(0, .8fr) 96px 96px 72px", borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={snapshot.sourceDatabasePath}>{snapshot.sourceDatabasePath}</div>
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{formatRelative(snapshot.createdAt)}</div>
</div>
<Pill>{snapshot.sourceDatabaseSha256.slice(0, 12)}</Pill>
<Pill>{manifest.workspaces.length} workspaces</Pill>
<Pill>{manifest.files.length} files</Pill>
<Button
size="small"
disabled={snapshot.active}
onClick={() => void actions.run(() => window.agentCompany.setRollbackSnapshotActive({ orgId, snapshotId: snapshot.id, active: true, employeeId }))}
>
{snapshot.active ? "active" : "Set"}
</Button>
</div>
);
}
function ReconciliationRow({
orgId,
employeeId,
item,
actions,
last,
}: {
orgId: string;
employeeId: string;
item: MigrationReconciliationItemRecord;
actions: SnapshotActions;
last: boolean;
}) {
const resolved = item.state === "resolved";
return (
<div
className="table-row"
style={{ gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr) 86px 112px 84px", borderBottom: last ? "none" : "1px solid var(--border-soft)" }}
>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{item.targetType} · {item.targetId}</div>
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{formatRelative(item.createdAt)}</div>
</div>
<div style={{ fontSize: 12, color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{item.reason}</div>
<Pill tone={resolved ? "green" : "amber"}>{item.state}</Pill>
<Pill>{item.sourceEventId.slice(0, 12)}</Pill>
<Button
size="small"
disabled={resolved}
onClick={() => void actions.run(() => window.agentCompany.resolveMigrationReconciliation({ orgId, reconciliationItemId: item.id, employeeId }))}
>
{resolved ? "Resolved" : "Resolve"}
</Button>
</div>
);
}
function parseReport(reportJson: string): ParsedReport {
const parsed = JSON.parse(reportJson) as ParsedReport;
if (!parsed.tableCounts || !parsed.tableChecksums || !parsed.idMaps || !Array.isArray(parsed.failures)) {
throw new Error("Migration report is invalid.");
}
return parsed;
}
-6
View File
@@ -1,9 +1,3 @@
export interface SaveComposioSettingsInput {
orgId: string;
apiKey: string;
toolkits: string[];
}
export interface ComposioToolkitSummaryDto {
slug: string;
name: string;
-2
View File
@@ -63,7 +63,6 @@ import type {
RunTaskInput,
RevokeProviderCredentialInput,
SaveProviderCredentialInput,
SaveComposioSettingsInput,
ComposioToolkitSummaryDto,
ConnectComposioToolkitInput,
ConnectComposioToolkitResult,
@@ -179,7 +178,6 @@ export interface DesktopApi {
setLegalHoldState(input: SetLegalHoldStateInput): Promise<DesktopResult<AppSnapshot>>;
createRetentionFreeze(input: CreateRetentionFreezeInput): Promise<DesktopResult<AppSnapshot>>;
setRetentionFreezeState(input: SetRetentionFreezeStateInput): Promise<DesktopResult<AppSnapshot>>;
saveComposioSettings(input: SaveComposioSettingsInput): Promise<DesktopResult<AppSnapshot>>;
listComposioToolkits(input: ListComposioToolkitsInput): Promise<DesktopResult<ComposioToolkitSummaryDto[]>>;
connectComposioToolkit(input: ConnectComposioToolkitInput): Promise<DesktopResult<ConnectComposioToolkitResult>>;
refreshComposioAccounts(input: RefreshComposioAccountsInput): Promise<DesktopResult<AppSnapshot>>;
-1
View File
@@ -74,7 +74,6 @@ export const IPC_CHANNELS = {
deleteMeeting: "agentcompany:delete-meeting",
createSprint: "agentcompany:create-sprint",
deleteSprint: "agentcompany:delete-sprint",
saveComposioSettings: "agentcompany:save-composio-settings",
listComposioToolkits: "agentcompany:list-composio-toolkits",
connectComposioToolkit: "agentcompany:connect-composio-toolkit",
refreshComposioAccounts: "agentcompany:refresh-composio-accounts",
+1 -22
View File
@@ -3,7 +3,6 @@ import { mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { randomUUID } from "node:crypto";
import { DatabaseSync } from "node:sqlite";
test("onboards, hires, and creates work in the Electron shell", async () => {
const profileDir = join(tmpdir(), `agentcompany-new-e2e-${randomUUID()}`);
@@ -115,7 +114,7 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
// is reachable from the test machine.
await page.getByRole("button", { name: "Connectors", exact: true }).click();
await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible();
await expect(page.getByRole("button", { name: "Refresh" })).toBeVisible();
await expect(page.getByRole("button", { name: "Refresh" })).toHaveCount(0);
await page.getByRole("button", { name: "Settings" }).click();
await expect(page.getByText("AI CEO", { exact: true })).toBeVisible();
@@ -130,7 +129,6 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await expect(page.getByRole("button", { name: "Background" })).toBeVisible();
await page.getByLabel("Request prompt").fill("Draft a provider control memo.");
await page.getByLabel("Request instructions").fill("Use direct language.");
await expect(page.getByLabel("Composio API key")).toBeVisible();
await page.getByRole("button", { name: "Providers" }).click();
await page.getByLabel("OpenAI data policy").selectOption("zero_data_retention");
@@ -147,17 +145,6 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await expect(page.getByText("Never run").first()).toBeVisible();
await page.getByRole("button", { name: "Run tests" }).click();
await expect(page.getByText("1/1 passed").first()).toBeVisible();
const legacyPath = createLegacyDatabase();
await page.getByRole("button", { name: "Advanced" }).click();
await page.getByRole("button", { name: "Report" }).click();
await page.getByLabel("Source database path").fill(legacyPath);
await page.locator(".modal").getByRole("button", { name: "Create" }).click();
await expect(page.getByText("organizations: 1").first()).toBeVisible();
await page.getByRole("button", { name: "Snapshot" }).click();
await expect(page.getByText("Rollback snapshots", { exact: true })).toBeVisible();
await expect(page.getByText(/\d+ workspaces/).first()).toBeVisible();
await page.getByRole("button", { name: "Set", exact: true }).click();
await expect(page.getByRole("button", { name: "active", exact: true })).toBeDisabled();
// Virtual office page
await page.getByRole("button", { name: "Office" }).click();
@@ -176,14 +163,6 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
}
});
function createLegacyDatabase(): string {
const legacyPath = join(tmpdir(), `agentcompany-legacy-${randomUUID()}.db`);
const legacy = new DatabaseSync(legacyPath);
legacy.exec("create table organizations(id text primary key); insert into organizations(id) values ('legacy-org');");
legacy.close();
return legacyPath;
}
function createSkillDirectory(): string {
const root = join(tmpdir(), `agentcompany-skill-${randomUUID()}`);
mkdirSync(join(root, "checks"), { recursive: true });
+14 -5
View File
@@ -40,30 +40,39 @@ describe("composio credential resolution", () => {
test("seeds the database from the credential file", () => {
const h = harness();
writeCred(h, "ak_first");
expect(resolveComposioApiKey(h.connection.db, h.orgId, h.credDir)).toBe("ak_first");
expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBe("ak_first");
expect(storedKey(h)).toBe("ak_first");
});
test("a rotated credential file replaces the stale stored key", () => {
const h = harness();
writeCred(h, "ak_old");
resolveComposioApiKey(h.connection.db, h.orgId, h.credDir);
resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir]);
writeCred(h, "ak_rotated");
expect(resolveComposioApiKey(h.connection.db, h.orgId, h.credDir)).toBe("ak_rotated");
expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBe("ak_rotated");
expect(storedKey(h)).toBe("ak_rotated");
});
test("finds the credential file in a later directory when the first has none", () => {
const h = harness();
writeCred(h, "ak_resources");
const emptyDir = join(tmpdir(), `ac-cred-empty-${randomUUID()}`);
mkdirSync(emptyDir, { recursive: true });
expect(resolveComposioApiKey(h.connection.db, h.orgId, [emptyDir, h.credDir])).toBe("ak_resources");
expect(storedKey(h)).toBe("ak_resources");
});
test("without a credential file the stored key stands", () => {
const h = harness();
const now = new Date().toISOString();
h.connection.db.prepare(
"insert into composio_settings(org_id, key, value, updated_at) values (?, 'api_key', 'ak_settings', ?)",
).run(h.orgId, now);
expect(resolveComposioApiKey(h.connection.db, h.orgId, h.credDir)).toBe("ak_settings");
expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBe("ak_settings");
});
test("no file and no stored key yields null", () => {
const h = harness();
expect(resolveComposioApiKey(h.connection.db, h.orgId, h.credDir)).toBeNull();
expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBeNull();
});
});