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(),
|
||||
mutationRequired,
|
||||
mutationToolNames: session.mutationToolNames,
|
||||
readOnlyToolNames: session.readOnlyToolNames,
|
||||
stepNumber,
|
||||
}),
|
||||
}),
|
||||
|
||||
@ -24,6 +24,42 @@ describe('Pascal agent intent', () => {
|
||||
).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', () => {
|
||||
expect(
|
||||
requiresPascalMutation([
|
||||
@ -40,6 +76,7 @@ describe('Pascal agent intent', () => {
|
||||
hasMutation: false,
|
||||
mutationRequired: true,
|
||||
mutationToolNames: ['create_room', 'add_door'],
|
||||
readOnlyToolNames: ['read_scene'],
|
||||
stepNumber: 1,
|
||||
}),
|
||||
).toEqual({
|
||||
@ -54,8 +91,24 @@ describe('Pascal agent intent', () => {
|
||||
hasMutation: true,
|
||||
mutationRequired: true,
|
||||
mutationToolNames: ['create_room'],
|
||||
readOnlyToolNames: ['read_scene'],
|
||||
stepNumber: 3,
|
||||
}),
|
||||
).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 =
|
||||
/(?:设计|创建|生成|添加|增加|新增|修改|调整|编辑|删除|移动|旋转|缩放|布置|装修|建造|搭建|修复|design|create|generate|add|modify|update|edit|delete|remove|move|rotate|scale|furnish|build|repair|fix)/i
|
||||
const CHINESE_MUTATION_ACTIONS =
|
||||
'设计|创建|生成|添加|增加|新增|修改|调整|编辑|删除|移动|旋转|缩放|布置|装修|建造|搭建|修复'
|
||||
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 {
|
||||
const latestUserMessage = [...messages]
|
||||
@ -24,15 +44,19 @@ export function requiresPascalMutation(messages: unknown[]): boolean {
|
||||
.map((part) => part.text)
|
||||
.join(' ')
|
||||
|
||||
return MUTATION_INTENT.test(text)
|
||||
return MUTATION_INTENT.test(actionableIntentText(text))
|
||||
}
|
||||
|
||||
export function pascalAgentStepPolicy(params: {
|
||||
hasMutation: boolean
|
||||
mutationRequired: boolean
|
||||
mutationToolNames: string[]
|
||||
readOnlyToolNames: string[]
|
||||
stepNumber: number
|
||||
}): { activeTools?: string[]; toolChoice: 'auto' | 'required' } {
|
||||
if (!params.mutationRequired) {
|
||||
return { activeTools: params.readOnlyToolNames, toolChoice: 'auto' }
|
||||
}
|
||||
if (!(params.mutationRequired && !params.hasMutation)) return { toolChoice: 'auto' }
|
||||
if (params.stepNumber < 1) return { toolChoice: 'required' }
|
||||
return {
|
||||
|
||||
@ -45,6 +45,7 @@ export interface PascalAgentMcpSession {
|
||||
close: () => Promise<void>
|
||||
hasMutation: () => boolean
|
||||
mutationToolNames: string[]
|
||||
readOnlyToolNames: string[]
|
||||
saveDraft: (abortSignal?: AbortSignal) => Promise<void>
|
||||
sceneName: string
|
||||
tools: ToolSet
|
||||
@ -117,6 +118,10 @@ export function isPascalAgentMutationTool(name: string): boolean {
|
||||
return MUTATION_TOOL_NAMES.has(name)
|
||||
}
|
||||
|
||||
function isPascalAgentWriteTool(name: string): boolean {
|
||||
return name === 'save_scene' || isPascalAgentMutationTool(name)
|
||||
}
|
||||
|
||||
function readSceneName(result: unknown, fallback: string): string {
|
||||
const structured = (result as { structuredContent?: { name?: unknown } })?.structuredContent
|
||||
return typeof structured?.name === 'string' && structured.name.trim()
|
||||
@ -228,10 +233,12 @@ export async function createPascalAgentMcpSession(params: {
|
||||
await client?.close()
|
||||
}
|
||||
|
||||
const toolNames = Object.keys(tools)
|
||||
return {
|
||||
close,
|
||||
hasMutation: () => mutationCount > 0,
|
||||
mutationToolNames: Object.keys(tools).filter(isPascalAgentMutationTool),
|
||||
mutationToolNames: toolNames.filter(isPascalAgentMutationTool),
|
||||
readOnlyToolNames: toolNames.filter((name) => !isPascalAgentWriteTool(name)),
|
||||
sceneName,
|
||||
tools,
|
||||
saveDraft: async (abortSignal) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user