godcrm/backend/services/agent-tools/connector-tools.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

222 lines
7.2 KiB
JavaScript

/**
* Connector-backed tool definitions + handlers — ADR-0028 Phase 4 (a).
*
* These tools exercise Space Connectors as their auth source. They are
* registered with `requires_connector` so the executor pre-flights the
* lookup; the handler reads the access_token from `context.injected_connector`.
*
* Add a new connector-backed tool by:
* 1. Append a definition to `CONNECTOR_TOOL_DEFS` with `requires_connector`.
* 2. Register a handler in `connectorToolHandlers`.
* 3. Re-export from `tool-definitions/index.js` (already imports this file).
*
* Security: the access_token comes via `context.injected_connector.access_token`
* — never logged, never returned in tool output, never passed to LLM.
*/
import axios from 'axios';
import { aiLogger } from '../../utils/logger.js';
const log = aiLogger.child({ module: 'connector_tools' });
// ─── Tool definitions (OpenAI function-calling schema) ──────────────
export const CONNECTOR_TOOL_DEFS = [
{
type: 'function',
requires_connector: 'figma',
function: {
name: 'figma_get_file',
description:
'Fetch a Figma file metadata + node tree using the active Figma connector ' +
'in the current space. Returns name, lastModified, document outline.',
parameters: {
type: 'object',
properties: {
file_key: {
type: 'string',
description: 'Figma file key (the `XXX` in figma.com/file/XXX/...).',
},
depth: {
type: 'number',
description: 'How deep to traverse the node tree (default 1, max 4).',
},
},
required: ['file_key'],
},
},
},
{
type: 'function',
requires_connector: 'slack',
function: {
name: 'slack_post_message',
description:
'Post a message to a Slack channel using the active Slack connector in the current space.',
parameters: {
type: 'object',
properties: {
channel: {
type: 'string',
description: 'Channel ID (preferred) or name with leading #.',
},
text: {
type: 'string',
description: 'Message text. Slack mrkdwn supported.',
},
},
required: ['channel', 'text'],
},
},
},
{
type: 'function',
requires_connector: 'github',
function: {
name: 'github_get_user',
description:
'Return authenticated user profile (login, name, email, public_repos) ' +
'via the active GitHub connector in the current space. Smoke test for ' +
'GitHub connector wiring.',
parameters: { type: 'object', properties: {} },
},
},
];
// ─── Requirements lookup (consumed by executor) ─────────────────────
//
// Map: tool_name → required connector type_slug. Built once at import.
export const connectorRequirements = Object.fromEntries(
CONNECTOR_TOOL_DEFS.filter((t) => t.requires_connector).map((t) => [
t.function.name,
t.requires_connector,
])
);
// ─── Handlers ───────────────────────────────────────────────────────
function getInjected(context, expected) {
const inj = context?.injected_connector;
if (!inj || inj.type_slug !== expected || !inj.access_token) {
// Should never happen — executor pre-flights. Defensive belt.
return null;
}
return inj;
}
async function figma_get_file(args, _userId, context) {
const inj = getInjected(context, 'figma');
if (!inj) return { error: 'connector_not_injected', expected: 'figma' };
const fileKey = args?.file_key;
if (!fileKey || typeof fileKey !== 'string') {
return { error: 'bad_args', message: 'file_key is required' };
}
const depth = Math.min(Math.max(Number(args?.depth ?? 1), 1), 4);
try {
const res = await axios.get(`https://api.figma.com/v1/files/${encodeURIComponent(fileKey)}`, {
params: { depth },
headers: { Authorization: `Bearer ${inj.access_token}` },
timeout: 20_000,
validateStatus: () => true,
});
if (res.status !== 200) {
return { error: `figma_http_${res.status}`, message: res.data?.err || res.data?.message || null };
}
const data = res.data || {};
return {
name: data.name,
lastModified: data.lastModified,
version: data.version,
thumbnailUrl: data.thumbnailUrl,
role: data.role,
document_id: data.document?.id,
top_level_pages: (data.document?.children || []).map((p) => ({
id: p.id,
name: p.name,
type: p.type,
child_count: Array.isArray(p.children) ? p.children.length : 0,
})),
account_label: inj.account_label,
};
} catch (err) {
log.warn({ err: err?.message, fileKey }, 'figma_get_file failed');
return { error: 'figma_request_failed', message: err?.message || 'unknown' };
}
}
async function slack_post_message(args, _userId, context) {
const inj = getInjected(context, 'slack');
if (!inj) return { error: 'connector_not_injected', expected: 'slack' };
const channel = args?.channel;
const text = args?.text;
if (!channel || !text) {
return { error: 'bad_args', message: 'channel and text are required' };
}
try {
const { data } = await axios.post(
'https://slack.com/api/chat.postMessage',
{ channel, text },
{
headers: {
Authorization: `Bearer ${inj.access_token}`,
'Content-Type': 'application/json; charset=utf-8',
},
timeout: 10_000,
validateStatus: () => true,
}
);
if (!data?.ok) {
return { error: `slack_${data?.error || 'unknown'}`, message: data?.error || null };
}
return {
ok: true,
channel: data.channel,
ts: data.ts,
account_label: inj.account_label,
};
} catch (err) {
log.warn({ err: err?.message, channel }, 'slack_post_message failed');
return { error: 'slack_request_failed', message: err?.message || 'unknown' };
}
}
async function github_get_user(_args, _userId, context) {
const inj = getInjected(context, 'github');
if (!inj) return { error: 'connector_not_injected', expected: 'github' };
try {
const res = await axios.get('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${inj.access_token}`,
Accept: 'application/vnd.github+json',
'User-Agent': 'godcrm-connectors',
},
timeout: 10_000,
validateStatus: () => true,
});
if (res.status !== 200) {
return { error: `github_http_${res.status}`, message: res.data?.message || null };
}
const u = res.data || {};
return {
login: u.login,
name: u.name,
email: u.email,
public_repos: u.public_repos,
followers: u.followers,
account_label: inj.account_label,
};
} catch (err) {
log.warn({ err: err?.message }, 'github_get_user failed');
return { error: 'github_request_failed', message: err?.message || 'unknown' };
}
}
export const connectorToolHandlers = {
figma_get_file,
slack_post_message,
github_get_user,
};
export default { CONNECTOR_TOOL_DEFS, connectorRequirements, connectorToolHandlers };