claude_swarm 0.1.14 → 0.1.16

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.
@@ -1,189 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "fast_mcp"
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"
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