ADR-043: Tool groups with a fail-closed enable cascade 

Status

Accepted

Date

2026-07-08

Authors

Netresearch DTT GmbH

Context 

With sixteen built-in tools (ADR-038, ADR-042) and third-party tools registering through the same nr_llm.tool tag, per-tool administration stops scaling: an admin who wants "no system introspection for this instance" or "only content tools for this configuration" has to know and toggle every individual tool — and a tool added later by an extension update silently escapes a decision that was meant to cover its whole family.

Decision 

Every tool declares a group, and enablement cascades across three levels, fail-closed:

ToolInterface::getGroup(): string (breaking, third parties must implement it — the same expansion pattern as requiresAdmin() in ADR-038). Built-ins use the curated taxonomy content, structure, system, accounts, configuration. Third-party tools declare their own group; the recommended value is the providing extension's key.

Level 1 — central group state. tx_nrllm_tool_group_state stores per-group admin overrides (mirroring tx_nrllm_tool_state; a missing row means enabled). Because the state is keyed by group name, a disabled group also covers same-group tools installed later. ToolAvailabilityService computes the effective state as group_enabled && tool_enabled: a per-tool override can not re-enable a tool inside a disabled group. The alternative — letting an explicit tool override outrank its group — was rejected because it turns "disable the group" into a soft hint whose real effect depends on invisible per-tool rows; the chosen rule keeps one glance at the group toggle authoritative.

Level 2 — per configuration. tx_nrllm_configuration gains allowed_tool_groups (comma list via selectCheckBox; items derived from the registry by an itemsProcFunc, so third-party groups appear automatically). Empty means "no group restriction". AllowedToolsResolver intersects the skill-declared allowed-tools union with the group gate; when only the group gate is set, it becomes the allow-list itself.

Level 3 — per run. The playground groups its tool checkboxes and adds a group checkbox with an indeterminate mixed state; the per-run selection is still intersected with levels 1–2 by the runtime gate.

Consequences 

  • Breaking: every ToolInterface implementation must add getGroup(). Costs one method per tool; buys family-wise control that survives extension updates.
  • An unknown or never-toggled group is enabled — grouping restricts, it does not quarantine new tools (isEnabledByDefault() and requiresAdmin() keep covering per-tool risk).
  • The group table stays name-keyed and FormEngine-free, like the tool-state table; orphaned group rows (extension removed) are harmless and inert.
  • The configuration gate composes with — never replaces — the global cascade: a globally disabled tool stays off even when its group is listed in allowed_tool_groups.