@@ -2,6 +2,7 @@ package inventory
22
33import (
44 "context"
5+ "fmt"
56 "sort"
67 "strings"
78)
@@ -101,6 +102,7 @@ func (b *Builder) WithToolsets(toolsetIDs []string) *Builder {
101102// WithTools specifies additional tools that bypass toolset filtering.
102103// These tools are additive - they will be included even if their toolset is not enabled.
103104// Read-only filtering still applies to these tools.
105+ // Input is cleaned (trimmed, deduplicated) during Build().
104106// Deprecated tool aliases are automatically resolved to their canonical names during Build().
105107// Returns self for chaining.
106108func (b * Builder ) WithTools (toolNames []string ) * Builder {
@@ -127,11 +129,33 @@ func (b *Builder) WithFilter(filter ToolFilter) *Builder {
127129 return b
128130}
129131
132+ // cleanTools trims whitespace and removes duplicates from tool names.
133+ // Empty strings after trimming are excluded.
134+ func cleanTools (tools []string ) []string {
135+ seen := make (map [string ]bool )
136+ var cleaned []string
137+ for _ , name := range tools {
138+ trimmed := strings .TrimSpace (name )
139+ if trimmed == "" {
140+ continue
141+ }
142+ if ! seen [trimmed ] {
143+ seen [trimmed ] = true
144+ cleaned = append (cleaned , trimmed )
145+ }
146+ }
147+ return cleaned
148+ }
149+
130150// Build creates the final Inventory with all configuration applied.
131151// This processes toolset filtering, tool name resolution, and sets up
132152// the inventory for use. The returned Inventory is ready for use with
133153// AvailableTools(), RegisterAll(), etc.
134- func (b * Builder ) Build () * Inventory {
154+ //
155+ // Build returns an error if any tools specified via WithTools() are not recognized
156+ // (i.e., they don't exist in the tool set and are not deprecated aliases).
157+ // This ensures invalid tool configurations fail fast at build time.
158+ func (b * Builder ) Build () (* Inventory , error ) {
135159 r := & Inventory {
136160 tools : b .tools ,
137161 resourceTemplates : b .resourceTemplates ,
@@ -145,10 +169,19 @@ func (b *Builder) Build() *Inventory {
145169 // Process toolsets and pre-compute metadata in a single pass
146170 r .enabledToolsets , r .unrecognizedToolsets , r .toolsetIDs , r .toolsetIDSet , r .defaultToolsetIDs , r .toolsetDescriptions = b .processToolsets ()
147171
148- // Process additional tools (resolve aliases)
172+ // Build set of valid tool names for validation
173+ validToolNames := make (map [string ]bool , len (b .tools ))
174+ for i := range b .tools {
175+ validToolNames [b .tools [i ].Tool .Name ] = true
176+ }
177+
178+ // Process additional tools (clean, resolve aliases, and track unrecognized)
149179 if len (b .additionalTools ) > 0 {
150- r .additionalTools = make (map [string ]bool , len (b .additionalTools ))
151- for _ , name := range b .additionalTools {
180+ cleanedTools := cleanTools (b .additionalTools )
181+
182+ r .additionalTools = make (map [string ]bool , len (cleanedTools ))
183+ var unrecognizedTools []string
184+ for _ , name := range cleanedTools {
152185 // Always include the original name - this handles the case where
153186 // the tool exists but is controlled by a feature flag that's OFF.
154187 r .additionalTools [name ] = true
@@ -157,11 +190,19 @@ func (b *Builder) Build() *Inventory {
157190 // the new consolidated tool is available.
158191 if canonical , isAlias := b .deprecatedAliases [name ]; isAlias {
159192 r .additionalTools [canonical ] = true
193+ } else if ! validToolNames [name ] {
194+ // Not a valid tool and not a deprecated alias - track as unrecognized
195+ unrecognizedTools = append (unrecognizedTools , name )
160196 }
161197 }
198+
199+ // Error out if there are unrecognized tools
200+ if len (unrecognizedTools ) > 0 {
201+ return nil , fmt .Errorf ("unrecognized tools: %s" , strings .Join (unrecognizedTools , ", " ))
202+ }
162203 }
163204
164- return r
205+ return r , nil
165206}
166207
167208// processToolsets processes the toolsetIDs configuration and returns:
0 commit comments