claude_swarm 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,8 @@ require_relative "process_tracker"
8
8
 
9
9
  module ClaudeSwarm
10
10
  class Orchestrator
11
+ RUN_DIR = File.expand_path("~/.claude-swarm/run")
12
+
11
13
  def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false,
12
14
  restore_session_path: nil)
13
15
  @config = configuration
@@ -17,6 +19,7 @@ module ClaudeSwarm
17
19
  @stream_logs = stream_logs
18
20
  @debug = debug
19
21
  @restore_session_path = restore_session_path
22
+ @session_path = nil
20
23
  end
21
24
 
22
25
  def start
@@ -29,9 +32,13 @@ module ClaudeSwarm
29
32
 
30
33
  # Use existing session path
31
34
  session_path = @restore_session_path
35
+ @session_path = session_path
32
36
  ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
33
37
  ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
34
38
 
39
+ # Create run symlink for restored session
40
+ create_run_symlink
41
+
35
42
  unless @prompt
36
43
  puts "šŸ“ Using existing session: #{session_path}/"
37
44
  puts
@@ -59,10 +66,14 @@ module ClaudeSwarm
59
66
  # Generate and set session path for all instances
60
67
  session_path = SessionPath.generate(working_dir: Dir.pwd)
61
68
  SessionPath.ensure_directory(session_path)
69
+ @session_path = session_path
62
70
 
63
71
  ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
64
72
  ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
65
73
 
74
+ # Create run symlink for new session
75
+ create_run_symlink
76
+
66
77
  unless @prompt
67
78
  puts "šŸ“ Session files will be saved to: #{session_path}/"
68
79
  puts
@@ -90,8 +101,13 @@ module ClaudeSwarm
90
101
  unless @prompt
91
102
  puts "šŸš€ Launching main instance: #{@config.main_instance}"
92
103
  puts " Model: #{main_instance[:model]}"
93
- puts " Directory: #{main_instance[:directory]}"
94
- puts " Allowed tools: #{main_instance[:tools].join(", ")}" if main_instance[:tools].any?
104
+ if main_instance[:directories].size == 1
105
+ puts " Directory: #{main_instance[:directory]}"
106
+ else
107
+ puts " Directories:"
108
+ main_instance[:directories].each { |dir| puts " - #{dir}" }
109
+ end
110
+ puts " Allowed tools: #{main_instance[:allowed_tools].join(", ")}" if main_instance[:allowed_tools].any?
95
111
  puts " Disallowed tools: #{main_instance[:disallowed_tools].join(", ")}" if main_instance[:disallowed_tools]&.any?
96
112
  puts " Connections: #{main_instance[:connections].join(", ")}" if main_instance[:connections].any?
97
113
  puts " šŸ˜Ž Vibe mode ON for this instance" if main_instance[:vibe]
@@ -119,8 +135,9 @@ module ClaudeSwarm
119
135
  log_thread.join
120
136
  end
121
137
 
122
- # Clean up child processes
138
+ # Clean up child processes and run symlink
123
139
  cleanup_processes
140
+ cleanup_run_symlink
124
141
  end
125
142
 
126
143
  private
@@ -140,6 +157,7 @@ module ClaudeSwarm
140
157
  Signal.trap(signal) do
141
158
  puts "\nšŸ›‘ Received #{signal} signal, cleaning up..."
142
159
  cleanup_processes
160
+ cleanup_run_symlink
143
161
  exit
144
162
  end
145
163
  end
@@ -152,6 +170,35 @@ module ClaudeSwarm
152
170
  puts "āš ļø Error during cleanup: #{e.message}"
153
171
  end
154
172
 
173
+ def create_run_symlink
174
+ return unless @session_path
175
+
176
+ FileUtils.mkdir_p(RUN_DIR)
177
+
178
+ # Session ID is the last part of the session path
179
+ session_id = File.basename(@session_path)
180
+ symlink_path = File.join(RUN_DIR, session_id)
181
+
182
+ # Remove stale symlink if exists
183
+ File.unlink(symlink_path) if File.symlink?(symlink_path)
184
+
185
+ # Create new symlink
186
+ File.symlink(@session_path, symlink_path)
187
+ rescue StandardError => e
188
+ # Don't fail the process if symlink creation fails
189
+ puts "āš ļø Warning: Could not create run symlink: #{e.message}" unless @prompt
190
+ end
191
+
192
+ def cleanup_run_symlink
193
+ return unless @session_path
194
+
195
+ session_id = File.basename(@session_path)
196
+ symlink_path = File.join(RUN_DIR, session_id)
197
+ File.unlink(symlink_path) if File.symlink?(symlink_path)
198
+ rescue StandardError
199
+ # Ignore errors during cleanup
200
+ end
201
+
155
202
  def start_log_streaming
