Promote AgentCompany-New to the repository root and retire the old app
The legacy application - its src, tests, landing site, promo assets, docs, release tooling, CI workflows, and root configs - leaves the tree; it stays reachable in history. Everything under AgentCompany-New moves up unchanged: same package name, so the production userData/database path is untouched, and e2e/build scripts already resolved paths relative to the project. The developer cred.txt stays gitignored at its new location. Full verify:ci passes from the new root.
This commit is contained in:
1 parent
2350f744e3
commit
07d0b4587d
1904 files changed
+1133
-282075
No files matched your search
@@ -0,0 +1,194 @@
|
||||
import { net } from "electron";
|
||||
|
||||
const BASE_URL = "https://backend.composio.dev/api/v3";
|
||||
|
||||
export interface ComposioToolSchema {
|
||||
name: string;
|
||||
description: string;
|
||||
toolkitSlug: string | null;
|
||||
parameters: {
|
||||
type: string;
|
||||
properties: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ComposioToolkitSummary {
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
logo: string | null;
|
||||
categories: string[];
|
||||
authSchemes: string[];
|
||||
noAuth: boolean;
|
||||
toolsCount: number;
|
||||
}
|
||||
|
||||
export interface ComposioConnectedAccount {
|
||||
id: string;
|
||||
toolkitSlug: string;
|
||||
status: string;
|
||||
authConfigId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ComposioToolExecutionResult {
|
||||
data: Record<string, unknown>;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export class ComposioClient {
|
||||
constructor(private readonly apiKey: string) {}
|
||||
|
||||
/** The toolkit catalog, most popular first. */
|
||||
async listToolkits(limit: number): Promise<ComposioToolkitSummary[]> {
|
||||
const toolkits: ComposioToolkitSummary[] = [];
|
||||
let cursor: string | null = null;
|
||||
while (toolkits.length < limit) {
|
||||
const params = new URLSearchParams({ limit: String(Math.min(100, limit - toolkits.length)) });
|
||||
if (cursor) params.set("cursor", cursor);
|
||||
const response = await this.request("GET", `${BASE_URL}/toolkits?${params.toString()}`);
|
||||
const items = Array.isArray(response.items) ? response.items as Array<Record<string, unknown>> : [];
|
||||
for (const item of items) {
|
||||
const meta = (item.meta ?? {}) as Record<string, unknown>;
|
||||
toolkits.push({
|
||||
slug: String(item.slug ?? ""),
|
||||
name: String(item.name ?? ""),
|
||||
description: String(meta.description ?? "").slice(0, 200),
|
||||
logo: typeof meta.logo === "string" ? meta.logo : null,
|
||||
categories: Array.isArray(meta.categories)
|
||||
? (meta.categories as Array<Record<string, unknown>>).map((c) => String(c.name ?? c.id ?? "")).filter(Boolean)
|
||||
: [],
|
||||
authSchemes: Array.isArray(item.auth_schemes) ? (item.auth_schemes as unknown[]).map(String) : [],
|
||||
noAuth: item.no_auth === true,
|
||||
toolsCount: Number(meta.tools_count ?? 0),
|
||||
});
|
||||
}
|
||||
cursor = typeof response.next_cursor === "string" && response.next_cursor ? response.next_cursor : null;
|
||||
if (!cursor || items.length === 0) break;
|
||||
}
|
||||
return toolkits;
|
||||
}
|
||||
|
||||
async getTools(toolkits: string[]): Promise<ComposioToolSchema[]> {
|
||||
const all: ComposioToolSchema[] = [];
|
||||
for (const toolkit of toolkits) {
|
||||
const params = new URLSearchParams({ toolkit_slug: toolkit, limit: "20" });
|
||||
const url = `${BASE_URL}/tools?${params.toString()}`;
|
||||
const response = await this.request("GET", url);
|
||||
if (Array.isArray(response.items)) {
|
||||
all.push(...(response.items as Array<Record<string, unknown>>).map(normalizeToolSchema));
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/** Creates a Composio-managed auth config for a toolkit. */
|
||||
async createAuthConfig(toolkitSlug: string): Promise<string> {
|
||||
const response = await this.request("POST", `${BASE_URL}/auth_configs`, {
|
||||
toolkit: { slug: toolkitSlug },
|
||||
});
|
||||
const authConfig = (response.auth_config ?? {}) as Record<string, unknown>;
|
||||
const id = String(authConfig.id ?? "");
|
||||
if (!id) throw new Error("Composio auth config creation returned no id.");
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Starts a connection for a user; the redirect URL completes it in the browser. */
|
||||
async createConnectedAccount(input: { authConfigId: string; userId: string }): Promise<{
|
||||
id: string;
|
||||
status: string;
|
||||
redirectUrl: string | null;
|
||||
}> {
|
||||
const response = await this.request("POST", `${BASE_URL}/connected_accounts`, {
|
||||
auth_config: { id: input.authConfigId },
|
||||
connection: { user_id: input.userId },
|
||||
});
|
||||
return {
|
||||
id: String(response.id ?? ""),
|
||||
status: String(response.status ?? ""),
|
||||
redirectUrl: typeof response.redirect_url === "string" ? response.redirect_url : null,
|
||||
};
|
||||
}
|
||||
|
||||
async listConnectedAccounts(userId: string): Promise<ComposioConnectedAccount[]> {
|
||||
const params = new URLSearchParams({ user_ids: userId, limit: "50" });
|
||||
const response = await this.request("GET", `${BASE_URL}/connected_accounts?${params.toString()}`);
|
||||
if (!Array.isArray(response.items)) return [];
|
||||
return (response.items as Array<Record<string, unknown>>).map((item) => {
|
||||
const toolkit = (item.toolkit ?? {}) as Record<string, unknown>;
|
||||
const authConfig = (item.auth_config ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
id: String(item.id ?? ""),
|
||||
toolkitSlug: String(toolkit.slug ?? "").toLowerCase(),
|
||||
status: String(item.status ?? ""),
|
||||
authConfigId: String(authConfig.id ?? ""),
|
||||
createdAt: String(item.created_at ?? ""),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async executeTool(input: {
|
||||
toolName: string;
|
||||
arguments: Record<string, unknown>;
|
||||
userId: string;
|
||||
connectedAccountId?: string;
|
||||
}): Promise<ComposioToolExecutionResult> {
|
||||
const url = `${BASE_URL}/tools/execute/${encodeURIComponent(input.toolName)}`;
|
||||
const body: Record<string, unknown> = {
|
||||
user_id: input.userId,
|
||||
arguments: input.arguments,
|
||||
};
|
||||
if (input.connectedAccountId) body.connected_account_id = input.connectedAccountId;
|
||||
const response = await this.request("POST", url, body);
|
||||
return {
|
||||
data: typeof response === "object" && response !== null ? response as Record<string, unknown> : {},
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
private async request(method: string, url: string, body?: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = net.request({ method, url });
|
||||
request.setHeader("x-api-key", this.apiKey);
|
||||
request.setHeader("Accept", "application/json");
|
||||
if (body) request.setHeader("Content-Type", "application/json");
|
||||
|
||||
let responseData = "";
|
||||
request.on("response", (response) => {
|
||||
response.on("data", (chunk) => { responseData += chunk.toString(); });
|
||||
response.on("end", () => {
|
||||
if (response.statusCode && response.statusCode >= 400) {
|
||||
reject(new Error(`Composio API ${method} ${url} returned ${response.statusCode}: ${responseData.slice(0, 200)}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(responseData) as Record<string, unknown>);
|
||||
} catch {
|
||||
reject(new Error(`Composio API returned non-JSON response: ${responseData.slice(0, 200)}`));
|
||||
}
|
||||
});
|
||||
response.on("error", reject);
|
||||
});
|
||||
request.on("error", reject);
|
||||
if (body) request.write(JSON.stringify(body));
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeToolSchema(raw: Record<string, unknown>): ComposioToolSchema {
|
||||
const params = raw.input_parameters ?? raw.parameters ?? raw.inputSchema ?? {};
|
||||
const schema = typeof params === "object" && params !== null ? params as Record<string, unknown> : {};
|
||||
const toolkit = (raw.toolkit ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
name: String(raw.slug ?? raw.name ?? ""),
|
||||
description: String(raw.description ?? "").slice(0, 300),
|
||||
toolkitSlug: typeof toolkit.slug === "string" ? toolkit.slug : null,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: (schema.properties ?? {}) as Record<string, unknown>,
|
||||
required: Array.isArray(schema.required) ? schema.required as string[] : [],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { ComposioToolkitSummaryDto, ConnectComposioToolkitResult } from "../../../shared/commands";
|
||||
import { ComposioClient, type ComposioToolkitSummary } from "./composio-client";
|
||||
import { resolveComposioApiKey } from "./composio-credentials";
|
||||
import { getComposioUserId } from "./composio-tool-bridge";
|
||||
import { normalizeSlug } from "./connector-grants";
|
||||
|
||||
const TOOLKIT_CATALOG_LIMIT = 200;
|
||||
const CATALOG_TTL_MS = 10 * 60 * 1000;
|
||||
|
||||
let catalogCache: { fetchedAt: number; toolkits: ComposioToolkitSummary[] } | null = null;
|
||||
|
||||
/**
|
||||
* Company-level Composio connections. One connected account per toolkit per
|
||||
* organization, mirrored into connector_accounts so agent runs and the
|
||||
* Connectors page read local state instead of calling out on every frame.
|
||||
*/
|
||||
export class ComposioConnectionService {
|
||||
constructor(private readonly db: DatabaseSync, private readonly credentialFileDir: string) {}
|
||||
|
||||
listToolkits(orgId: string): Promise<ComposioToolkitSummaryDto[]> {
|
||||
const client = this.requireClient(orgId);
|
||||
return this.catalog(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts (or restarts) the company's connection for a toolkit: creates a
|
||||
* Composio-managed auth config once per toolkit, initiates the connected
|
||||
* account for the company user, and records the pending account locally.
|
||||
* The returned redirect URL completes authorization in the browser.
|
||||
*/
|
||||
async connectToolkit(orgId: string, toolkitSlug: string, toolkitName: string): Promise<ConnectComposioToolkitResult> {
|
||||
const slug = normalizeSlug(toolkitSlug);
|
||||
const client = this.requireClient(orgId);
|
||||
const authConfigId = await this.ensureAuthConfig(client, orgId, slug);
|
||||
const account = await client.createConnectedAccount({
|
||||
authConfigId,
|
||||
userId: getComposioUserId(this.db, orgId),
|
||||
});
|
||||
if (!account.id) throw new Error("Composio did not return a connected account id.");
|
||||
const now = new Date().toISOString();
|
||||
this.db.prepare(`
|
||||
insert into connector_accounts(id, org_id, toolkit_slug, toolkit_name, auth_config_id, status, created_at, updated_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
on conflict(org_id, toolkit_slug) do update set
|
||||
id = excluded.id,
|
||||
toolkit_name = excluded.toolkit_name,
|
||||
auth_config_id = excluded.auth_config_id,
|
||||
status = excluded.status,
|
||||
updated_at = excluded.updated_at
|
||||
`).run(account.id, orgId, slug, toolkitName.trim() || slug, authConfigId, account.status || "INITIATED", now, now);
|
||||
return { accountId: account.id, status: account.status, redirectUrl: account.redirectUrl };
|
||||
}
|
||||
|
||||
/** Pulls account statuses from Composio and updates the local mirror. */
|
||||
async refreshAccounts(orgId: string): Promise<void> {
|
||||
const client = this.requireClient(orgId);
|
||||
const accounts = await client.listConnectedAccounts(getComposioUserId(this.db, orgId));
|
||||
const now = new Date().toISOString();
|
||||
for (const account of accounts) {
|
||||
if (!account.toolkitSlug) continue;
|
||||
this.db.prepare(`
|
||||
insert into connector_accounts(id, org_id, toolkit_slug, toolkit_name, auth_config_id, status, created_at, updated_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
on conflict(org_id, toolkit_slug) do update set
|
||||
id = excluded.id,
|
||||
auth_config_id = excluded.auth_config_id,
|
||||
status = excluded.status,
|
||||
updated_at = excluded.updated_at
|
||||
`).run(
|
||||
account.id,
|
||||
orgId,
|
||||
account.toolkitSlug,
|
||||
account.toolkitSlug,
|
||||
account.authConfigId,
|
||||
account.status,
|
||||
account.createdAt || now,
|
||||
now,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private requireClient(orgId: string): ComposioClient {
|
||||
const apiKey = resolveComposioApiKey(this.db, orgId, this.credentialFileDir);
|
||||
if (!apiKey) throw new Error("Composio API key is not configured for this company.");
|
||||
return new ComposioClient(apiKey);
|
||||
}
|
||||
|
||||
private async ensureAuthConfig(client: ComposioClient, orgId: string, slug: string): Promise<string> {
|
||||
const key = `auth_config:${slug}`;
|
||||
const existing = this.db.prepare(
|
||||
"select value from composio_settings where org_id = ? and key = ? limit 1",
|
||||
).get(orgId, key) as { value: string } | undefined;
|
||||
if (existing?.value) return existing.value;
|
||||
const authConfigId = await client.createAuthConfig(slug);
|
||||
this.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
|
||||
`).run(orgId, key, authConfigId, new Date().toISOString());
|
||||
return authConfigId;
|
||||
}
|
||||
|
||||
private async catalog(client: ComposioClient): Promise<ComposioToolkitSummaryDto[]> {
|
||||
if (catalogCache && Date.now() - catalogCache.fetchedAt < CATALOG_TTL_MS) {
|
||||
return catalogCache.toolkits;
|
||||
}
|
||||
const toolkits = await client.listToolkits(TOOLKIT_CATALOG_LIMIT);
|
||||
catalogCache = { fetchedAt: Date.now(), toolkits };
|
||||
return toolkits;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
|
||||
/**
|
||||
* The org's Composio API key. When the database has none, a developer
|
||||
* credential file next to the app (cred.txt, first line "Composio apikey:
|
||||
* ak_...", gitignored) seeds it once for this org — the product owner's
|
||||
* local path. Packaged installs configure the key in Settings instead; a
|
||||
* missing file simply yields null.
|
||||
*/
|
||||
export function resolveComposioApiKey(db: DatabaseSync, orgId: string, credentialFileDir: string = process.cwd()): 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;
|
||||
if (stored?.value) return stored.value;
|
||||
|
||||
const path = join(credentialFileDir, "cred.txt");
|
||||
if (!existsSync(path)) return null;
|
||||
const firstLine = readFileSync(path, "utf8").split("\n")[0] ?? "";
|
||||
const key = firstLine.split(":").slice(1).join(":").trim();
|
||||
if (!key) return null;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
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(orgId, "api_key", key, now);
|
||||
upsert.run(orgId, "entity_id", orgId, now);
|
||||
return key;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { ToolDefinition, ToolCall } from "../provider-tool-turn";
|
||||
import { ComposioClient, type ComposioToolSchema } from "./composio-client";
|
||||
import { resolveComposioApiKey } from "./composio-credentials";
|
||||
import { ConnectorGrantsService } from "./connector-grants";
|
||||
|
||||
const COMPOSIO_TOOL_PREFIX = "composio_";
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
interface CacheEntry {
|
||||
tools: ToolDefinition[];
|
||||
fetchedAt: number;
|
||||
toolkitsKey: string;
|
||||
}
|
||||
|
||||
const toolCache = new Map<string, CacheEntry>();
|
||||
/** tool slug (upper case) → toolkit slug, learned from the tools listing. */
|
||||
const toolkitByTool = new Map<string, string>();
|
||||
|
||||
export function isComposioTool(toolName: string): boolean {
|
||||
return toolName.startsWith(COMPOSIO_TOOL_PREFIX);
|
||||
}
|
||||
|
||||
export function composioToolName(composioName: string): string {
|
||||
return `${COMPOSIO_TOOL_PREFIX}${composioName.toLowerCase()}`;
|
||||
}
|
||||
|
||||
function originalToolName(prefixedName: string): string {
|
||||
return prefixedName.slice(COMPOSIO_TOOL_PREFIX.length).toUpperCase();
|
||||
}
|
||||
|
||||
/** Toolkit a tool belongs to: learned mapping first, slug prefix as contract. */
|
||||
function toolkitForTool(toolSlug: string): string {
|
||||
return toolkitByTool.get(toolSlug) ?? toolSlug.split("_")[0].toLowerCase();
|
||||
}
|
||||
|
||||
export function composioSchemaToToolDefinition(schema: ComposioToolSchema): ToolDefinition {
|
||||
return {
|
||||
name: composioToolName(schema.name),
|
||||
description: schema.description,
|
||||
parameters: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: schema.parameters.properties,
|
||||
required: schema.parameters.required ?? [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The connector tools one agent is allowed to carry into a run: the
|
||||
* 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 = getComposioApiKey(db, orgId);
|
||||
if (!apiKey) return [];
|
||||
const grants = new ConnectorGrantsService(db);
|
||||
const toolkits = grants.toolkitsForAgent(orgId, agentId);
|
||||
if (toolkits.length === 0) return [];
|
||||
const toolkitsKey = toolkits.join(",");
|
||||
const cacheKey = `${orgId}:${toolkitsKey}`;
|
||||
const cached = toolCache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
||||
return cached.tools;
|
||||
}
|
||||
try {
|
||||
const client = new ComposioClient(apiKey);
|
||||
const schemas = await client.getTools(toolkits);
|
||||
for (const schema of schemas) {
|
||||
if (schema.toolkitSlug) toolkitByTool.set(schema.name.toUpperCase(), schema.toolkitSlug.toLowerCase());
|
||||
}
|
||||
const tools = schemas.map(composioSchemaToToolDefinition);
|
||||
toolCache.set(cacheKey, { tools, fetchedAt: Date.now(), toolkitsKey });
|
||||
return tools;
|
||||
} catch {
|
||||
return cached?.tools ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeComposioTool(
|
||||
db: DatabaseSync,
|
||||
orgId: string,
|
||||
agentId: string,
|
||||
call: ToolCall,
|
||||
): Promise<string> {
|
||||
const apiKey = getComposioApiKey(db, orgId);
|
||||
if (!apiKey) return "ERROR: Composio API key is not configured for this company.";
|
||||
const toolSlug = originalToolName(call.name);
|
||||
const toolkit = toolkitForTool(toolSlug);
|
||||
const grants = new ConnectorGrantsService(db);
|
||||
const allowed = grants.toolkitsForAgent(orgId, agentId);
|
||||
if (!allowed.includes(toolkit)) {
|
||||
return `ERROR: You have no grant for the "${toolkit}" connector. Ask the CEO to grant it to you (grant_connector).`;
|
||||
}
|
||||
const account = db.prepare(
|
||||
"select id from connector_accounts where org_id = ? and toolkit_slug = ? and status = 'ACTIVE'",
|
||||
).get(orgId, toolkit) as { id: string } | undefined;
|
||||
if (!account) {
|
||||
return `ERROR: The "${toolkit}" connector is no longer connected. Ask the board to reconnect it on the Connectors page.`;
|
||||
}
|
||||
const client = new ComposioClient(apiKey);
|
||||
try {
|
||||
const result = await client.executeTool({
|
||||
toolName: toolSlug,
|
||||
arguments: call.arguments,
|
||||
userId: getComposioUserId(db, orgId),
|
||||
connectedAccountId: account.id,
|
||||
});
|
||||
if (result.error) return `ERROR: ${result.error}`;
|
||||
return JSON.stringify(result.data);
|
||||
} catch (error) {
|
||||
return `ERROR: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getComposioApiKey(db: DatabaseSync, orgId: string): string | null {
|
||||
return resolveComposioApiKey(db, orgId);
|
||||
}
|
||||
|
||||
export function getComposioUserId(db: DatabaseSync, orgId: string): string {
|
||||
const row = db.prepare(
|
||||
"select value from composio_settings where org_id = ? and key = 'entity_id' limit 1",
|
||||
).get(orgId) as { value: string } | undefined;
|
||||
return row?.value ?? orgId;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { createId } from "../../../shared/ids";
|
||||
|
||||
export interface ConnectorAccountRow {
|
||||
id: string;
|
||||
orgId: string;
|
||||
toolkitSlug: string;
|
||||
toolkitName: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ConnectorGrantRow {
|
||||
id: string;
|
||||
orgId: string;
|
||||
agentId: string;
|
||||
toolkitSlug: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which agents may use which connected toolkits. Connections belong to the
|
||||
* company (one connected account per toolkit per org); the CEO hands
|
||||
* individual toolkits to individual agents, and both the tool loadout and
|
||||
* the execution guard read from these grants.
|
||||
*/
|
||||
export class ConnectorGrantsService {
|
||||
constructor(private readonly db: DatabaseSync) {}
|
||||
|
||||
grant(input: { orgId: string; agentId: string; toolkitSlug: string; grantedByType: "agent" | "employee"; grantedById: string | null }): void {
|
||||
const agent = this.db.prepare("select id from agents where id = ? and org_id = ? and lifecycle = 'active'")
|
||||
.get(input.agentId, input.orgId);
|
||||
if (!agent) throw new Error("Grant target must be an active agent in this organization.");
|
||||
const slug = normalizeSlug(input.toolkitSlug);
|
||||
const account = this.db.prepare(
|
||||
"select id from connector_accounts where org_id = ? and toolkit_slug = ? and status = 'ACTIVE'",
|
||||
).get(input.orgId, slug);
|
||||
if (!account) throw new Error(`No active connection for toolkit "${slug}". Connect it on the Connectors page first.`);
|
||||
this.db.prepare(`
|
||||
insert into agent_connector_grants(id, org_id, agent_id, toolkit_slug, granted_by_type, granted_by_id, created_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?)
|
||||
on conflict(org_id, agent_id, toolkit_slug) do nothing
|
||||
`).run(createId("cgr"), input.orgId, input.agentId, slug, input.grantedByType, input.grantedById, new Date().toISOString());
|
||||
}
|
||||
|
||||
revoke(input: { orgId: string; agentId: string; toolkitSlug: string }): void {
|
||||
this.db.prepare("delete from agent_connector_grants where org_id = ? and agent_id = ? and toolkit_slug = ?")
|
||||
.run(input.orgId, input.agentId, normalizeSlug(input.toolkitSlug));
|
||||
}
|
||||
|
||||
/** Toolkits this agent may use: granted AND actively connected. */
|
||||
toolkitsForAgent(orgId: string, agentId: string): string[] {
|
||||
const rows = this.db.prepare(`
|
||||
select g.toolkit_slug as slug
|
||||
from agent_connector_grants g
|
||||
join connector_accounts a on a.org_id = g.org_id and a.toolkit_slug = g.toolkit_slug and a.status = 'ACTIVE'
|
||||
where g.org_id = ? and g.agent_id = ?
|
||||
order by g.toolkit_slug asc
|
||||
`).all(orgId, agentId) as Array<{ slug: string }>;
|
||||
return rows.map((row) => row.slug);
|
||||
}
|
||||
|
||||
activeAccounts(orgId: string): ConnectorAccountRow[] {
|
||||
const rows = this.db.prepare(`
|
||||
select id, org_id, toolkit_slug, toolkit_name, status
|
||||
from connector_accounts
|
||||
where org_id = ? and status = 'ACTIVE'
|
||||
order by toolkit_slug asc
|
||||
`).all(orgId) as Array<{ id: string; org_id: string; toolkit_slug: string; toolkit_name: string; status: string }>;
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
orgId: row.org_id,
|
||||
toolkitSlug: row.toolkit_slug,
|
||||
toolkitName: row.toolkit_name,
|
||||
status: row.status,
|
||||
}));
|
||||
}
|
||||
|
||||
grantsForOrg(orgId: string): ConnectorGrantRow[] {
|
||||
const rows = this.db.prepare(`
|
||||
select id, org_id, agent_id, toolkit_slug
|
||||
from agent_connector_grants
|
||||
where org_id = ?
|
||||
order by toolkit_slug asc
|
||||
`).all(orgId) as Array<{ id: string; org_id: string; agent_id: string; toolkit_slug: string }>;
|
||||
return rows.map((row) => ({ id: row.id, orgId: row.org_id, agentId: row.agent_id, toolkitSlug: row.toolkit_slug }));
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSlug(slug: string): string {
|
||||
const normalized = slug.trim().toLowerCase();
|
||||
if (!normalized) throw new Error("Toolkit slug is required.");
|
||||
return normalized;
|
||||
}
|
||||
Reference in new issue
Block a user