feat: serve 模式默认启用权限问询(acceptEdits)并修复回复链路

- 默认权限模式从 undefined 改为 acceptEdits(工作目录内读写自动放行)
- 始终传递 --permission-prompt-tool stdio 给子进程
- 修复 updatedInput 为 undefined 导致子进程 Zod 解析失败
- always 回复附带 updatedPermissions + decisionClassification
- 新增 permission_replied/question_replied SSE 事件通知 UI 撤销弹窗
- hook_callback 控制请求自动回复防止子进程阻塞
- PendingPermission 保存 permission_suggestions 供 always 回复使用
This commit is contained in:
DoSun 2026-05-09 11:10:13 +08:00
parent e37e9d3ad5
commit 9c3441331c
2 changed files with 61 additions and 12 deletions

View File

@ -21,16 +21,22 @@ export function createPermissionRoutes(sessionManager: SessionManager): Hono {
interrupt?: boolean
}>()
const isAlways = body.behavior === 'always'
const mappedBehavior: 'allow' | 'deny' =
body.behavior === 'reject' ? 'deny' :
body.behavior === 'once' || body.behavior === 'always' ? 'allow' :
body.behavior === 'deny' ? 'deny' : 'allow'
const updatedPermissions = body.updated_permissions
?? (isAlways ? found.perm.suggestions : undefined)
found.handle.replyPermission(requestId, mappedBehavior, {
updatedInput: body.updated_input,
updatedPermissions: body.updated_permissions,
updatedInput: body.updated_input ?? {},
updatedPermissions,
message: body.message,
interrupt: body.interrupt,
decisionClassification: isAlways ? 'user_permanent' : mappedBehavior === 'allow' ? 'user_temporary' : undefined,
})
return c.json({ resolved: true })

View File

@ -37,6 +37,7 @@ export type PendingPermission = {
input: Record<string, unknown>
title: string
description: string
suggestions: Record<string, unknown>[]
}
export type PendingQuestion = {
@ -211,7 +212,7 @@ export class SessionHandle {
this.cwd = opts.cwd
this.eventBus = opts.eventBus
this._model = opts.model
this._permissionMode = opts.permissionMode
this._permissionMode = opts.permissionMode ?? 'acceptEdits'
this.verbose = opts.verbose ?? false
}
@ -235,14 +236,10 @@ export class SessionHandle {
'--session-id',
this.sessionId,
...(this.opts.model ? ['--model', this.opts.model] : []),
...(this.opts.permissionMode
? [
'--permission-mode',
this.opts.permissionMode,
'--permission-prompt-tool',
'stdio',
]
: []),
'--permission-mode',
this._permissionMode,
'--permission-prompt-tool',
'stdio',
...(this.opts.resumeSessionId
? ['--resume', this.opts.resumeSessionId]
: []),
@ -495,6 +492,8 @@ export class SessionHandle {
input: (request.input as Record<string, unknown>) ?? {},
title: `${request.tool_name}: ${JSON.stringify(request.input).slice(0, 80)}`,
description: `Execute ${request.tool_name}`,
suggestions:
(request.permission_suggestions as Record<string, unknown>[]) ?? [],
}
this.pendingPermissions.set(requestId, perm)
this.emitEvent('control_request', { request_id: requestId, request })
@ -512,6 +511,38 @@ export class SessionHandle {
this.emitEvent('control_request', { request_id: requestId, request })
} else if (request?.subtype === 'hook_callback') {
this.emitEvent('control_request', { request_id: requestId, request })
const callbackId = request.callback_id as string | undefined
if (!callbackId || callbackId === 'AUTO_APPROVE_CALLBACK_ID') {
this.writeStdin(jsonStringify({
type: 'control_response',
response: {
subtype: 'success',
request_id: requestId,
response: {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'allow',
permissionDecisionReason: 'Auto-approved by serve',
},
},
},
}))
} else {
this.writeStdin(jsonStringify({
type: 'control_response',
response: {
subtype: 'success',
request_id: requestId,
response: {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'ask',
permissionDecisionReason: 'Forwarding to can_use_tool',
},
},
},
}))
}
}
break
}
@ -669,6 +700,7 @@ export class SessionHandle {
updatedPermissions?: Record<string, unknown>[]
message?: string
interrupt?: boolean
decisionClassification?: string
},
): void {
const response = {
@ -680,10 +712,13 @@ export class SessionHandle {
behavior === 'allow'
? {
behavior: 'allow',
updatedInput: opts?.updatedInput,
updatedInput: opts?.updatedInput ?? {},
...(opts?.updatedPermissions
? { updatedPermissions: opts.updatedPermissions }
: {}),
...(opts?.decisionClassification
? { decisionClassification: opts.decisionClassification }
: {}),
}
: {
behavior: 'deny',
@ -694,6 +729,10 @@ export class SessionHandle {
}
this.writeStdin(jsonStringify(response))
this.pendingPermissions.delete(requestId)
this.emitEvent('permission_replied', {
request_id: requestId,
behavior,
})
}
replyQuestion(
@ -714,6 +753,10 @@ export class SessionHandle {
}
this.writeStdin(jsonStringify(response))
this.pendingQuestions.delete(requestId)
this.emitEvent('question_replied', {
request_id: requestId,
action,
})
}
kill(): void {