Tool perms

This commit is contained in:
Siddharth Ganesan
2026-03-04 13:44:46 -08:00
parent 37337aece5
commit 08fb8c1651
6 changed files with 45 additions and 3 deletions

View File

@@ -5,6 +5,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
import { buildIntegrationToolSchemas } from '@/lib/copilot/chat-payload'
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
import { generateWorkspaceContext } from '@/lib/copilot/workspace-context'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('MothershipExecuteAPI')
@@ -40,9 +41,10 @@ export async function POST(req: NextRequest) {
ExecuteRequestSchema.parse(body)
const effectiveChatId = chatId || crypto.randomUUID()
const [workspaceContext, integrationTools] = await Promise.all([
const [workspaceContext, integrationTools, userPermission] = await Promise.all([
generateWorkspaceContext(workspaceId, userId),
buildIntegrationToolSchemas(),
getUserEntityPermissions(userId, 'workspace', workspaceId).catch(() => null),
])
const requestPayload: Record<string, unknown> = {
@@ -55,6 +57,7 @@ export async function POST(req: NextRequest) {
isHosted: true,
workspaceContext,
...(integrationTools.length > 0 ? { integrationTools } : {}),
...(userPermission ? { userPermission } : {}),
}
const result = await orchestrateCopilotStream(requestPayload, {

View File

@@ -18,6 +18,7 @@ export interface SubagentOrchestratorOptions extends Omit<OrchestratorOptions, '
userId: string
workflowId?: string
workspaceId?: string
userPermission?: string
onComplete?: (result: SubagentOrchestratorResult) => void | Promise<void>
}
@@ -40,7 +41,7 @@ export async function orchestrateSubagentStream(
requestPayload: Record<string, unknown>,
options: SubagentOrchestratorOptions
): Promise<SubagentOrchestratorResult> {
const { userId, workflowId, workspaceId } = options
const { userId, workflowId, workspaceId, userPermission } = options
const execContext = await buildExecutionContext(userId, workflowId, workspaceId)
const msgId = requestPayload?.messageId
@@ -59,7 +60,12 @@ export async function orchestrateSubagentStream(
'Content-Type': 'application/json',
...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}),
},
body: JSON.stringify({ ...requestPayload, userId, stream: true }),
body: JSON.stringify({
...requestPayload,
userId,
stream: true,
...(userPermission ? { userPermission } : {}),
}),
},
context,
execContext,

View File

@@ -121,6 +121,14 @@ async function executeManageCustomTool(
return { success: false, error: "Missing required 'operation' argument" }
}
const writeOps: string[] = ['add', 'edit', 'delete']
if (writeOps.includes(operation) && context.userPermission && context.userPermission !== 'write' && context.userPermission !== 'admin') {
return {
success: false,
error: `Permission denied: '${operation}' on manage_custom_tool requires write access. You have '${context.userPermission}' permission.`,
}
}
try {
if (operation === 'list') {
const toolsForUser = await listCustomTools({
@@ -481,6 +489,7 @@ async function executeServerToolDirect(
const result = await routeExecution(toolName, enrichedParams, {
userId: context.userId,
workspaceId: context.workspaceId,
userPermission: context.userPermission,
})
return { success: true, output: result }
} catch (error) {

View File

@@ -145,5 +145,6 @@ export interface ExecutionContext {
userId: string
workflowId: string
workspaceId?: string
userPermission?: string
decryptedEnvVars?: Record<string, string>
}

View File

@@ -3,6 +3,7 @@ import type { z } from 'zod'
export interface ServerToolContext {
userId: string
workspaceId?: string
userPermission?: string
}
/**

View File

@@ -20,6 +20,18 @@ export type ExecuteResponseSuccess = (typeof ExecuteResponseSuccessSchema)['_typ
const logger = createLogger('ServerToolRouter')
const WRITE_ACTIONS: Record<string, string[]> = {
knowledge_base: ['create', 'add_document', 'delete'],
user_table: ['create', 'insert', 'update', 'delete', 'delete_table'],
manage_custom_tool: ['add', 'edit', 'delete'],
}
function isActionAllowed(toolName: string, action: string, userPermission: string): boolean {
const writeActions = WRITE_ACTIONS[toolName]
if (!writeActions || !writeActions.includes(action)) return true
return userPermission === 'write' || userPermission === 'admin'
}
/** Registry of all server tools. Tools self-declare their validation schemas. */
const serverToolRegistry: Record<string, BaseServerTool> = {
[getBlocksMetadataServerTool.name]: getBlocksMetadataServerTool,
@@ -53,6 +65,16 @@ export async function routeExecution(
logger.debug('Routing to tool', { toolName })
// Action-level permission enforcement for mixed read/write tools
if (context?.userPermission && WRITE_ACTIONS[toolName]) {
const action = (payload as Record<string, unknown>)?.action as string
if (action && !isActionAllowed(toolName, action, context.userPermission)) {
throw new Error(
`Permission denied: '${action}' on ${toolName} requires write access. You have '${context.userPermission}' permission.`
)
}
}
// Validate input if tool declares a schema
const args = tool.inputSchema ? tool.inputSchema.parse(payload ?? {}) : (payload ?? {})