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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/CLAUDE.md +9 -5
- data/EXAMPLES.md +164 -0
- data/README.md +23 -28
- data/claude-swarm.yml +2 -2
- data/example/microservices-team.yml +17 -17
- data/example/test-generation.yml +7 -10
- data/lib/claude_swarm/claude_code_executor.rb +11 -6
- data/lib/claude_swarm/claude_mcp_server.rb +1 -1
- data/lib/claude_swarm/cli.rb +6 -20
- data/lib/claude_swarm/mcp_generator.rb +3 -22
- data/lib/claude_swarm/orchestrator.rb +11 -7
- data/lib/claude_swarm/task_tool.rb +6 -2
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +0 -2
- data/llms.txt +383 -0
- metadata +5 -5
- data/lib/claude_swarm/permission_mcp_server.rb +0 -189
- data/lib/claude_swarm/permission_tool.rb +0 -201
- /data/{sdk-docs.md → claude-sdk-docs.md} +0 -0
@@ -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
|