fix(assistant): disabled tools are listed and callable — the gate's refusal IS the Settings signal

Live evidence, two ways: the 9abc1623 banner never fired because D-16 hides
ungranted tools (model never calls → refused_categories always empty), and
the [[needs:id>]] marker fix failed because a small local model answers with
a workaround narrative instead of emitting structured markers.

The model's reliable, trained behavior is tool CALLING — so disabled tools
are now listed in a DISABLED prompt section and remain in the schema. A call
hits the execution gate, which refuses and records the category → the
trusted chrome offers Settings → AI Data Access. Deterministic and
model-independent. The prompt split is UX/attack-surface shaping; the
security boundary remains the server-side grant re-check in execute_tool
(loop_.rs), unchanged and now the single enforcement layer by design.

Tests: ungranted_tool_only_ever_in_disabled_section (section-aware),
disabled_tools_are_listed_as_callable_but_refused, marker extraction kept
as a harmless safety net. 127/127 assistant suite green.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-07 11:51:02 -04:00
co-authored by Claude
parent 5e71f256e4
commit 6815a7d1eb
3 changed files with 87 additions and 58 deletions
+3 -2
View File
@@ -190,8 +190,9 @@ pub async fn run_loop(
/// The single choke point every tool call passes through, regardless of
/// which backend produced it. Enforces, in order: D-06 (curated allowlist —
/// unknown names are refused, never silently ignored), D-16 (default-closed
/// category grants — re-checked here even though the system prompt already
/// omits ungranted tools; never trust that as the only enforcement layer),
/// category grants — re-checked here even though the system prompt splits
/// available vs DISABLED tools; never trust the prompt as an enforcement
/// layer),
/// schema validation (never coerce, never guess — AI-SPEC §4b.1), and D-07
/// (every destructive tool suspends on the confirm gate before execution —
/// only a matching human "yes" releases it; a decline or timeout returns a