Repository navigation
Add AUTO mode denial observability and caps #4476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
f58e32e
dc25b0b
9338f8b
f74f0bb
b1d2647
e6f02fb
07107ba
b8b86ea
b235d58
e666485
415dc8b
5eedfaa
90df3c0
d51bf52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ import { | |
| firePostToolUseHook, | ||
| firePostToolUseFailureHook, | ||
| fireNotificationHook, | ||
| firePermissionDeniedHook, | ||
| firePermissionRequestHook, | ||
| appendAdditionalContext, | ||
| } from './toolHookTriggers.js'; | ||
|
|
@@ -1713,6 +1714,21 @@ export class CoreToolScheduler { | |
| this.setStatusInternal(reqInfo.callId, 'scheduled'); | ||
| continue; | ||
| case 'blocked': | ||
| if (!this.config.getDisableAllHooks()) { | ||
| const messageBus = this.config.getMessageBus() as | ||
| | MessageBus | ||
| | undefined; | ||
| if (messageBus) { | ||
| void firePermissionDeniedHook( | ||
| messageBus, | ||
| canonicalName, | ||
| toolParams, | ||
| reqInfo.callId, | ||
| outcome.reason, | ||
| signal, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] No test in A regression in the wiring (wrong argument order, missing — qwen3.7-max via Qwen Code /review |
||
| ); | ||
| } | ||
| } | ||
| this.setStatusInternal( | ||
| reqInfo.callId, | ||
| 'error', | ||
|
|
@@ -1731,7 +1747,7 @@ export class CoreToolScheduler { | |
| // a pmForcedAsk fallback isn't an audit-worthy event). | ||
| if (fallback.fallback) { | ||
| debugLogger.warn( | ||
| `Auto mode fallback to manual approval (${fallback.reason}): consecutiveBlock=${denialState.consecutiveBlock}, consecutiveUnavailable=${denialState.consecutiveUnavailable}`, | ||
| `Auto mode fallback to manual approval (${fallback.reason}): consecutiveBlock=${denialState.consecutiveBlock}, consecutiveUnavailable=${denialState.consecutiveUnavailable}, totalBlock=${denialState.totalBlock}, totalUnavailable=${denialState.totalUnavailable}`, | ||
| ); | ||
| } | ||
| break; | ||
|
|
@@ -2188,11 +2204,10 @@ export class CoreToolScheduler { | |
| this.setToolCallOutcome(callId, outcome); | ||
|
|
||
| // AUTO-mode denialTracking recovery: when the user manually approves a | ||
| // call that fell back from AUTO (either by streak threshold or by an | ||
| // explicit ask rule), reset the consecutiveBlock counter so subsequent | ||
| // calls return to classifier flow. Without this, a session that hit | ||
| // the denial threshold once would stay in fallback for the rest of | ||
| // the session even after the user explicitly approves the next call. | ||
| // call that fell back from AUTO (by denial cap or by an explicit ask | ||
| // rule), clear the armed counters so subsequent calls return to | ||
| // classifier flow. Without this, a session that hit a denial threshold | ||
| // once would stay in fallback even after explicit user approval. | ||
| // Cancel / abort do NOT reset — spec §9.1.4 treats rejection as a | ||
| // signal that the classifier was correct to block. | ||
| if ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ import type { | |
| HookExecutionRequest, | ||
| HookExecutionResponse, | ||
| } from '../confirmation-bus/types.js'; | ||
| import type { PermissionDeniedReason } from '../permissions/autoMode.js'; | ||
| import { | ||
| createHookOutput, | ||
| type PreToolUseHookOutput, | ||
|
|
@@ -512,6 +513,56 @@ export async function firePermissionRequestHook( | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Result of PermissionDenied hook execution | ||
| */ | ||
| export interface PermissionDeniedHookResult { | ||
| /** Reserved for future non-blocking hook diagnostics */ | ||
| hookError?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Fire PermissionDenied hook via MessageBus. | ||
| * Called after AUTO mode blocks a tool call without asking the user. | ||
| * This hook is observational only; it cannot approve or modify the call. | ||
| */ | ||
| export async function firePermissionDeniedHook( | ||
| messageBus: MessageBus | undefined, | ||
| toolName: string, | ||
| toolInput: Record<string, unknown>, | ||
| toolUseId: string, | ||
| reason: PermissionDeniedReason, | ||
| signal?: AbortSignal, | ||
| ): Promise<PermissionDeniedHookResult> { | ||
| if (!messageBus) { | ||
| return {}; | ||
| } | ||
|
|
||
| try { | ||
| await messageBus.request<HookExecutionRequest, HookExecutionResponse>( | ||
| { | ||
| type: MessageBusType.HOOK_EXECUTION_REQUEST, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Unlike every other
Since callers use const response = await messageBus.request<...>(...);
if (!response.success || !response.output) {
const message = response.error?.message || 'hook runner returned no output';
debugLogger.warn(`PermissionDenied hook response failure for ${toolName}: ${message}`);
return { hookError: message };
}
return {};And in the catch block: — qwen3.7-max via Qwen Code /review |
||
| eventName: 'PermissionDenied', | ||
| input: { | ||
| tool_name: toolName, | ||
| tool_input: toolInput, | ||
| tool_use_id: toolUseId, | ||
| reason, | ||
| }, | ||
| signal, | ||
| }, | ||
| MessageBusType.HOOK_EXECUTION_RESPONSE, | ||
| ); | ||
|
|
||
| return {}; | ||
| } catch (error) { | ||
| debugLogger.warn( | ||
| `PermissionDenied hook error: ${error instanceof Error ? error.message : String(error)}`, | ||
| ); | ||
| return {}; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Append additional context to tool response content | ||
| * | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] The
blockedpath has no debug log, while the siblingfallbackpath (line ~1749) logsdebugLogger.warn(...)with full counter state (consecutiveBlock,consecutiveUnavailable,totalBlock,totalUnavailable). The same asymmetry exists inSession.ts.An operator debugging "AUTO mode is denying all tool calls" has zero server-side signal on the blocked path: no tool name, no reason (
classifier_blockedvsclassifier_unavailable), no counter state. Adding a symmetricdebugLogger.warnhere would close the observability gap.— qwen3.7-max via Qwen Code /review