fix: enforce read-only Pascal agent requests
This commit is contained in:
parent
42bd2827a6
commit
beb6ff4f04
@ -256,6 +256,7 @@ export async function POST(request: Request) {
|
|||||||
hasMutation: session.hasMutation(),
|
hasMutation: session.hasMutation(),
|
||||||
mutationRequired,
|
mutationRequired,
|
||||||
mutationToolNames: session.mutationToolNames,
|
mutationToolNames: session.mutationToolNames,
|
||||||
|
readOnlyToolNames: session.readOnlyToolNames,
|
||||||
stepNumber,
|
stepNumber,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -24,6 +24,42 @@ describe('Pascal agent intent', () => {
|
|||||||
).toBe(false)
|
).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('does not treat a negated mutation as permission to write', () => {
|
||||||
|
expect(
|
||||||
|
requiresPascalMutation([
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '请只读取当前场景并汇总元素数量,不要修改任何模型。',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toBe(false)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
requiresPascalMutation([
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
parts: [{ type: 'text', text: 'Inspect this scene, but do not modify the model.' }],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps other requested mutations after removing a negated action', () => {
|
||||||
|
expect(
|
||||||
|
requiresPascalMutation([
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
parts: [{ type: 'text', text: '不要删除现有墙体,只添加一扇窗。' }],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
test('uses the latest user message', () => {
|
test('uses the latest user message', () => {
|
||||||
expect(
|
expect(
|
||||||
requiresPascalMutation([
|
requiresPascalMutation([
|
||||||
@ -40,6 +76,7 @@ describe('Pascal agent intent', () => {
|
|||||||
hasMutation: false,
|
hasMutation: false,
|
||||||
mutationRequired: true,
|
mutationRequired: true,
|
||||||
mutationToolNames: ['create_room', 'add_door'],
|
mutationToolNames: ['create_room', 'add_door'],
|
||||||
|
readOnlyToolNames: ['read_scene'],
|
||||||
stepNumber: 1,
|
stepNumber: 1,
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
@ -54,8 +91,24 @@ describe('Pascal agent intent', () => {
|
|||||||
hasMutation: true,
|
hasMutation: true,
|
||||||
mutationRequired: true,
|
mutationRequired: true,
|
||||||
mutationToolNames: ['create_room'],
|
mutationToolNames: ['create_room'],
|
||||||
|
readOnlyToolNames: ['read_scene'],
|
||||||
stepNumber: 3,
|
stepNumber: 3,
|
||||||
}),
|
}),
|
||||||
).toEqual({ toolChoice: 'auto' })
|
).toEqual({ toolChoice: 'auto' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('only exposes inspection tools for read-only requests', () => {
|
||||||
|
expect(
|
||||||
|
pascalAgentStepPolicy({
|
||||||
|
hasMutation: false,
|
||||||
|
mutationRequired: false,
|
||||||
|
mutationToolNames: ['apply_patch', 'save_scene'],
|
||||||
|
readOnlyToolNames: ['read_scene', 'list_levels'],
|
||||||
|
stepNumber: 0,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
activeTools: ['read_scene', 'list_levels'],
|
||||||
|
toolChoice: 'auto',
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,25 @@
|
|||||||
const MUTATION_INTENT =
|
const CHINESE_MUTATION_ACTIONS =
|
||||||
/(?:设计|创建|生成|添加|增加|新增|修改|调整|编辑|删除|移动|旋转|缩放|布置|装修|建造|搭建|修复|design|create|generate|add|modify|update|edit|delete|remove|move|rotate|scale|furnish|build|repair|fix)/i
|
'设计|创建|生成|添加|增加|新增|修改|调整|编辑|删除|移动|旋转|缩放|布置|装修|建造|搭建|修复'
|
||||||
|
const ENGLISH_MUTATION_ACTIONS =
|
||||||
|
'design|create|generate|add|modify|change|update|edit|delete|remove|move|rotate|scale|furnish|build|repair|fix'
|
||||||
|
|
||||||
|
const MUTATION_INTENT = new RegExp(
|
||||||
|
`(?:${CHINESE_MUTATION_ACTIONS}|${ENGLISH_MUTATION_ACTIONS})`,
|
||||||
|
'i',
|
||||||
|
)
|
||||||
|
|
||||||
|
const NEGATED_CHINESE_MUTATION = new RegExp(
|
||||||
|
`(?:不要|请勿|勿|无需|不需要|不用|禁止|避免|不(?:再)?)[^,。;;,.!?!?]{0,32}(?:${CHINESE_MUTATION_ACTIONS})`,
|
||||||
|
'gi',
|
||||||
|
)
|
||||||
|
const NEGATED_ENGLISH_MUTATION = new RegExp(
|
||||||
|
`(?:do\\s+not|don't|dont|please\\s+do\\s+not|please\\s+don't|without|avoid)\\s+[^,.!?;]{0,48}(?:${ENGLISH_MUTATION_ACTIONS})`,
|
||||||
|
'gi',
|
||||||
|
)
|
||||||
|
|
||||||
|
function actionableIntentText(text: string): string {
|
||||||
|
return text.replace(NEGATED_CHINESE_MUTATION, ' ').replace(NEGATED_ENGLISH_MUTATION, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
export function requiresPascalMutation(messages: unknown[]): boolean {
|
export function requiresPascalMutation(messages: unknown[]): boolean {
|
||||||
const latestUserMessage = [...messages]
|
const latestUserMessage = [...messages]
|
||||||
@ -24,15 +44,19 @@ export function requiresPascalMutation(messages: unknown[]): boolean {
|
|||||||
.map((part) => part.text)
|
.map((part) => part.text)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
|
|
||||||
return MUTATION_INTENT.test(text)
|
return MUTATION_INTENT.test(actionableIntentText(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pascalAgentStepPolicy(params: {
|
export function pascalAgentStepPolicy(params: {
|
||||||
hasMutation: boolean
|
hasMutation: boolean
|
||||||
mutationRequired: boolean
|
mutationRequired: boolean
|
||||||
mutationToolNames: string[]
|
mutationToolNames: string[]
|
||||||
|
readOnlyToolNames: string[]
|
||||||
stepNumber: number
|
stepNumber: number
|
||||||
}): { activeTools?: string[]; toolChoice: 'auto' | 'required' } {
|
}): { activeTools?: string[]; toolChoice: 'auto' | 'required' } {
|
||||||
|
if (!params.mutationRequired) {
|
||||||
|
return { activeTools: params.readOnlyToolNames, toolChoice: 'auto' }
|
||||||
|
}
|
||||||
if (!(params.mutationRequired && !params.hasMutation)) return { toolChoice: 'auto' }
|
if (!(params.mutationRequired && !params.hasMutation)) return { toolChoice: 'auto' }
|
||||||
if (params.stepNumber < 1) return { toolChoice: 'required' }
|
if (params.stepNumber < 1) return { toolChoice: 'required' }
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export interface PascalAgentMcpSession {
|
|||||||
close: () => Promise<void>
|
close: () => Promise<void>
|
||||||
hasMutation: () => boolean
|
hasMutation: () => boolean
|
||||||
mutationToolNames: string[]
|
mutationToolNames: string[]
|
||||||
|
readOnlyToolNames: string[]
|
||||||
saveDraft: (abortSignal?: AbortSignal) => Promise<void>
|
saveDraft: (abortSignal?: AbortSignal) => Promise<void>
|
||||||
sceneName: string
|
sceneName: string
|
||||||
tools: ToolSet
|
tools: ToolSet
|
||||||
@ -117,6 +118,10 @@ export function isPascalAgentMutationTool(name: string): boolean {
|
|||||||
return MUTATION_TOOL_NAMES.has(name)
|
return MUTATION_TOOL_NAMES.has(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPascalAgentWriteTool(name: string): boolean {
|
||||||
|
return name === 'save_scene' || isPascalAgentMutationTool(name)
|
||||||
|
}
|
||||||
|
|
||||||
function readSceneName(result: unknown, fallback: string): string {
|
function readSceneName(result: unknown, fallback: string): string {
|
||||||
const structured = (result as { structuredContent?: { name?: unknown } })?.structuredContent
|
const structured = (result as { structuredContent?: { name?: unknown } })?.structuredContent
|
||||||
return typeof structured?.name === 'string' && structured.name.trim()
|
return typeof structured?.name === 'string' && structured.name.trim()
|
||||||
@ -228,10 +233,12 @@ export async function createPascalAgentMcpSession(params: {
|
|||||||
await client?.close()
|
await client?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toolNames = Object.keys(tools)
|
||||||
return {
|
return {
|
||||||
close,
|
close,
|
||||||
hasMutation: () => mutationCount > 0,
|
hasMutation: () => mutationCount > 0,
|
||||||
mutationToolNames: Object.keys(tools).filter(isPascalAgentMutationTool),
|
mutationToolNames: toolNames.filter(isPascalAgentMutationTool),
|
||||||
|
readOnlyToolNames: toolNames.filter((name) => !isPascalAgentWriteTool(name)),
|
||||||
sceneName,
|
sceneName,
|
||||||
tools,
|
tools,
|
||||||
saveDraft: async (abortSignal) => {
|
saveDraft: async (abortSignal) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user