1ff5c3377c
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>
56 lines
1.7 KiB
TypeScript
56 lines
1.7 KiB
TypeScript
import type { prisma } from "@capakraken/db";
|
|
import type { PermissionKey, SystemRole } from "@capakraken/shared";
|
|
import type { z } from "zod";
|
|
import type { TRPCContext } from "../../trpc.js";
|
|
|
|
export type ToolContext = {
|
|
db: typeof prisma;
|
|
userId: string;
|
|
userRole: string;
|
|
permissions: Set<PermissionKey>;
|
|
session?: TRPCContext["session"];
|
|
dbUser?: TRPCContext["dbUser"];
|
|
roleDefaults?: TRPCContext["roleDefaults"];
|
|
/**
|
|
* If true, the ctx.db passed in is already wrapped by
|
|
* `createReadOnlyProxy` and any scoped tRPC caller the tool spawns
|
|
* MUST also receive the proxied client — otherwise a read-only tool
|
|
* can smuggle writes through a tRPC caller that bypasses the proxy.
|
|
*/
|
|
isReadOnly?: boolean;
|
|
};
|
|
|
|
export interface ToolAccessRequirements {
|
|
requiredPermissions?: PermissionKey[];
|
|
allowedSystemRoles?: SystemRole[];
|
|
requiresPlanningRead?: boolean;
|
|
requiresCostView?: boolean;
|
|
requiresAdvancedAssistant?: boolean;
|
|
requiresResourceOverview?: boolean;
|
|
}
|
|
|
|
export interface ToolDef {
|
|
type: "function";
|
|
function: {
|
|
name: string;
|
|
description: string;
|
|
parameters: Record<string, unknown>;
|
|
};
|
|
access?: ToolAccessRequirements;
|
|
/** EGAI 4.3.1.2 — optional Zod schema to validate tool results before returning to the AI */
|
|
resultSchema?: z.ZodType;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export type ToolExecutor = (params: any, ctx: ToolContext) => Promise<unknown>;
|
|
|
|
export function withToolAccess(
|
|
tools: ToolDef[],
|
|
accessByName: Partial<Record<string, ToolAccessRequirements>>,
|
|
): ToolDef[] {
|
|
return tools.map((tool) => ({
|
|
...tool,
|
|
...(accessByName[tool.function.name] ? { access: accessByName[tool.function.name] } : {}),
|
|
}));
|
|
}
|