bxcore, brokerd, loopd: an invalid-arguments denial says why
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -48,8 +48,15 @@ fn every_denial_and_a_failure_come_back_as_they_were_sent() {
|
||||
DenyReason::StateUnreadable,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|reason| ToolResponse::Denied { reason })
|
||||
.map(|reason| ToolResponse::Denied {
|
||||
reason,
|
||||
detail: None,
|
||||
})
|
||||
.collect();
|
||||
answers.push(ToolResponse::Denied {
|
||||
reason: DenyReason::InvalidArguments,
|
||||
detail: Some("invalid path: n/a".to_string()),
|
||||
});
|
||||
answers.push(ToolResponse::Failed {
|
||||
message: "the runner arrives in M3b".to_string(),
|
||||
});
|
||||
|
||||
@@ -5,7 +5,8 @@ mod support;
|
||||
|
||||
use bxcore::{CallId, DenyReason, SessionId, ToolRequest, ToolResponse};
|
||||
use loopd::tools::{
|
||||
Dispatch, FakeTools, Pending, Registry, ToolPort, cap_result, denial_text, dispatch,
|
||||
Dispatch, FakeTools, Pending, Registry, ToolPort, cap_result, denial_message, denial_text,
|
||||
dispatch,
|
||||
};
|
||||
|
||||
fn req(tool: &str, arguments: &str) -> ToolRequest {
|
||||
@@ -252,9 +253,35 @@ fn every_deny_reason_has_its_sentence() {
|
||||
];
|
||||
for (reason, want) in table {
|
||||
assert_eq!(denial_text(reason), want, "{reason:?}");
|
||||
assert_eq!(
|
||||
denial_message(reason, None),
|
||||
want,
|
||||
"{reason:?}: without a detail, the sentence alone"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The broker's detail goes after the sentence, before its full stop.
|
||||
#[test]
|
||||
fn a_denial_with_a_detail_appends_it_to_the_sentence() {
|
||||
assert_eq!(
|
||||
denial_message(
|
||||
DenyReason::InvalidArguments,
|
||||
Some("invalid argument shape: unknown field `file`, expected `text`")
|
||||
),
|
||||
"Denied: the arguments are not valid for this tool: invalid argument shape: unknown field `file`, expected `text`."
|
||||
);
|
||||
assert_eq!(
|
||||
denial_message(DenyReason::InvalidArguments, Some("invalid path: n/a")),
|
||||
"Denied: the arguments are not valid for this tool: invalid path: n/a."
|
||||
);
|
||||
assert_eq!(
|
||||
denial_message(DenyReason::NoGrant, Some("x")),
|
||||
"Denied: no grant allows this call: x.",
|
||||
"any reason's detail is shown the same way"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn results_are_cut_on_a_character_boundary_and_marked() {
|
||||
assert_eq!(cap_result("short", 100), ("short".to_string(), false));
|
||||
@@ -296,7 +323,8 @@ fn fake_tools_answer_echo_deny_the_rest_and_record_calls() {
|
||||
assert_eq!(
|
||||
fake.call(&req(tool, "{}"), &mut on_pending),
|
||||
ToolResponse::Denied {
|
||||
reason: DenyReason::NoGrant
|
||||
reason: DenyReason::NoGrant,
|
||||
detail: None,
|
||||
},
|
||||
"{tool}: the clock is loopd's own now, not the port's"
|
||||
);
|
||||
|
||||
@@ -31,6 +31,7 @@ fn tool_events(events: &[TurnEvent]) -> Vec<String> {
|
||||
fn a_denial_is_a_fixed_sentence_for_the_model_and_an_event_for_the_owner() {
|
||||
let s = setup(vec![ToolResponse::Denied {
|
||||
reason: DenyReason::TaintTooHigh,
|
||||
detail: None,
|
||||
}]);
|
||||
s.server.route(
|
||||
CHAT,
|
||||
@@ -78,10 +79,55 @@ fn a_denial_is_a_fixed_sentence_for_the_model_and_an_event_for_the_owner() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The broker's detail reaches the model, in the log and in the next request; the owner's event
|
||||
/// is the same as for any denial.
|
||||
#[test]
|
||||
fn an_invalid_call_tells_the_model_why() {
|
||||
let s = setup(vec![ToolResponse::Denied {
|
||||
reason: DenyReason::InvalidArguments,
|
||||
detail: Some("invalid path: n/a".to_string()),
|
||||
}]);
|
||||
s.server.route(
|
||||
CHAT,
|
||||
vec![Reply::fixture("tool_call"), Reply::fixture("plain")],
|
||||
);
|
||||
let mut session = s.session("a");
|
||||
let (result, events) = s.turn(&mut session, "x");
|
||||
assert!(result.is_ok(), "{result:?}");
|
||||
let want = "Denied: the arguments are not valid for this tool: invalid path: n/a.";
|
||||
match &session.records()[4] {
|
||||
LogRecord::ToolResult {
|
||||
content,
|
||||
class,
|
||||
untrusted,
|
||||
truncated,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(content, want);
|
||||
assert_eq!(
|
||||
(*class, *untrusted, *truncated),
|
||||
(DataClass::Public, false, false)
|
||||
);
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
assert_eq!(
|
||||
tool_events(&events),
|
||||
[
|
||||
"started read_file",
|
||||
"denied read_file InvalidArguments",
|
||||
"result read_file"
|
||||
]
|
||||
);
|
||||
let m2 = s.server.requests_to(CHAT)[1].json();
|
||||
assert_eq!(m2["messages"][3]["content"], want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_denied_call_tool_names_the_target_tool_in_the_denial() {
|
||||
let s = setup(vec![ToolResponse::Denied {
|
||||
reason: DenyReason::NoGrant,
|
||||
detail: None,
|
||||
}]);
|
||||
s.server.route(
|
||||
CHAT,
|
||||
@@ -158,6 +204,7 @@ fn a_pending_approval_that_ends_in_a_refusal() {
|
||||
pending(7, "2026-09-18T12:15:00.000Z"),
|
||||
ToolResponse::Denied {
|
||||
reason: DenyReason::ApprovalRefused,
|
||||
detail: None,
|
||||
},
|
||||
]);
|
||||
s.server.route(
|
||||
|
||||
Reference in New Issue
Block a user