Refusing can say why, and the why is an instruction
"Don't" told the model it was refused and nothing else, so it did the one sensible thing left and asked what you would rather -- a whole round spent on something you knew when you pressed the button. "Give reason" opens a box beside it, and what you write goes back with the refusal. The reason changes what the model is *told*, not only what it reads, and that is the whole of the feature. `_not_allowed` branches: given nothing to go on, "say what you were going to do and ask what they would prefer" is right; given a reason it is exactly wrong, because the answer is already on the screen above and the model spends a round asking for it again. So it is pointed at the reason and told to carry on from it. The "do not look for a way round" half is kept either way -- that half is about the refusal and holds regardless. A card-level field rather than `text.<key>`. One card covers everything in the round for the reason the primitive exists, so one reason answers the round; and on an approval card `text.<key>` already means a corrected command, which is a different thing arriving in the same shape. Read only on a refusal, so a reason typed and then abandoned by pressing Allow cannot travel with a permission. Bounded where the Reply is built, so nothing downstream thinks about length, and put on the tool event as well as in the result -- a transcript saying a step was refused without saying why is one you had to have been watching to understand. It is also the one thing in a tool result that is genuinely not untrusted: the reader's own words, stated as theirs, needing no fence. Both halves of the control are in the DOM with one hidden and the textarea disabled while hidden, which is the rule the edit box beside it already states: a field created by a click submits nothing when the click handler fails, and an empty `reason` arriving would have to be told from one somebody cleared. The version bump is not incidental. chat.css changed and the service worker caches it under a name keyed on the version, so without it the first reload serves the old stylesheet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -105,7 +105,7 @@ def _shell_call(command: str, *, call_id: str = "c1") -> dict:
|
||||
}
|
||||
|
||||
|
||||
async def _authorise_with(context, calls, *, answers, verdict=interaction.ALLOW):
|
||||
async def _authorise_with(context, calls, *, answers, verdict=interaction.ALLOW, reason=""):
|
||||
"""Run `_authorise` and answer the card it puts up."""
|
||||
generation = generation_service.Generation(chat_id="x", message_id="y")
|
||||
arguments = generation_service._arguments_for(context, calls)
|
||||
@@ -120,7 +120,7 @@ async def _authorise_with(context, calls, *, answers, verdict=interaction.ALLOW)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
pending = generation.pending
|
||||
pending.resolve(verdict, answers=answers)
|
||||
pending.resolve(verdict, answers=answers, reason=reason)
|
||||
decided, allowed, edited = await task
|
||||
return arguments, decided, allowed, edited, pending
|
||||
|
||||
@@ -223,6 +223,47 @@ async def test_declining_ignores_the_edit(db, user_id, machine):
|
||||
assert arguments[0]["command"] == "pytest"
|
||||
|
||||
|
||||
async def test_a_refusal_with_a_reason_answers_every_call_in_the_round(
|
||||
db, user_id, machine
|
||||
):
|
||||
"""One card covers the round, so one reason answers the round.
|
||||
|
||||
That is the design claim worth pinning: the reader said "not on production"
|
||||
about what they were shown, and what they were shown was both commands. A
|
||||
reason attached to one of them would be the card describing something other
|
||||
than what it asked about.
|
||||
"""
|
||||
context = _context(db, user_id, machine)
|
||||
calls = [_shell_call("pytest", call_id="c1"), _shell_call("make deploy", call_id="c2")]
|
||||
|
||||
_arguments, decided, allowed, _edited, _ = await _authorise_with(
|
||||
context,
|
||||
calls,
|
||||
answers={},
|
||||
verdict=interaction.DENY,
|
||||
reason="not on production",
|
||||
)
|
||||
|
||||
assert allowed == set(), "nothing ran"
|
||||
assert set(decided) == {0, 1}, "and every call was answered without the runner"
|
||||
for outcome in decided.values():
|
||||
assert "not on production" in outcome.content
|
||||
assert outcome.event["error"] == "Declined: not on production"
|
||||
|
||||
|
||||
async def test_a_refusal_with_no_reason_is_unchanged(db, user_id, machine):
|
||||
"""The path everything already took, still taking it."""
|
||||
context = _context(db, user_id, machine)
|
||||
calls = [_shell_call("pytest")]
|
||||
|
||||
_arguments, decided, _allowed, _edited, _ = await _authorise_with(
|
||||
context, calls, answers={}, verdict=interaction.DENY
|
||||
)
|
||||
|
||||
assert decided[0].event["error"] == "Declined."
|
||||
assert "ask what they would prefer" in decided[0].content
|
||||
|
||||
|
||||
async def test_only_the_edited_call_in_a_round_is_changed(db, user_id, machine):
|
||||
"""One card covers the whole round, and the answers are keyed per item."""
|
||||
context = _context(db, user_id, machine)
|
||||
|
||||
Reference in New Issue
Block a user