156
203
  Thread.new do
157
204
  session_log_path = File.join(ENV.fetch("CLAUDE_SWARM_SESSION_PATH", nil), "session.log")
@@ -208,9 +255,17 @@ module ClaudeSwarm
208
255
  if @vibe || instance[:vibe]
209
256
  parts << "--dangerously-skip-permissions"
210
257
  else
258
+ # Build allowed tools list including MCP connections
259
+ allowed_tools = instance[:allowed_tools].dup
260
+
261
+ # Add mcp__instance_name for each connection
262
+ instance[:connections].each do |connection_name|
263
+ allowed_tools << "mcp__#{connection_name}"
264
+ end
265
+
211
266
  # Add allowed tools if any
212
- if instance[:tools].any?
213
- tools_str = instance[:tools].join(",")
267
+ if allowed_tools.any?
268
+ tools_str = allowed_tools.join(",")
214
269
  parts << "--allowedTools"
215
270
  parts << tools_str
216
271
  end
@@ -221,10 +276,6 @@ module ClaudeSwarm
221
276
  parts << "--disallowedTools"
222
277
  parts << disallowed_tools_str
223
278
  end
224
-
225
- # Add permission prompt tool unless in vibe mode
226
- parts << "--permission-prompt-tool"
227
- parts << "mcp__permissions__check_permission"
228
279
  end
229
280
 
230
281
  if instance[:prompt]
@@ -234,6 +285,14 @@ module ClaudeSwarm
234
285
 
235
286
  parts << "--debug" if @debug
236
287
 
288
+ # Add additional directories with --add-dir
289
+ if instance[:directories].size > 1
290
+ instance[:directories][1..].each do |additional_dir|
291
+ parts << "--add-dir"
292
+ parts << additional_dir
293
+ end
294
+ end
295
+
237
296
  mcp_config_path = @generator.mcp_config_path(@config.main_instance)
238
297
  parts << "--mcp-config"
239
298
  parts << mcp_config_path
@@ -4,7 +4,7 @@ module ClaudeSwarm
4
4
  class TaskTool < FastMcp::Tool
5
5
  tool_name "task"
6
6
  description "Execute a task using Claude Code. There is no description parameter."
7
- annotations(readOnlyHint: true, openWorldHint: false, destructiveHint: false)
7
+ annotations(read_only_hint: true, open_world_hint: false, destructive_hint: false)
8
8
 
9
9
  arguments do
10
10
  required(:prompt).filled(:string).description("The task or question for the agent")
@@ -22,11 +22,14 @@ module ClaudeSwarm
22
22
  }
23
23
 
24
24
  # Add allowed tools from instance config
25
- options[:allowed_tools] = instance_config[:tools] if instance_config[:tools]&.any?
25
+ options[:allowed_tools] = instance_config[:allowed_tools] if instance_config[:allowed_tools]&.any?
26
26
 
27
27
  # Add disallowed tools from instance config
28
28
  options[:disallowed_tools] = instance_config[:disallowed_tools] if instance_config[:disallowed_tools]&.any?
29
29
 
30
+ # Add connections from instance config
31
+ options[:connections] = instance_config[:connections] if instance_config[:connections]&.any?
32
+
30
33
  response = executor.execute(prompt, options)
31
34
 
32
35
  # Return just the result text as expected by MCP
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "0.1.15"
4
+ VERSION = "0.1.17"
5
5
  end
data/lib/claude_swarm.rb CHANGED
@@ -7,8 +7,6 @@ require_relative "claude_swarm/mcp_generator"
7
7
  require_relative "claude_swarm/orchestrator"
8
8
  require_relative "claude_swarm/claude_code_executor"
9
9
  require_relative "claude_swarm/claude_mcp_server"
