engine.ToolCallService trusted its caller for several checks. The chat's
turn engine made them around the service; a caller with no chat turn (an
MCP endpoint, later) would have skipped them. The rule that now holds for
any caller: a tool call runs as the signed-in person with their current
permissions and budget, a refusal happens before any data action, and the
call is logged with redacted input.
1. execute refuses a tool the person is not offered now. It builds the
offer from the request's policy and session (once per scope). Before,
only the turn engine's allowed-tools set did, and navigate and
search_help ran although the policy had switched them off.
2. A request without a policy is refused. UiApiToolbox, KnowledgeTool
and BulkLoadTools read a missing policy as "every tool"; they refuse.
3. A request whose context was made for another session than its own is
refused, for a call and for a confirmation. ConfirmedToolCall carries
the confirming session.
4. New executeAndLog: runs the call and writes the assistant_tool_call
row, redacting the input itself, also for a refused call. Redaction
and the row moved out of TurnEngine (redactInput is gone; what a
presentation logs is AssistantTool.inputForLog). The turn engine runs
its calls in parallel under its own time limit and writes rows in
call order, so it uses the two halves, execute and
log(request, outcome); an ArchUnit rule keeps every other class on
executeAndLog. The chat's rows are unchanged.
5. The service's javadoc says what it enforces and what a caller still
supplies (rate limiting, per-request call limits and timeouts,
classification).
6. The confirm route refuses while the kill switch is on (503
KILL_SWITCH) and when the policy no longer allows the tool (403),
before the pending action is claimed, so it stays pending.
7. Tool families are registered explicitly: the QBit's own list
(AssistantRuntime.ownToolFamilies) plus
AssistantQBitConfig.withToolFamily for a host or another QBit. The
classpath scan is gone. The registered names are logged once at
startup; a duplicate name or an empty registry stops the start.
8. ToolRegistry.definitions sorts the keys of every object of an input
schema, so two JVMs send the same bytes (a Map.of schema iterated in
a per-JVM order, and the provider mappings serialise in iteration
order).
9. The service itself requires assistant access, checks the kill switch
(fresh) and the hard budget (the policy's hard limits and a stopped
budget row, read without a budget event or alert, when a scope's
first call arrives). New BudgetService.stoppedReason.
Tests: ToolCallServiceTest (fixes 1 to 4, 6, 9, called with no turn),
ToolRegistryTest (7, 8), ToolCallArchitectureTest (the new rule).
ChatToolSetTest and ToolCallChecksTest pass unchanged. Test helpers that
handed a tool a null policy now hand it the defaults.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>