fix(approval): undelivered or unanswered CLI approval prompts are not user denials
When the CLI approval callback raises, when no callback is registered on the thread while prompt_toolkit owns the terminal, or when the input() read is interrupted, prompt_dangerous_approval returned "deny" and the command gate rendered "BLOCKED: User denied this command" — attributing a refusal to a user who was never asked (#22992). #112308 fixed the gateway half of the class (withdrawn prompts -> outcome "cancelled" with a cause); this closes the CLI residual on the same shape. - tools/approval_prompt.py: those three paths return an Unanswered("cancelled") sentinel carrying the cause; MCP elicitation consent maps it to "cancel". - tools/approval.py: the CLI gate renders "BLOCKED: <noun> was not approved: the approval prompt could not be delivered or was not answered (<cause>)" with outcome "cancelled" — still fail-closed, "Silence is not consent". - tools/file_tools_write_guards.py: the protected-instruction write gate reports the undelivered prompt instead of "was denied by the user". - Shared metrics: "cancelled" is a counted approval outcome (contract + v2 schema) instead of falling into "unknown". - Docs: hook `choice="cancelled"` now covers the CLI causes. Fixes #22992
This commit is contained in:
@@ -493,6 +493,7 @@
|
||||
"approval_outcome": {
|
||||
"enum": [
|
||||
"approved",
|
||||
"cancelled",
|
||||
"denied",
|
||||
"not_required",
|
||||
"timed_out",
|
||||
@@ -578,6 +579,7 @@
|
||||
"outcome": {
|
||||
"enum": [
|
||||
"approved",
|
||||
"cancelled",
|
||||
"denied",
|
||||
"timed_out",
|
||||
"unknown"
|
||||
|
||||
@@ -61,7 +61,7 @@ TOOL_CATEGORIES = frozenset({
|
||||
"skill", "terminal", "unknown", "web",
|
||||
})
|
||||
TOOL_OUTCOMES = frozenset({"blocked", "cancelled", "failed", "success", "timed_out", "unknown"})
|
||||
TOOL_APPROVAL_OUTCOMES = frozenset({"approved", "denied", "not_required", "timed_out", "unknown"})
|
||||
TOOL_APPROVAL_OUTCOMES = frozenset({"approved", "cancelled", "denied", "not_required", "timed_out", "unknown"})
|
||||
TOOL_APPROVAL_ATTRIBUTIONS = frozenset({"tool_call", "unattributed"})
|
||||
TOOL_LATENCY_BUCKETS = frozenset({
|
||||
"100ms_to_250ms", "10s_to_30s", "1s_to_2s", "250ms_to_500ms", "2s_to_5s", "500ms_to_1s",
|
||||
@@ -548,6 +548,7 @@ _APPROVAL_CHOICES = {
|
||||
),
|
||||
**dict.fromkeys(("deny", "denied", "smart_deny"), "denied"),
|
||||
**dict.fromkeys(("timed_out", "timeout"), "timed_out"),
|
||||
"cancelled": "cancelled", # prompt withdrawn / undeliverable / unanswered — not a user decision
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user