10
- require_relative "claude_swarm/permission_tool"
11
- require_relative "claude_swarm/permission_mcp_server"
12
10
  require_relative "claude_swarm/session_path"
13
11
  require_relative "claude_swarm/session_info_tool"
14
12
  require_relative "claude_swarm/reset_session_tool"
data/llms.txt CHANGED
@@ -225,10 +225,10 @@ cat ~/.claude-swarm/sessions/PROJECT/TIMESTAMP/session.log.json
225
225
  - Check `main:` references valid instance key
226
226
  **"Circular dependency detected"**
227
227
  - Remove circular connections between instances
228
- **"Permission denied for tool X"**
228
+ **"Tool not allowed"**
229
229
  - Check `allowed_tools` includes the tool
230
230
  - Check `disallowed_tools` doesn't block it
231
- - Use `--vibe` to skip permissions (dangerous)
231
+ - Use `vibe: true` to skip all checks (dangerous)
232
232
  **"MCP server failed to start"**
233
233
  - Check the command/URL is correct
234
234
  - Verify MCP server is installed
@@ -362,12 +362,11 @@ bundle exec rake release
362
362
  - `CLAUDE_SWARM_HOME`: Override session storage location (default: ~/.claude-swarm)
363
363
  - `ANTHROPIC_MODEL`: Default Claude model if not specified
364
364
  ## Security Considerations
365
- - Tool restrictions enforced at permission layer
366
- - All permissions logged for audit
365
+ - Tool restrictions enforced through configuration
367
366
  - Vibe mode bypasses ALL restrictions - use carefully
368
367
  - Session files may contain sensitive data
369
368
  - Each instance runs with its directory context
370
- - MCP servers inherit instance permissions
369
+ - MCP servers inherit instance tool configurations
371
370
  ## Limitations
372
371
  - Tree hierarchy only (no circular dependencies)
373
372
  - Session restoration is experimental
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude_swarm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.15
4
+ version: 0.1.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda
@@ -59,21 +59,24 @@ files:
59
59
  - README.md
60
60
  - RELEASING.md
61
61
  - Rakefile
62
+ - claude-sdk-docs.md
62
63
  - claude-swarm.yml
63
64
  - example/claude-swarm.yml
64
65
  - example/microservices-team.yml
65
66
  - example/session-restoration-demo.yml
66
67
  - example/test-generation.yml
68
+ - examples/monitoring-demo.yml
69
+ - examples/multi-directory.yml
67
70
  - exe/claude-swarm
68
71
  - lib/claude_swarm.rb
69
72
  - lib/claude_swarm/claude_code_executor.rb
70
73
  - lib/claude_swarm/claude_mcp_server.rb
71
74
  - lib/claude_swarm/cli.rb
75
+ - lib/claude_swarm/commands/ps.rb
76
+ - lib/claude_swarm/commands/show.rb
72
77
  - lib/claude_swarm/configuration.rb
73
78
  - lib/claude_swarm/mcp_generator.rb
74
79
  - lib/claude_swarm/orchestrator.rb
75
- - lib/claude_swarm/permission_mcp_server.rb
76
- - lib/claude_swarm/permission_tool.rb
77
80
  - lib/claude_swarm/process_tracker.rb
78
81
  - lib/claude_swarm/reset_session_tool.rb
79
82
  - lib/claude_swarm/session_info_tool.rb
@@ -81,7 +84,6 @@ files:
81
84
  - lib/claude_swarm/task_tool.rb
82
85
  - lib/claude_swarm/version.rb
83
86
  - llms.txt
84
- - sdk-docs.md
85
87
  homepage: https://github.com/parruda/claude-swarm
86
88
  licenses: []
87
89
  metadata:
