Files
CapaKraken/packages/api/src/router/assistant-tools.ts
T
Hartmut 1ff5c3377c security: block raw/tx escape hatches on read-only AI DB proxy (#47)
The read-only proxy previously wrapped model delegates to block writes,
but left client-level raw/escape hatches ($transaction, $executeRaw,
$executeRawUnsafe, $queryRawUnsafe, $runCommandRaw) intact. A read-tool
could smuggle DML via raw SQL, or open an interactive $transaction whose
tx-scoped client (unproxied by construction) accepts writes.

- read-only-prisma: block $transaction, $executeRaw, $executeRawUnsafe,
  $queryRawUnsafe, $runCommandRaw at the client level. Template-tagged
  $queryRaw stays allowed (read-only by API contract).
- assistant-tools: add create_estimate to MUTATION_TOOLS — it uses
  $transaction internally and was previously bypassing the proxy only
  because $transaction wasn't blocked.
- shared: document isReadOnly flag on ToolContext so any scoped tRPC
  caller a tool spawns keeps the proxied client.
- helpers: note the runtime wrap at assistant-tools.ts:739 is
  authoritative; forwarding ctx.db verbatim is correct.
- tests: cover model writes, raw escapes, and the allowed $queryRaw
  path (7 cases, all pass).
- loosen one estimate-detail test that compared the exact db instance
  (fails once that instance is a proxy; the assertion's intent is the
  estimate id).

Covers EGAI 4.1.1.2 / IAAI 3.6.22.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 08:38:05 +02:00

811 lines
24 KiB
TypeScript

/**
* AI Assistant Tool definitions for OpenAI Function Calling.
* Each tool has a JSON schema (for the AI) and an execute function (for the server).
*
* Helpers, callers, and access control are extracted into:
* - ./assistant-tools/helpers.ts (~1,400 lines of formatters, error converters, parsers)
* - ./assistant-tools/callers.ts (35 tRPC caller factory bindings)
* - ./assistant-tools/access-control.ts (role/permission gating logic)
*/
import { PermissionKey } from "@capakraken/shared";
import { createReadOnlyProxy } from "../lib/read-only-prisma.js";
import { logger } from "../lib/logger.js";
// ─── Extracted modules ──────────────────────────────────────────────────────
import {
createChargeabilityReportCaller,
createComputationGraphCaller,
createTimelineCaller,
createAuditLogCaller,
createImportExportCaller,
createDispoCaller,
createResourceCaller,
createSettingsCaller,
createSystemRoleConfigCaller,
createUserCaller,
createNotificationCaller,
createEstimateCaller,
createWebhookCaller,
createCountryCaller,
createHolidayCalendarCaller,
createBlueprintCaller,
createRoleCaller,
createClientCaller,
createOrgUnitCaller,
createProjectCaller,
createRateCardCaller,
createReportCaller,
createVacationCaller,
createEntitlementCaller,
createCommentCaller,
createManagementLevelCaller,
createUtilizationCategoryCaller,
createCalculationRuleCaller,
createEffortRuleCaller,
createExperienceMultiplierCaller,
createDashboardCaller,
createInsightsCaller,
createScenarioCaller,
createAllocationCaller,
createStaffingCaller,
} from "./assistant-tools/callers.js";
import {
assertPermission,
assertAdminRole,
fmtDate,
formatHolidayCalendarEntry,
formatHolidayCalendar,
formatCountry,
resolveHolidayPeriod,
parseIsoDate,
parseOptionalIsoDate,
parseDateTime,
parseOptionalDateTime,
isAssistantToolErrorResult,
toAssistantIndexedFieldError,
toAssistantNotFoundError,
toAssistantAllocationNotFoundError,
toAssistantProjectNotFoundError,
toAssistantTimelineMutationError,
toAssistantVacationMutationError,
toAssistantProjectCreationError,
toAssistantDemandCreationError,
toAssistantDemandFillError,
toAssistantEstimateNotFoundError,
toAssistantEstimateReadError,
toAssistantEstimateMutationError,
toAssistantEstimateCreationError,
toAssistantHolidayCalendarNotFoundError,
toAssistantHolidayCalendarMutationError,
toAssistantHolidayEntryMutationError,
toAssistantHolidayEntryNotFoundError,
toAssistantRoleMutationError,
toAssistantClientMutationError,
toAssistantOrgUnitMutationError,
toAssistantCountryMutationError,
toAssistantResourceCreationError,
toAssistantResourceMutationError,
toAssistantProjectMutationError,
toAssistantMetroCityMutationError,
toAssistantVacationCreationError,
toAssistantEntitlementMutationError,
toAssistantUserMutationError,
toAssistantUserResourceLinkError,
toAssistantTotpEnableError,
toAssistantWebhookNotFoundError,
toAssistantWebhookMutationError,
toAssistantAuditLogEntryNotFoundError,
toAssistantTaskActionError,
toAssistantTaskAssignmentError,
toAssistantBroadcastNotFoundError,
toAssistantDispoImportBatchNotFoundError,
toAssistantReminderNotFoundError,
toAssistantNotificationReadError,
toAssistantNotificationDeletionError,
toAssistantReminderCreationError,
toAssistantCommentResolveError,
toAssistantCommentCreationError,
toAssistantNotificationCreationError,
toAssistantCountryNotFoundError,
toAssistantTaskNotFoundError,
fmtEur,
normalizeAssistantExecutionError,
resolveEntityOrAssistantError,
resolveProjectIdentifier,
resolveResourceIdentifier,
resolveResponsiblePerson,
createScopedCallerContext,
sanitizeWebhook,
sanitizeWebhookList,
parseAssistantVacationRequestType,
} from "./assistant-tools/helpers.js";
import {
LEGACY_MONOLITHIC_TOOL_ACCESS,
getAssistantToolAccessFailure,
toAssistantToolAccessError,
getAvailableAssistantToolsForContext as getAvailableToolsForCtx_,
} from "./assistant-tools/access-control.js";
export { canAccessAssistantTool } from "./assistant-tools/access-control.js";
import {
advancedTimelineToolDefinitions,
createAdvancedTimelineExecutors,
} from "./assistant-tools/advanced-timeline.js";
import {
allocationPlanningMutationToolDefinitions,
allocationPlanningReadToolDefinitions,
createAllocationPlanningExecutors,
} from "./assistant-tools/allocation-planning.js";
import {
settingsAdminToolDefinitions,
createSettingsAdminExecutors,
} from "./assistant-tools/settings-admin.js";
import {
createVacationHolidayExecutors,
vacationHolidayMutationToolDefinitions,
vacationHolidayReadToolDefinitions,
} from "./assistant-tools/vacation-holidays.js";
import {
createRolesAnalyticsExecutors,
rolesAnalyticsMutationToolDefinitions,
rolesAnalyticsReadToolDefinitions,
} from "./assistant-tools/roles-analytics.js";
import {
clientMutationToolDefinitions,
createClientsOrgUnitsExecutors,
orgUnitMutationToolDefinitions,
} from "./assistant-tools/clients-org-units.js";
import {
chargeabilityComputationReadToolDefinitions,
createChargeabilityComputationExecutors,
} from "./assistant-tools/chargeability-computation.js";
import {
configReadmodelToolDefinitions,
createConfigReadmodelExecutors,
} from "./assistant-tools/config-readmodels.js";
import {
countryMetroAdminToolDefinitions,
createCountryMetroAdminExecutors,
} from "./assistant-tools/country-metro-admin.js";
import {
countryReadmodelToolDefinitions,
createCountryReadmodelExecutors,
} from "./assistant-tools/country-readmodels.js";
import {
createUserSelfServiceExecutors,
userSelfServiceToolDefinitions,
} from "./assistant-tools/user-self-service.js";
import {
createUserAdminExecutors,
userAdminToolDefinitions,
} from "./assistant-tools/user-admin.js";
import {
createNotificationsTasksExecutors,
notificationInboxToolDefinitions,
notificationTaskToolDefinitions,
} from "./assistant-tools/notifications-tasks.js";
import {
createEstimateExecutors,
estimateMutationToolDefinitions,
estimateReadToolDefinitions,
} from "./assistant-tools/estimates.js";
import {
createProjectExecutors,
projectMutationToolDefinitions,
projectReadToolDefinitions,
} from "./assistant-tools/projects.js";
import {
createStaffingDemandExecutors,
staffingDemandMutationToolDefinitions,
staffingDemandReadToolDefinitions,
} from "./assistant-tools/staffing-demand.js";
import {
createResourceExecutors,
resourceMutationToolDefinitions,
resourceReadToolDefinitions,
} from "./assistant-tools/resources.js";
import {
blueprintsRateCardsToolDefinitions,
createBlueprintsRateCardsExecutors,
} from "./assistant-tools/blueprints-rate-cards.js";
import {
createDashboardInsightsReportsExecutors,
dashboardInsightsReportsToolDefinitions,
} from "./assistant-tools/dashboard-insights-reports.js";
import {
createScenarioRateAnalysisExecutors,
scenarioRateAnalysisToolDefinitions,
} from "./assistant-tools/scenario-rate-analysis.js";
import {
createImportExportDispoExecutors,
importExportDispoToolDefinitions,
} from "./assistant-tools/import-export-dispo.js";
import {
commentMutationToolDefinitions,
commentReadToolDefinitions,
createCommentExecutors,
} from "./assistant-tools/comments.js";
import {
auditHistoryToolDefinitions,
createAuditHistoryExecutors,
} from "./assistant-tools/audit-history.js";
import {
createPlanningNavigationExecutors,
planningNavigationToolDefinitions,
} from "./assistant-tools/planning-navigation.js";
import {
createVacationEntitlementExecutors,
vacationEntitlementToolDefinitions,
} from "./assistant-tools/vacation-entitlements.js";
import {
withToolAccess,
type ToolAccessRequirements,
type ToolContext,
type ToolDef,
type ToolExecutor,
} from "./assistant-tools/shared.js";
export type { ToolContext } from "./assistant-tools/shared.js";
// ─── Mutation tool set for audit logging (EGAI 4.1.3.1 / IAAI 3.6.26) ──────
export const MUTATION_TOOLS = new Set([
"import_csv_data",
"update_system_settings",
"clear_stored_runtime_secrets",
"test_ai_connection",
"test_smtp_connection",
"test_gemini_connection",
"update_system_role_config",
"create_webhook",
"update_webhook",
"delete_webhook",
"test_webhook",
"stage_dispo_import_batch",
"cancel_dispo_import_batch",
"resolve_dispo_staged_record",
"commit_dispo_import_batch",
"create_allocation",
"cancel_allocation",
"update_allocation_status",
"update_timeline_allocation_inline",
"apply_timeline_project_shift",
"quick_assign_timeline_resource",
"batch_quick_assign_timeline_resources",
"batch_shift_timeline_allocations",
"update_resource",
"deactivate_resource",
"create_resource",
"update_project",
"create_project",
"delete_project",
"create_vacation",
"approve_vacation",
"reject_vacation",
"cancel_vacation",
"set_entitlement",
"create_demand",
"fill_demand",
"generate_project_cover",
"remove_project_cover",
"create_role",
"update_role",
"delete_role",
"create_client",
"update_client",
"delete_client",
"create_org_unit",
"update_org_unit",
"create_country",
"update_country",
"create_metro_city",
"update_metro_city",
"delete_metro_city",
"create_holiday_calendar",
"update_holiday_calendar",
"delete_holiday_calendar",
"create_holiday_calendar_entry",
"update_holiday_calendar_entry",
"delete_holiday_calendar_entry",
"send_broadcast",
"create_task_for_user",
"create_reminder",
"update_task_status",
"execute_task_action",
"create_comment",
"resolve_comment",
"mark_notification_read",
"save_dashboard_layout",
"toggle_favorite_project",
"set_column_preferences",
"generate_totp_secret",
"verify_and_enable_totp",
"create_user",
"set_user_password",
"update_user_role",
"update_user_name",
"link_user_resource",
"auto_link_users_by_email",
"set_user_permissions",
"reset_user_permissions",
"disable_user_totp",
"create_notification",
"update_reminder",
"delete_reminder",
"delete_notification",
"assign_task",
"create_estimate",
"clone_estimate",
"update_estimate_draft",
"submit_estimate_version",
"approve_estimate_version",
"create_estimate_revision",
"create_estimate_export",
"create_estimate_planning_handoff",
"generate_estimate_weekly_phasing",
"update_estimate_commercial_terms",
]);
export const ADVANCED_ASSISTANT_TOOLS = new Set([
"find_best_project_resource",
"get_timeline_entries_view",
"get_timeline_holiday_overlays",
"get_project_timeline_context",
"preview_project_shift",
"update_timeline_allocation_inline",
"apply_timeline_project_shift",
"quick_assign_timeline_resource",
"batch_quick_assign_timeline_resources",
"batch_shift_timeline_allocations",
"get_chargeability_report",
"get_resource_computation_graph",
"get_project_computation_graph",
]);
// Callers imported from ./assistant-tools/callers.ts
// Helpers imported from ./assistant-tools/helpers.ts
// ─── Tool Definitions ───────────────────────────────────────────────────────
export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess(
[
// ── READ TOOLS ──
...resourceReadToolDefinitions,
...projectReadToolDefinitions,
...advancedTimelineToolDefinitions,
...allocationPlanningReadToolDefinitions,
...vacationHolidayReadToolDefinitions,
...vacationHolidayMutationToolDefinitions,
...rolesAnalyticsReadToolDefinitions,
...chargeabilityComputationReadToolDefinitions,
...planningNavigationToolDefinitions,
// ── WRITE TOOLS ──
...allocationPlanningMutationToolDefinitions,
...resourceMutationToolDefinitions,
...projectMutationToolDefinitions,
...vacationEntitlementToolDefinitions,
// ── DEMAND / STAFFING ──
...staffingDemandReadToolDefinitions,
...staffingDemandMutationToolDefinitions,
// ── BLUEPRINT ──
...blueprintsRateCardsToolDefinitions,
// ── ESTIMATES ──
...estimateReadToolDefinitions,
...estimateMutationToolDefinitions,
// ── ROLES ──
...rolesAnalyticsMutationToolDefinitions,
// ── CLIENTS ──
...clientMutationToolDefinitions,
// ── ADMIN / CONFIG READ TOOLS ──
...countryReadmodelToolDefinitions,
...countryMetroAdminToolDefinitions,
...configReadmodelToolDefinitions,
...userAdminToolDefinitions,
...userSelfServiceToolDefinitions,
...notificationInboxToolDefinitions,
...dashboardInsightsReportsToolDefinitions,
// ── ORG UNIT MANAGEMENT ──
...orgUnitMutationToolDefinitions,
// ── TASK MANAGEMENT ──
...notificationTaskToolDefinitions,
...commentReadToolDefinitions,
...scenarioRateAnalysisToolDefinitions,
...commentMutationToolDefinitions,
...auditHistoryToolDefinitions,
...importExportDispoToolDefinitions,
...settingsAdminToolDefinitions,
],
LEGACY_MONOLITHIC_TOOL_ACCESS,
);
const TOOL_DEFINITIONS_BY_NAME = new Map(
TOOL_DEFINITIONS.map((tool) => [tool.function.name, tool]),
);
// Access control imported from ./assistant-tools/access-control.ts
export function getAvailableAssistantToolsForContext(
permissions: Set<PermissionKey>,
userRole: string,
): ToolDef[] {
return getAvailableToolsForCtx_(TOOL_DEFINITIONS, permissions, userRole);
}
// ─── Tool Executors ─────────────────────────────────────────────────────────
const executors = {
...createResourceExecutors({
assertPermission,
createResourceCaller,
createRoleCaller,
createCountryCaller,
createOrgUnitCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveEntityOrAssistantError,
toAssistantResourceMutationError,
toAssistantResourceCreationError,
}),
...createProjectExecutors({
assertPermission,
createProjectCaller,
createBlueprintCaller,
createClientCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResponsiblePerson,
resolveEntityOrAssistantError,
toAssistantNotFoundError,
toAssistantProjectMutationError,
toAssistantProjectCreationError,
toAssistantProjectNotFoundError,
}),
...createStaffingDemandExecutors({
assertPermission,
createAllocationCaller,
createStaffingCaller,
createRoleCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResourceIdentifier,
resolveEntityOrAssistantError,
parseIsoDate,
parseOptionalIsoDate,
fmtDate,
toAssistantDemandCreationError,
toAssistantDemandFillError,
}),
...createAdvancedTimelineExecutors({
assertPermission,
createStaffingCaller,
createTimelineCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResourceIdentifier,
parseIsoDate,
fmtDate,
isAssistantToolErrorResult,
toAssistantIndexedFieldError,
toAssistantTimelineMutationError,
}),
...createAllocationPlanningExecutors({
assertPermission,
createAllocationCaller,
createTimelineCaller,
createScopedCallerContext,
resolveProjectIdentifier,
resolveResourceIdentifier,
parseIsoDate,
parseOptionalIsoDate,
fmtDate,
toAssistantAllocationNotFoundError,
}),
...createVacationHolidayExecutors({
createEntitlementCaller,
createVacationCaller,
createHolidayCalendarCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveHolidayPeriod,
resolveEntityOrAssistantError,
assertAdminRole,
fmtDate,
formatHolidayCalendar,
formatHolidayCalendarEntry,
toAssistantHolidayCalendarMutationError,
toAssistantHolidayCalendarNotFoundError,
toAssistantHolidayEntryMutationError,
toAssistantHolidayEntryNotFoundError,
}),
...createRolesAnalyticsExecutors({
createRoleCaller,
createResourceCaller,
createDashboardCaller,
createScopedCallerContext,
resolveResourceIdentifier,
toAssistantRoleMutationError,
}),
...createClientsOrgUnitsExecutors({
createClientCaller,
createOrgUnitCaller,
createScopedCallerContext,
toAssistantClientMutationError,
toAssistantOrgUnitMutationError,
}),
...createChargeabilityComputationExecutors({
assertPermission,
createChargeabilityReportCaller,
createComputationGraphCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveProjectIdentifier,
}),
...createBlueprintsRateCardsExecutors({
createBlueprintCaller,
createRateCardCaller,
createScopedCallerContext,
resolveResourceIdentifier,
resolveEntityOrAssistantError,
parseOptionalIsoDate,
fmtDate,
}),
...createDashboardInsightsReportsExecutors({
assertPermission,
createDashboardCaller,
createInsightsCaller,
createReportCaller,
createScopedCallerContext,
}),
...createPlanningNavigationExecutors({
createEstimateCaller,
createClientCaller,
createOrgUnitCaller,
createTimelineCaller,
createScopedCallerContext,
resolveProjectIdentifier,
parseIsoDate,
}),
...createScenarioRateAnalysisExecutors({
assertPermission,
createRateCardCaller,
createScenarioCaller,
createInsightsCaller,
createScopedCallerContext,
fmtEur,
}),
...createCommentExecutors({
createCommentCaller,
createScopedCallerContext,
toAssistantCommentCreationError,
toAssistantCommentResolveError,
}),
...createAuditHistoryExecutors({
createAuditLogCaller,
createScopedCallerContext,
}),
...createVacationEntitlementExecutors({
createVacationCaller,
createEntitlementCaller,
createScopedCallerContext,
resolveResourceIdentifier,
parseIsoDate,
fmtDate,
parseAssistantVacationRequestType,
toAssistantVacationCreationError,
toAssistantVacationMutationError,
toAssistantEntitlementMutationError,
}),
// ── ESTIMATES ──
...createEstimateExecutors({
assertPermission,
createEstimateCaller,
createScopedCallerContext,
resolveProjectIdentifier,
toAssistantEstimateNotFoundError,
toAssistantEstimateReadError,
toAssistantEstimateCreationError,
toAssistantEstimateMutationError,
}),
// ── ROLES ──
// ── CLIENTS ──
// ── ADMIN / CONFIG ──
...createCountryReadmodelExecutors({
createCountryCaller,
createScopedCallerContext,
formatCountry,
toAssistantCountryNotFoundError,
}),
...createCountryMetroAdminExecutors({
createCountryCaller,
createScopedCallerContext,
assertAdminRole,
formatCountry,
toAssistantCountryMutationError,
toAssistantMetroCityMutationError,
}),
...createConfigReadmodelExecutors({
createManagementLevelCaller,
createUtilizationCategoryCaller,
createCalculationRuleCaller,
createEffortRuleCaller,
createExperienceMultiplierCaller,
createScopedCallerContext,
}),
...createUserAdminExecutors({
createUserCaller,
createScopedCallerContext,
toAssistantUserMutationError,
toAssistantUserResourceLinkError,
}),
...createUserSelfServiceExecutors({
createUserCaller,
createScopedCallerContext,
toAssistantCurrentUserError: toAssistantUserMutationError,
toAssistantTotpEnableError,
}),
...createNotificationsTasksExecutors({
createNotificationCaller,
createScopedCallerContext,
parseDateTime,
parseOptionalDateTime,
toAssistantTaskNotFoundError,
toAssistantTaskActionError,
toAssistantTaskAssignmentError,
toAssistantBroadcastNotFoundError,
toAssistantReminderNotFoundError,
toAssistantNotificationReadError,
toAssistantNotificationDeletionError,
toAssistantReminderCreationError,
toAssistantNotificationCreationError,
}),
...createImportExportDispoExecutors({
assertPermission,
createImportExportCaller,
createDispoCaller,
createScopedCallerContext,
toAssistantDispoImportBatchNotFoundError,
}),
...createSettingsAdminExecutors({
createSettingsCaller,
createSystemRoleConfigCaller,
createWebhookCaller,
createAuditLogCaller,
createProjectCaller,
createScopedCallerContext,
parseIsoDate,
resolveProjectIdentifier,
sanitizeWebhook,
sanitizeWebhookList,
toAssistantWebhookNotFoundError,
toAssistantWebhookMutationError,
toAssistantAuditLogEntryNotFoundError,
}),
};
// ─── Executor ───────────────────────────────────────────────────────────────
export interface ToolAction {
type: string;
url?: string;
scope?: string[];
description?: string;
}
export interface ToolResult {
content: string;
action?: ToolAction;
data?: unknown;
}
export async function executeTool(
name: string,
args: string,
ctx: ToolContext,
): Promise<ToolResult> {
const executor = executors[name as keyof typeof executors];
if (!executor) return { content: JSON.stringify({ error: `Unknown tool: ${name}` }) };
try {
const toolDefinition = TOOL_DEFINITIONS_BY_NAME.get(name);
const accessFailure = toolDefinition
? getAssistantToolAccessFailure(toolDefinition, ctx)
: null;
if (accessFailure) {
throw toAssistantToolAccessError(accessFailure);
}
const params = JSON.parse(args);
// EGAI 4.1.1.2 / IAAI 3.6.22 — read-only tools get a DB proxy that blocks writes
const isMutation = MUTATION_TOOLS.has(name);
const effectiveCtx = isMutation ? ctx : { ...ctx, db: createReadOnlyProxy(ctx.db) };
// Audit-log all mutation tool executions (EGAI 4.1.3.1 / IAAI 3.6.26)
if (isMutation) {
logger.info(
{ tool: name, params, userId: ctx.userId, userRole: ctx.userRole },
"AI assistant mutation tool executed",
);
}
const result = await executor(params, effectiveCtx);
// EGAI 4.3.1.2 — validate tool result against schema if defined
if (toolDefinition?.resultSchema && result != null) {
const validation = toolDefinition.resultSchema.safeParse(result);
if (!validation.success) {
logger.warn(
{ tool: name, errors: validation.error.issues },
"Tool result failed schema validation — proceeding with unvalidated result",
);
}
}
// Detect action payloads (e.g. navigation, invalidation)
if (result && typeof result === "object" && "__action" in (result as Record<string, unknown>)) {
const actionResult = result as Record<string, unknown>;
const actionType = actionResult.__action as string;
if (actionType === "navigate") {
const url = actionResult.url as string;
const desc = (actionResult.description as string | undefined) ?? url;
return {
content: JSON.stringify({ description: desc }),
data: { description: desc },
action: { type: "navigate", url, description: desc },
};
}
if (actionType === "invalidate") {
const scope = actionResult.scope as string[];
// Strip __action, scope, and large data from the result sent back to the AI
const { __action: _, scope: _s, coverImageUrl: _img, ...rest } = actionResult;
const content = JSON.stringify(rest);
return {
content: content.length > 4000 ? content.slice(0, 4000) + '..."' : content,
data: rest,
action: { type: "invalidate", scope },
};
}
}
// Cap tool result size to prevent oversized OpenAI conversation payloads
const content = typeof result === "string" ? result : JSON.stringify(result);
return {
content: content.length > 8000 ? content.slice(0, 8000) + '..."' : content,
...(typeof result === "string" ? {} : { data: result }),
};
} catch (err) {
const normalizedError = normalizeAssistantExecutionError(err);
logger.error(
{
tool: name,
userId: ctx.userId,
userRole: ctx.userRole,
error: err instanceof Error ? { message: err.message, stack: err.stack } : err,
},
"AI assistant tool execution failed",
);
return { content: JSON.stringify(normalizedError), data: normalizedError };
}
}