@@ -1,189 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "fast_mcp_annotations"
5
- require "logger"
6
- require "fileutils"
7
- require_relative "permission_tool"
8
- require_relative "session_path"
9
- require_relative "process_tracker"
10
-
11
- module ClaudeSwarm
12
- class PermissionMcpServer
13
- # Server configuration
14
- SERVER_NAME = "claude-swarm-permissions"
15
- SERVER_VERSION = "1.0.0"
16
-
17
- # Tool categories
18
- FILE_TOOLS = %w[Read Write Edit].freeze
19
- BASH_TOOL = "Bash"
20
-
21
- # Pattern matching
22
- TOOL_PATTERN_REGEX = /^([^()]+)\(([^)]+)\)$/
23
- PARAM_PATTERN_REGEX = /^(\w+)\s*:\s*(.+)$/
24
-
25
- def initialize(allowed_tools: nil, disallowed_tools: nil)
26
- @allowed_tools = allowed_tools
27
- @disallowed_tools = disallowed_tools
28
- setup_logging
29
- end
30
-
31
- def start
32
- configure_permission_tool
33
- create_and_start_server
34
- end
35
-
36
- private
37
-
38
- def configure_permission_tool
39
- allowed_patterns = parse_tool_patterns(@allowed_tools)
40
- disallowed_patterns = parse_tool_patterns(@disallowed_tools)
41
-
42
- log_configuration(allowed_patterns, disallowed_patterns)
43
-
44
- PermissionTool.allowed_patterns = allowed_patterns
45
- PermissionTool.disallowed_patterns = disallowed_patterns
46
- PermissionTool.logger = @logger
47
- end
48
-
49
- def create_and_start_server
50
- # Track this process
51
- session_path = SessionPath.from_env
52
- if session_path && File.exist?(session_path)
53
- tracker = ProcessTracker.new(session_path)
54
- tracker.track_pid(Process.pid, "mcp_permissions")
55
- end
56
-
57
- server = FastMcp::Server.new(
58
- name: SERVER_NAME,
59
- version: SERVER_VERSION
60
- )
61
-
62
- server.register_tool(PermissionTool)
63
- @logger.info("Permission MCP server started successfully")
64
- server.start
65
- end
66
-
67
- def setup_logging
68
- session_path = SessionPath.from_env
69
- SessionPath.ensure_directory(session_path)
70
- @logger = create_logger(session_path)
71
- @logger.info("Permission MCP server logging initialized")
72
- end
73
-
74
- def create_logger(session_path)
75
- log_path = File.join(session_path, "permissions.log")
76
- logger = Logger.new(log_path)
77
- logger.level = Logger::DEBUG
78
- logger.formatter = log_formatter
79
- logger
80
- end
81
-
82
- def log_formatter
83
- proc do |severity, datetime, _progname, msg|
84
- "[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")}] [#{severity}] #{msg}\n"
85
- end
86
- end
87
-
88
- def log_configuration(allowed_patterns, disallowed_patterns)
89
- @logger.info("Starting permission MCP server with allowed patterns: #{allowed_patterns.inspect}, " \
90
- "disallowed patterns: #{disallowed_patterns.inspect}")
91
- end
92
-
93
- def parse_tool_patterns(tools)
94
- return [] if tools.nil? || tools.empty?
95
-
96
- normalize_tool_list(tools).filter_map do |tool|
97
- parse_single_tool_pattern(tool.strip)
98
- end
99
- end
100
-
101
- def normalize_tool_list(tools)
102
- tools.is_a?(Array) ? tools : tools.split(/[,\s]+/)
103
- end
104
-
105
- def parse_single_tool_pattern(tool)
106
- return nil if tool.empty?
107
-
108
- if (match = tool.match(TOOL_PATTERN_REGEX))
109
- parse_tool_with_pattern(match[1], match[2])
110
- elsif tool.include?("*")
111
- create_wildcard_tool_pattern(tool)
112
- else
113
- create_exact_tool_pattern(tool)
114
- end
115
- end
116
-
117
- def parse_tool_with_pattern(tool_name, pattern)
118
- case tool_name
119
- when *FILE_TOOLS
120
- create_file_tool_pattern(tool_name, pattern)
121
- when BASH_TOOL
122
- create_bash_tool_pattern(tool_name, pattern)
123
- else
124
- create_custom_tool_pattern(tool_name, pattern)
125
- end
126
- end
127
-
128
- def create_file_tool_pattern(tool_name, pattern)
129
- {
130
- tool_name: tool_name,
131
- pattern: File.expand_path(pattern),
132
- type: :glob
133
- }
134
- end
135
-
136
- def create_bash_tool_pattern(tool_name, pattern)
137
- {
138
- tool_name: tool_name,
139
- pattern: process_bash_pattern(pattern),
140
- type: :regex
141
- }
142
- end
143
-
144
- def process_bash_pattern(pattern)
145
- if pattern.include?(":")
146
- # Colon syntax: convert parts and join with spaces
147
- pattern.split(":")
148
- .map { |part| part.gsub("*", ".*") }
149
- .join(" ")
150
- else
151
- # Literal pattern: escape asterisks
152
- pattern.gsub("*", "\\*")
153
- end
154
- end
155
-
156
- def create_custom_tool_pattern(tool_name, pattern)
157
- {
158
- tool_name: tool_name,
159
- pattern: parse_parameter_patterns(pattern),
160
- type: :params
161
- }
162
- end
163
-
164
- def parse_parameter_patterns(pattern)
165
- pattern.split(",").each_with_object({}) do |param_pair, params|
166
- param_pair = param_pair.strip
167
- if (match = param_pair.match(PARAM_PATTERN_REGEX))
168
- params[match[1]] = match[2]
169
- end
170
- end
171
- end
172
-
173
- def create_wildcard_tool_pattern(tool)
174
- {
175
- tool_name: tool.gsub("*", ".*"),
176
- pattern: nil,
177
- type: :regex
178
- }
179
- end
180
-
181
- def create_exact_tool_pattern(tool)
182
- {
183
- tool_name: tool,
184
- pattern: nil,
185
- type: :exact
186
- }
187
- end
188
- end
189
- end
@@ -1,201 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "fast_mcp_annotations"
5
-
6
- module ClaudeSwarm
7
- class PermissionTool < FastMcp::Tool
8
- # Class variables to store allowed/disallowed patterns and logger
9
- class << self
10
- attr_accessor :allowed_patterns, :disallowed_patterns, :logger
11
- end
12
-
13
- # Tool categories
14
- FILE_TOOLS = %w[Read Write Edit].freeze
15
- BASH_TOOL = "Bash"
16
-
17
- # Response behaviors
18
- BEHAVIOR_ALLOW = "allow"
19
- BEHAVIOR_DENY = "deny"
20
-
21
- # File matching flags
22
- FILE_MATCH_FLAGS = File::FNM_DOTMATCH | File::FNM_PATHNAME | File::FNM_EXTGLOB
23
-
24
- tool_name "check_permission"
25
- description "Check if a tool is allowed to be used based on configured patterns"
26
-
27
- arguments do
28
- required(:tool_name).filled(:string).description("The tool requesting permission")
29
- required(:input).value(:hash).description("The input for the tool")
30
- end
31
-
32
- def call(tool_name:, input:)
33
- @current_tool_name = tool_name
34
- log_request(tool_name, input)
35
-
36
- result = evaluate_permission(tool_name, input)
37
- response = JSON.generate(result)
38
-
39
- log_response(response)
40
- response
41
- end
42
-
43
- private
44
-
45
- def evaluate_permission(tool_name, input)
46
- if explicitly_disallowed?(tool_name, input)
47
- deny_response(tool_name, "explicitly disallowed")
48
- elsif implicitly_allowed?(tool_name, input)
49
- allow_response(input)
50
- else
51
- deny_response(tool_name, "not allowed by configured patterns")
52
- end
53
- end
54
-
55
- def explicitly_disallowed?(tool_name, input)
56
- check_patterns(disallowed_patterns, tool_name, input, "Disallowed")
57
- end
58
-
59
- def implicitly_allowed?(tool_name, input)
60
- allowed_patterns.empty? || check_patterns(allowed_patterns, tool_name, input, "Allowed")
61
- end
62
-
63
- def check_patterns(patterns, tool_name, input, pattern_type)
64
- patterns.any? do |pattern_hash|
65
- match = matches_pattern?(tool_name, input, pattern_hash)
66
- log_pattern_check(pattern_type, pattern_hash, tool_name, input, match)
67
- match
68
- end
69
- end
70
-
71
- def matches_pattern?(tool_name, input, pattern_hash)
72
- return false unless tool_name_matches?(tool_name, pattern_hash)
73
- return true if pattern_hash[:pattern].nil?
74
-
75
- match_tool_specific_pattern(tool_name, input, pattern_hash)
76
- end
77
-
78
- def tool_name_matches?(tool_name, pattern_hash)
79
- case pattern_hash[:type]
80
- when :regex
81
- tool_name.match?(/^#{pattern_hash[:tool_name]}$/)
82
- else
83
- tool_name == pattern_hash[:tool_name]
84
- end
85
- end
86
-
87
- def match_tool_specific_pattern(_tool_name, input, pattern_hash)
88
- case pattern_hash[:tool_name]
89
- when BASH_TOOL
90
- match_bash_pattern(input, pattern_hash)
91
- when *FILE_TOOLS
92
- match_file_pattern(input, pattern_hash[:pattern])
93
- else
94
- match_custom_tool_pattern(input, pattern_hash)
95
- end
96
- end
97
-
98
- def match_bash_pattern(input, pattern_hash)
99
- command = extract_field_value(input, "command")
100
- return false unless command
101
-
102
- if pattern_hash[:type] == :regex
103
- command.match?(/^#{pattern_hash[:pattern]}$/)
104
- else
105
- command == pattern_hash[:pattern]
106
- end
107
- end
108
-
109
- def match_file_pattern(input, pattern)
110
- file_path = extract_field_value(input, "file_path")
111
- unless file_path
112
- log_missing_field("file_path", input)
113
- return false
114
- end
115
-
116
- File.fnmatch(pattern, File.expand_path(file_path), FILE_MATCH_FLAGS)
117
- end
118
-
119
- def match_custom_tool_pattern(input, pattern_hash)
120
- return false unless pattern_hash[:type] == :params && pattern_hash[:pattern].is_a?(Hash)
121
- return false if pattern_hash[:pattern].empty?
122
-
123
- match_parameter_patterns(input, pattern_hash[:pattern])
124
- end
125
-
126
- def match_parameter_patterns(input, param_patterns)
127
- param_patterns.all? do |param_name, param_pattern|
128
- value = extract_field_value(input, param_name.to_s)
129
- return false unless value
130
-
131
- regex_pattern = glob_to_regex(param_pattern)
132
- value.to_s.match?(/^#{regex_pattern}$/)
133
- end
134
- end
135
-
136
- def extract_field_value(input, field_name)
137
- input[field_name] || input[field_name.to_sym]
138
- end
139
-
140
- def glob_to_regex(pattern)
141
- Regexp.escape(pattern)
142
- .gsub('\*', ".*")
143
- .gsub('\?', ".")
144
- end
145
-
146
- # Response builders
147
- def allow_response(input)
148
- log_decision("ALLOWED", "matches configured patterns")
149
- {
150
- "behavior" => BEHAVIOR_ALLOW,
151
- "updatedInput" => input
152
- }
153
- end
154
-
155
- def deny_response(tool_name, reason)
156
- log_decision("DENIED", "is #{reason}")
157
- {
158
- "behavior" => BEHAVIOR_DENY,
159
- "message" => "Tool '#{tool_name}' is #{reason}"
160
- }
161
- end
162
-
163
- # Logging helpers
164
- def log_request(tool_name, input)
165
- logger&.info("Permission check requested for tool: #{tool_name}")
166
- logger&.info("Tool input: #{input.inspect}")
167
- logger&.info("Checking against allowed patterns: #{allowed_patterns.inspect}")
168
- logger&.info("Checking against disallowed patterns: #{disallowed_patterns.inspect}")
169
- end
170
-
171
- def log_response(response)
172
- logger&.info("Returning response: #{response}")
173
- end
174
-
175
- def log_pattern_check(pattern_type, pattern_hash, tool_name, input, match)
176
- logger&.info("#{pattern_type} pattern '#{pattern_hash.inspect}' vs '#{tool_name}' " \
177
- "with input '#{input.inspect}': #{match}")
178
- end
179
-
180
- def log_decision(status, reason)
181
- logger&.info("#{status}: Tool '#{@current_tool_name}' #{reason}")
182
- end
183
-
184
- def log_missing_field(field_name, input)
185
- logger&.info("#{field_name} not found in input: #{input.inspect}")
186
- end
187
-
188
- # Convenience accessors
189
- def logger
190
- self.class.logger
191
- end
192
-
193
- def allowed_patterns
194
- self.class.allowed_patterns || []
195
- end
196
-
197
- def disallowed_patterns
198
- self.class.disallowed_patterns || []
199
- end
200
- end
201
- end
File without changes