claude_swarm 1.0.0 → 1.0.2
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/.claude/commands/release.md +1 -1
- data/.claude/hooks/lint-code-files.rb +65 -0
- data/.rubocop.yml +22 -2
- data/CHANGELOG.md +21 -1
- data/CLAUDE.md +1 -1
- data/CONTRIBUTING.md +69 -0
- data/README.md +27 -2
- data/Rakefile +71 -3
- data/analyze_coverage.rb +94 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
- data/docs/v2/README.md +308 -0
- data/docs/v2/guides/claude-code-agents.md +262 -0
- data/docs/v2/guides/complete-tutorial.md +3088 -0
- data/docs/v2/guides/getting-started.md +1456 -0
- data/docs/v2/guides/memory-adapters.md +998 -0
- data/docs/v2/guides/plugins.md +816 -0
- data/docs/v2/guides/quick-start-cli.md +1745 -0
- data/docs/v2/guides/rails-integration.md +1902 -0
- data/docs/v2/guides/swarm-memory.md +599 -0
- data/docs/v2/reference/cli.md +729 -0
- data/docs/v2/reference/ruby-dsl.md +2154 -0
- data/docs/v2/reference/yaml.md +1835 -0
- data/docs-team-swarm.yml +2222 -0
- data/examples/learning-assistant/assistant.md +7 -0
- data/examples/learning-assistant/example-memories/concept-example.md +90 -0
- data/examples/learning-assistant/example-memories/experience-example.md +66 -0
- data/examples/learning-assistant/example-memories/fact-example.md +76 -0
- data/examples/learning-assistant/example-memories/memory-index.md +78 -0
- data/examples/learning-assistant/example-memories/skill-example.md +168 -0
- data/examples/learning-assistant/learning_assistant.rb +34 -0
- data/examples/learning-assistant/learning_assistant.yml +20 -0
- data/examples/v2/dsl/01_basic.rb +44 -0
- data/examples/v2/dsl/02_core_parameters.rb +59 -0
- data/examples/v2/dsl/03_capabilities.rb +71 -0
- data/examples/v2/dsl/04_llm_parameters.rb +56 -0
- data/examples/v2/dsl/05_advanced_flags.rb +73 -0
- data/examples/v2/dsl/06_permissions.rb +80 -0
- data/examples/v2/dsl/07_mcp_server.rb +62 -0
- data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
- data/examples/v2/dsl/09_agent_hooks.rb +67 -0
- data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
- data/examples/v2/dsl/11_delegation.rb +60 -0
- data/examples/v2/dsl/12_complete_integration.rb +137 -0
- data/examples/v2/file_tools_swarm.yml +102 -0
- data/examples/v2/hooks/01_basic_hooks.rb +133 -0
- data/examples/v2/hooks/02_usage_tracking.rb +201 -0
- data/examples/v2/hooks/03_production_monitoring.rb +429 -0
- data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
- data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
- data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
- data/examples/v2/hooks/swarm_summary.sh +44 -0
- data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
- data/examples/v2/hooks/validate_bash.rb +59 -0
- data/examples/v2/multi_directory_permissions.yml +221 -0
- data/examples/v2/node_context_demo.rb +127 -0
- data/examples/v2/node_workflow.rb +173 -0
- data/examples/v2/path_resolution_demo.rb +216 -0
- data/examples/v2/simple-swarm-v2.rb +90 -0
- data/examples/v2/simple-swarm-v2.yml +62 -0
- data/examples/v2/swarm.yml +71 -0
- data/examples/v2/swarm_with_hooks.yml +61 -0
- data/examples/v2/swarm_with_hooks_simple.yml +25 -0
- data/examples/v2/think_tool_demo.rb +62 -0
- data/exe/swarm +6 -0
- data/lib/claude_swarm/claude_mcp_server.rb +0 -6
- data/lib/claude_swarm/cli.rb +10 -3
- data/lib/claude_swarm/commands/ps.rb +19 -20
- data/lib/claude_swarm/commands/show.rb +1 -1
- data/lib/claude_swarm/configuration.rb +10 -12
- data/lib/claude_swarm/mcp_generator.rb +10 -1
- data/lib/claude_swarm/orchestrator.rb +73 -49
- data/lib/claude_swarm/system_utils.rb +37 -11
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +1 -0
- data/lib/claude_swarm/yaml_loader.rb +22 -0
- data/lib/claude_swarm.rb +7 -3
- data/lib/swarm_cli/cli.rb +201 -0
- data/lib/swarm_cli/command_registry.rb +61 -0
- data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
- data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
- data/lib/swarm_cli/commands/migrate.rb +55 -0
- data/lib/swarm_cli/commands/run.rb +173 -0
- data/lib/swarm_cli/config_loader.rb +97 -0
- data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
- data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
- data/lib/swarm_cli/interactive_repl.rb +918 -0
- data/lib/swarm_cli/mcp_serve_options.rb +44 -0
- data/lib/swarm_cli/mcp_tools_options.rb +59 -0
- data/lib/swarm_cli/migrate_options.rb +54 -0
- data/lib/swarm_cli/migrator.rb +132 -0
- data/lib/swarm_cli/options.rb +151 -0
- data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
- data/lib/swarm_cli/ui/components/content_block.rb +120 -0
- data/lib/swarm_cli/ui/components/divider.rb +57 -0
- data/lib/swarm_cli/ui/components/panel.rb +62 -0
- data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
- data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
- data/lib/swarm_cli/ui/formatters/number.rb +58 -0
- data/lib/swarm_cli/ui/formatters/text.rb +77 -0
- data/lib/swarm_cli/ui/formatters/time.rb +73 -0
- data/lib/swarm_cli/ui/icons.rb +59 -0
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
- data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
- data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
- data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
- data/lib/swarm_cli/version.rb +5 -0
- data/lib/swarm_cli.rb +44 -0
- data/lib/swarm_memory/adapters/base.rb +141 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
- data/lib/swarm_memory/chat_extension.rb +34 -0
- data/lib/swarm_memory/cli/commands.rb +306 -0
- data/lib/swarm_memory/core/entry.rb +37 -0
- data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
- data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
- data/lib/swarm_memory/core/path_normalizer.rb +75 -0
- data/lib/swarm_memory/core/semantic_index.rb +244 -0
- data/lib/swarm_memory/core/storage.rb +288 -0
- data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
- data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
- data/lib/swarm_memory/dsl/memory_config.rb +113 -0
- data/lib/swarm_memory/embeddings/embedder.rb +36 -0
- data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
- data/lib/swarm_memory/errors.rb +21 -0
- data/lib/swarm_memory/integration/cli_registration.rb +30 -0
- data/lib/swarm_memory/integration/configuration.rb +43 -0
- data/lib/swarm_memory/integration/registration.rb +31 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
- data/lib/swarm_memory/optimization/analyzer.rb +244 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
- data/lib/swarm_memory/prompts/memory.md.erb +109 -0
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
- data/lib/swarm_memory/search/semantic_search.rb +112 -0
- data/lib/swarm_memory/search/text_search.rb +42 -0
- data/lib/swarm_memory/search/text_similarity.rb +80 -0
- data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
- data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
- data/lib/swarm_memory/tools/load_skill.rb +313 -0
- data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
- data/lib/swarm_memory/tools/memory_delete.rb +99 -0
- data/lib/swarm_memory/tools/memory_edit.rb +185 -0
- data/lib/swarm_memory/tools/memory_glob.rb +160 -0
- data/lib/swarm_memory/tools/memory_grep.rb +247 -0
- data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
- data/lib/swarm_memory/tools/memory_read.rb +123 -0
- data/lib/swarm_memory/tools/memory_write.rb +231 -0
- data/lib/swarm_memory/utils.rb +50 -0
- data/lib/swarm_memory/version.rb +5 -0
- data/lib/swarm_memory.rb +166 -0
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
- data/lib/swarm_sdk/agent/builder.rb +461 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
- data/lib/swarm_sdk/agent/chat.rb +1159 -0
- data/lib/swarm_sdk/agent/context.rb +112 -0
- data/lib/swarm_sdk/agent/context_manager.rb +309 -0
- data/lib/swarm_sdk/agent/definition.rb +556 -0
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
- data/lib/swarm_sdk/configuration.rb +296 -0
- data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
- data/lib/swarm_sdk/context_compactor.rb +340 -0
- data/lib/swarm_sdk/hooks/adapter.rb +359 -0
- data/lib/swarm_sdk/hooks/context.rb +197 -0
- data/lib/swarm_sdk/hooks/definition.rb +80 -0
- data/lib/swarm_sdk/hooks/error.rb +29 -0
- data/lib/swarm_sdk/hooks/executor.rb +146 -0
- data/lib/swarm_sdk/hooks/registry.rb +147 -0
- data/lib/swarm_sdk/hooks/result.rb +150 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
- data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
- data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
- data/lib/swarm_sdk/log_collector.rb +51 -0
- data/lib/swarm_sdk/log_stream.rb +69 -0
- data/lib/swarm_sdk/markdown_parser.rb +75 -0
- data/lib/swarm_sdk/model_aliases.json +5 -0
- data/lib/swarm_sdk/models.json +1 -0
- data/lib/swarm_sdk/models.rb +120 -0
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/permissions/config.rb +239 -0
- data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
- data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
- data/lib/swarm_sdk/permissions/validator.rb +173 -0
- data/lib/swarm_sdk/permissions_builder.rb +122 -0
- data/lib/swarm_sdk/plugin.rb +147 -0
- data/lib/swarm_sdk/plugin_registry.rb +101 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
- data/lib/swarm_sdk/result.rb +97 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
- data/lib/swarm_sdk/swarm/builder.rb +586 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
- data/lib/swarm_sdk/swarm.rb +982 -0
- data/lib/swarm_sdk/tools/bash.rb +274 -0
- data/lib/swarm_sdk/tools/clock.rb +44 -0
- data/lib/swarm_sdk/tools/delegate.rb +164 -0
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
- data/lib/swarm_sdk/tools/edit.rb +150 -0
- data/lib/swarm_sdk/tools/glob.rb +158 -0
- data/lib/swarm_sdk/tools/grep.rb +228 -0
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
- data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
- data/lib/swarm_sdk/tools/read.rb +251 -0
- data/lib/swarm_sdk/tools/registry.rb +93 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
- data/lib/swarm_sdk/tools/think.rb +95 -0
- data/lib/swarm_sdk/tools/todo_write.rb +216 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
- data/lib/swarm_sdk/tools/write.rb +117 -0
- data/lib/swarm_sdk/utils.rb +50 -0
- data/lib/swarm_sdk/version.rb +5 -0
- data/lib/swarm_sdk.rb +157 -0
- data/llm.v2.txt +13407 -0
- data/rubocop/cop/security/no_reflection_methods.rb +47 -0
- data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
- data/swarm_cli.gemspec +57 -0
- data/swarm_memory.gemspec +28 -0
- data/swarm_sdk.gemspec +41 -0
- data/team.yml +1 -1
- data/team_full.yml +1875 -0
- data/{team_v2.yml → team_sdk.yml} +121 -52
- metadata +247 -4
- data/EXAMPLES.md +0 -164
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Permissions
|
|
5
|
+
# ErrorFormatter generates user-friendly error messages for permission violations
|
|
6
|
+
class ErrorFormatter
|
|
7
|
+
class << self
|
|
8
|
+
# Generate a permission denied error message
|
|
9
|
+
#
|
|
10
|
+
# @param path [String] The path that was denied
|
|
11
|
+
# @param allowed_patterns [Array<String>] List of allowed path patterns
|
|
12
|
+
# @param denied_patterns [Array<String>] List of denied path patterns
|
|
13
|
+
# @param matching_pattern [String, nil] The specific pattern that blocked this path
|
|
14
|
+
# @param tool_name [String] Name of the tool that was denied
|
|
15
|
+
# @return [String] Formatted error message with system reminder
|
|
16
|
+
def permission_denied(path:, allowed_patterns:, denied_patterns: [], matching_pattern: nil, tool_name:)
|
|
17
|
+
operation_verb = case tool_name.to_s
|
|
18
|
+
when "Read" then "read"
|
|
19
|
+
when "Write" then "write to"
|
|
20
|
+
when "Edit", "MultiEdit" then "edit"
|
|
21
|
+
when "Glob" then "access directory"
|
|
22
|
+
when "Grep" then "search in"
|
|
23
|
+
else "access"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Build policy explanation
|
|
27
|
+
policy_info = if matching_pattern && matching_pattern != "(not in allowed list)"
|
|
28
|
+
# Show the specific denied pattern that blocked this path
|
|
29
|
+
"Blocked by policy: #{matching_pattern}"
|
|
30
|
+
elsif matching_pattern == "(not in allowed list)" && allowed_patterns.any?
|
|
31
|
+
# Show allowed patterns when path doesn't match any
|
|
32
|
+
patterns = allowed_patterns.map { |p| " - #{p}" }.join("\n")
|
|
33
|
+
"Path not in allowed list. Allowed paths:\n#{patterns}"
|
|
34
|
+
elsif denied_patterns.any?
|
|
35
|
+
# Show denied patterns
|
|
36
|
+
patterns = denied_patterns.map { |p| " - #{p}" }.join("\n")
|
|
37
|
+
"Denied paths:\n#{patterns}"
|
|
38
|
+
elsif allowed_patterns.any?
|
|
39
|
+
# Show allowed patterns
|
|
40
|
+
patterns = allowed_patterns.map { |p| " - #{p}" }.join("\n")
|
|
41
|
+
"Allowed paths (not matched):\n#{patterns}"
|
|
42
|
+
else
|
|
43
|
+
"No access policy configured"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
reminder = <<~REMINDER
|
|
47
|
+
|
|
48
|
+
<system-reminder>
|
|
49
|
+
PERMISSION DENIED: You do not have permission to #{operation_verb} '#{path}'.
|
|
50
|
+
|
|
51
|
+
#{policy_info}
|
|
52
|
+
|
|
53
|
+
This is an UNRECOVERABLE error set by user policy. You MUST stop trying to access files matching this pattern.
|
|
54
|
+
|
|
55
|
+
Policy explanation:
|
|
56
|
+
- This policy blocks ALL files matching the pattern, not just this specific file
|
|
57
|
+
- Do not attempt to access other files matching this pattern - they will also be denied
|
|
58
|
+
- Do not try to work around this restriction by using different tool arguments
|
|
59
|
+
- The user has explicitly denied access to these resources via security policy
|
|
60
|
+
|
|
61
|
+
You should inform the user that you cannot proceed due to permission restrictions on this file pattern.
|
|
62
|
+
</system-reminder>
|
|
63
|
+
REMINDER
|
|
64
|
+
|
|
65
|
+
"Permission denied: Cannot #{operation_verb} '#{path}'#{reminder}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Generate a command permission denied error message
|
|
69
|
+
#
|
|
70
|
+
# @param command [String] The command that was denied
|
|
71
|
+
# @param allowed_patterns [Array<Regexp>] List of allowed command regex patterns
|
|
72
|
+
# @param denied_patterns [Array<Regexp>] List of denied command regex patterns
|
|
73
|
+
# @param matching_pattern [String, nil] The specific pattern that blocked this command
|
|
74
|
+
# @param tool_name [String] Name of the tool (typically "bash")
|
|
75
|
+
# @return [String] Formatted error message with system reminder
|
|
76
|
+
def command_permission_denied(command:, allowed_patterns:, denied_patterns: [], matching_pattern: nil, tool_name:)
|
|
77
|
+
# Build policy explanation
|
|
78
|
+
policy_info = if matching_pattern && matching_pattern != "(not in allowed list)"
|
|
79
|
+
# Show the specific denied pattern that blocked this command
|
|
80
|
+
"Blocked by policy: #{matching_pattern}"
|
|
81
|
+
elsif matching_pattern == "(not in allowed list)" && allowed_patterns.any?
|
|
82
|
+
# Show allowed patterns when command doesn't match any
|
|
83
|
+
patterns = allowed_patterns.map { |p| " - #{p.source}" }.join("\n")
|
|
84
|
+
"Command not in allowed list. Allowed command patterns:\n#{patterns}"
|
|
85
|
+
elsif denied_patterns.any?
|
|
86
|
+
# Show denied patterns
|
|
87
|
+
patterns = denied_patterns.map { |p| " - #{p.source}" }.join("\n")
|
|
88
|
+
"Denied command patterns:\n#{patterns}"
|
|
89
|
+
elsif allowed_patterns.any?
|
|
90
|
+
# Show allowed patterns
|
|
91
|
+
patterns = allowed_patterns.map { |p| " - #{p.source}" }.join("\n")
|
|
92
|
+
"Allowed command patterns (not matched):\n#{patterns}"
|
|
93
|
+
else
|
|
94
|
+
"No command policy configured"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
reminder = <<~REMINDER
|
|
98
|
+
|
|
99
|
+
<system-reminder>
|
|
100
|
+
PERMISSION DENIED: You do not have permission to execute command '#{command}'.
|
|
101
|
+
|
|
102
|
+
#{policy_info}
|
|
103
|
+
|
|
104
|
+
This is an UNRECOVERABLE error set by user policy. You MUST stop trying to execute commands matching this pattern.
|
|
105
|
+
|
|
106
|
+
Policy explanation:
|
|
107
|
+
- This policy blocks ALL commands matching the pattern, not just this specific command
|
|
108
|
+
- Do not attempt to execute other commands matching this pattern - they will also be denied
|
|
109
|
+
- Do not try to work around this restriction by modifying the command slightly
|
|
110
|
+
- The user has explicitly denied access to these commands via security policy
|
|
111
|
+
|
|
112
|
+
You should inform the user that you cannot proceed due to permission restrictions on this command.
|
|
113
|
+
</system-reminder>
|
|
114
|
+
REMINDER
|
|
115
|
+
|
|
116
|
+
"Permission denied: Cannot execute command '#{command}'#{reminder}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Permissions
|
|
5
|
+
# PathMatcher handles glob pattern matching for file paths
|
|
6
|
+
#
|
|
7
|
+
# Supports gitignore-style glob patterns with:
|
|
8
|
+
# - Standard globs: *, **, ?, [abc], {a,b}
|
|
9
|
+
# - Recursive matching: **/* matches all nested files
|
|
10
|
+
# - Negation: !pattern to explicitly deny
|
|
11
|
+
#
|
|
12
|
+
# Examples:
|
|
13
|
+
# PathMatcher.matches?("tmp/**/*", "tmp/foo/bar.rb") # => true
|
|
14
|
+
# PathMatcher.matches?("*.log", "debug.log") # => true
|
|
15
|
+
# PathMatcher.matches?("src/**/*.{rb,js}", "src/a/b.rb") # => true
|
|
16
|
+
class PathMatcher
|
|
17
|
+
class << self
|
|
18
|
+
# Check if a path matches a glob pattern
|
|
19
|
+
#
|
|
20
|
+
# @param pattern [String] Glob pattern to match against
|
|
21
|
+
# @param path [String] File path to check
|
|
22
|
+
# @return [Boolean] True if path matches pattern
|
|
23
|
+
def matches?(pattern, path)
|
|
24
|
+
# Remove leading ! for negation patterns (handled by caller)
|
|
25
|
+
pattern = pattern.delete_prefix("!")
|
|
26
|
+
|
|
27
|
+
# Use File.fnmatch with pathname and extglob flags
|
|
28
|
+
# FNM_PATHNAME: ** matches directories recursively
|
|
29
|
+
# FNM_EXTGLOB: Support {a,b} patterns
|
|
30
|
+
File.fnmatch(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Permissions
|
|
5
|
+
# Validator decorates tools to enforce permission checks before execution
|
|
6
|
+
#
|
|
7
|
+
# Uses the Decorator pattern (via SimpleDelegator) to wrap tool instances
|
|
8
|
+
# and validate file paths and commands before allowing tool execution.
|
|
9
|
+
#
|
|
10
|
+
# Example:
|
|
11
|
+
# write_tool = Tools::Write.new
|
|
12
|
+
# permissions = Config.new(
|
|
13
|
+
# {
|
|
14
|
+
# allowed_paths: ["tmp/**/*"],
|
|
15
|
+
# allowed_commands: ["^git (status|diff)$"]
|
|
16
|
+
# },
|
|
17
|
+
# base_directories: ["."]
|
|
18
|
+
# )
|
|
19
|
+
# validated_tool = Validator.new(write_tool, permissions)
|
|
20
|
+
#
|
|
21
|
+
# # This will be denied:
|
|
22
|
+
# validated_tool.call({"file_path" => "/etc/passwd", "content" => "..."})
|
|
23
|
+
class Validator < SimpleDelegator
|
|
24
|
+
# Initialize validator decorator
|
|
25
|
+
#
|
|
26
|
+
# @param tool [RubyLLM::Tool] Tool instance to wrap
|
|
27
|
+
# @param permissions_config [Config] Permission configuration
|
|
28
|
+
def initialize(tool, permissions_config)
|
|
29
|
+
super(tool)
|
|
30
|
+
@permissions = permissions_config
|
|
31
|
+
@tool = tool
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Intercept RubyLLM's call method to validate permissions
|
|
35
|
+
#
|
|
36
|
+
# RubyLLM calls tool.call(args) where args have string keys.
|
|
37
|
+
# We must override call (not execute) because SimpleDelegator doesn't
|
|
38
|
+
# automatically intercept methods defined in the superclass.
|
|
39
|
+
#
|
|
40
|
+
# @param args [Hash] Tool arguments with string keys
|
|
41
|
+
# @return [String] Tool result or permission denied message
|
|
42
|
+
def call(args)
|
|
43
|
+
# Validate Bash commands if this is the Bash tool
|
|
44
|
+
if bash_tool?
|
|
45
|
+
command = args["command"]
|
|
46
|
+
if command && !@permissions.command_allowed?(command)
|
|
47
|
+
# Find the specific pattern that blocks this command
|
|
48
|
+
matching_pattern = @permissions.find_blocking_command_pattern(command)
|
|
49
|
+
|
|
50
|
+
return ErrorFormatter.command_permission_denied(
|
|
51
|
+
command: command,
|
|
52
|
+
allowed_patterns: @permissions.allowed_commands,
|
|
53
|
+
denied_patterns: @permissions.denied_commands,
|
|
54
|
+
matching_pattern: matching_pattern,
|
|
55
|
+
tool_name: @tool.name,
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Extract paths from arguments (handles both string and symbol keys)
|
|
61
|
+
paths = extract_paths_from_args(args)
|
|
62
|
+
|
|
63
|
+
# Determine if this is a directory search tool (Glob/Grep)
|
|
64
|
+
directory_search = directory_search_tool?
|
|
65
|
+
|
|
66
|
+
# Validate each path
|
|
67
|
+
paths.each do |path|
|
|
68
|
+
next if @permissions.allowed?(path, directory_search: directory_search)
|
|
69
|
+
|
|
70
|
+
# Show absolute path in error message for clarity
|
|
71
|
+
absolute_path = @permissions.to_absolute(path)
|
|
72
|
+
|
|
73
|
+
# Find the specific pattern that blocks this path
|
|
74
|
+
matching_pattern = @permissions.find_blocking_pattern(path, directory_search: directory_search)
|
|
75
|
+
|
|
76
|
+
return ErrorFormatter.permission_denied(
|
|
77
|
+
path: absolute_path,
|
|
78
|
+
allowed_patterns: @permissions.allowed_patterns,
|
|
79
|
+
denied_patterns: @permissions.denied_patterns,
|
|
80
|
+
matching_pattern: matching_pattern,
|
|
81
|
+
tool_name: @tool.name,
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# All permissions validated, call wrapped tool
|
|
86
|
+
__getobj__.call(args)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Check if the tool is the Bash tool
|
|
92
|
+
#
|
|
93
|
+
# @return [Boolean] True if tool is Bash
|
|
94
|
+
def bash_tool?
|
|
95
|
+
@tool.name.to_s == "Bash"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check if the tool is a directory search tool (Glob or Grep)
|
|
99
|
+
#
|
|
100
|
+
# @return [Boolean] True if tool searches directories
|
|
101
|
+
def directory_search_tool?
|
|
102
|
+
tool_name = @tool.name.to_s
|
|
103
|
+
tool_name == "Glob" || tool_name == "Grep"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Extract file paths from tool arguments
|
|
107
|
+
#
|
|
108
|
+
# RubyLLM always passes arguments with string keys to call().
|
|
109
|
+
#
|
|
110
|
+
# Different tools have different parameter structures:
|
|
111
|
+
# - Write/Edit/Read: file_path parameter
|
|
112
|
+
# - MultiEdit: edits array with file_path in each edit
|
|
113
|
+
# - Glob/Grep: path parameter (directory to search)
|
|
114
|
+
# - Glob: pattern parameter may contain directory (e.g., "lib/**/*.rb")
|
|
115
|
+
# - Bash: command parameter (validated separately via command_allowed?)
|
|
116
|
+
#
|
|
117
|
+
# @param args [Hash] Tool arguments with string keys
|
|
118
|
+
# @return [Array<String>] List of file paths to validate
|
|
119
|
+
def extract_paths_from_args(args)
|
|
120
|
+
paths = []
|
|
121
|
+
|
|
122
|
+
# Single file path parameter (Write, Edit, Read)
|
|
123
|
+
paths << args["file_path"] if args["file_path"]
|
|
124
|
+
|
|
125
|
+
# Path parameter (Glob, Grep)
|
|
126
|
+
paths << args["path"] if args["path"]
|
|
127
|
+
|
|
128
|
+
# Glob pattern may contain directory prefix (e.g., "lib/**/*.rb")
|
|
129
|
+
# Extract the base directory from the pattern for validation
|
|
130
|
+
# Note: Only do this for Glob, not Grep (Grep pattern is a regex, not a path)
|
|
131
|
+
if @tool.name.to_s == "Glob"
|
|
132
|
+
pattern = args["pattern"]
|
|
133
|
+
if pattern && !pattern.start_with?("/")
|
|
134
|
+
# Extract first directory component from relative patterns
|
|
135
|
+
base_dir = extract_base_directory(pattern)
|
|
136
|
+
paths << base_dir if base_dir
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# MultiEdit has array of edits
|
|
141
|
+
edits = args["edits"]
|
|
142
|
+
edits&.each do |edit|
|
|
143
|
+
paths << edit["file_path"] if edit.is_a?(Hash) && edit["file_path"]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
paths.compact.uniq
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Extract base directory from a glob pattern
|
|
150
|
+
#
|
|
151
|
+
# Examples:
|
|
152
|
+
# "lib/**/*.rb" => "lib"
|
|
153
|
+
# "src/main.rb" => "src"
|
|
154
|
+
# "**/*.rb" => nil (no specific directory)
|
|
155
|
+
# "*.rb" => nil (current directory)
|
|
156
|
+
#
|
|
157
|
+
# @param pattern [String] Glob pattern
|
|
158
|
+
# @return [String, nil] Base directory or nil
|
|
159
|
+
def extract_base_directory(pattern)
|
|
160
|
+
return if pattern.nil? || pattern.empty?
|
|
161
|
+
|
|
162
|
+
# Split on / and take first component
|
|
163
|
+
parts = pattern.split("/")
|
|
164
|
+
first_part = parts.first
|
|
165
|
+
|
|
166
|
+
# Skip if pattern starts with wildcard (means current directory)
|
|
167
|
+
return if first_part.include?("*") || first_part.include?("?")
|
|
168
|
+
|
|
169
|
+
first_part
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# DSL builder for tool permissions configuration
|
|
5
|
+
#
|
|
6
|
+
# Provides fluent API for configuring tool permissions using underscore syntax:
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# permissions do
|
|
10
|
+
# tool(:Write).allow_paths "tmp/**/*"
|
|
11
|
+
# tool(:Write).deny_paths "tmp/secrets/**"
|
|
12
|
+
# tool(:Read).deny_paths "lib/**/*"
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @example Bash commands
|
|
16
|
+
# permissions do
|
|
17
|
+
# tool(:Bash).allow_commands "^git (status|diff|log)$"
|
|
18
|
+
# tool(:Bash).deny_commands "^rm -rf"
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
class PermissionsBuilder
|
|
22
|
+
def initialize
|
|
23
|
+
@permissions = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Build permissions from block
|
|
28
|
+
#
|
|
29
|
+
# @yield Block for configuring permissions
|
|
30
|
+
# @return [Hash] Permissions configuration
|
|
31
|
+
def build(&block)
|
|
32
|
+
builder = new
|
|
33
|
+
builder.instance_eval(&block)
|
|
34
|
+
builder.to_h
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Convert to hash format expected by AgentDefinition
|
|
39
|
+
#
|
|
40
|
+
# @return [Hash] Permissions config
|
|
41
|
+
def to_h
|
|
42
|
+
@permissions
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get a tool permissions proxy for configuring a specific tool
|
|
46
|
+
#
|
|
47
|
+
# @param tool_name [Symbol, String] Tool name
|
|
48
|
+
# @return [ToolPermissionsProxy] Proxy for configuring this tool
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# tool(:Write).allow_paths "tmp/**/*"
|
|
52
|
+
# tool(:Bash).deny_commands "^rm -rf"
|
|
53
|
+
def tool(tool_name)
|
|
54
|
+
ToolPermissionsProxy.new(tool_name, @permissions)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Proxy for configuring permissions on a specific tool
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# tool(:Write).allow_paths "tmp/**/*"
|
|
62
|
+
# tool(:Write).deny_paths "tmp/secrets/**"
|
|
63
|
+
# tool(:Bash).allow_commands "^git status$"
|
|
64
|
+
#
|
|
65
|
+
class ToolPermissionsProxy
|
|
66
|
+
def initialize(tool_name, permissions_hash)
|
|
67
|
+
@tool_name = tool_name.to_sym
|
|
68
|
+
@permissions = permissions_hash
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Add allowed path patterns
|
|
72
|
+
#
|
|
73
|
+
# @param patterns [Array<String>] Glob patterns for allowed paths
|
|
74
|
+
# @return [self]
|
|
75
|
+
def allow_paths(*patterns)
|
|
76
|
+
ensure_tool_config
|
|
77
|
+
@permissions[@tool_name][:allowed_paths] ||= []
|
|
78
|
+
@permissions[@tool_name][:allowed_paths].concat(patterns.flatten)
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Add denied path patterns
|
|
83
|
+
#
|
|
84
|
+
# @param patterns [Array<String>] Glob patterns for denied paths
|
|
85
|
+
# @return [self]
|
|
86
|
+
def deny_paths(*patterns)
|
|
87
|
+
ensure_tool_config
|
|
88
|
+
@permissions[@tool_name][:denied_paths] ||= []
|
|
89
|
+
@permissions[@tool_name][:denied_paths].concat(patterns.flatten)
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Add allowed command patterns (Bash tool only)
|
|
94
|
+
#
|
|
95
|
+
# @param patterns [Array<String>] Regex patterns for allowed commands
|
|
96
|
+
# @return [self]
|
|
97
|
+
def allow_commands(*patterns)
|
|
98
|
+
ensure_tool_config
|
|
99
|
+
@permissions[@tool_name][:allowed_commands] ||= []
|
|
100
|
+
@permissions[@tool_name][:allowed_commands].concat(patterns.flatten)
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Add denied command patterns (Bash tool only)
|
|
105
|
+
#
|
|
106
|
+
# @param patterns [Array<String>] Regex patterns for denied commands
|
|
107
|
+
# @return [self]
|
|
108
|
+
def deny_commands(*patterns)
|
|
109
|
+
ensure_tool_config
|
|
110
|
+
@permissions[@tool_name][:denied_commands] ||= []
|
|
111
|
+
@permissions[@tool_name][:denied_commands].concat(patterns.flatten)
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# Ensure tool entry exists in permissions hash
|
|
118
|
+
def ensure_tool_config
|
|
119
|
+
@permissions[@tool_name] ||= {}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Base class for SwarmSDK plugins
|
|
5
|
+
#
|
|
6
|
+
# Plugins provide tools, storage, configuration parsing, and lifecycle hooks.
|
|
7
|
+
# Plugins are self-registering - they call SwarmSDK::PluginRegistry.register
|
|
8
|
+
# when the gem is loaded.
|
|
9
|
+
#
|
|
10
|
+
# @example Implementing a plugin
|
|
11
|
+
# class MyPlugin < SwarmSDK::Plugin
|
|
12
|
+
# def name
|
|
13
|
+
# :my_plugin
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# def tools
|
|
17
|
+
# [:MyTool, :OtherTool]
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# def create_tool(tool_name, context)
|
|
21
|
+
# # Create and return tool instance
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# SwarmSDK::PluginRegistry.register(MyPlugin.new)
|
|
26
|
+
class Plugin
|
|
27
|
+
# Plugin name (must be unique)
|
|
28
|
+
#
|
|
29
|
+
# @return [Symbol] Plugin identifier
|
|
30
|
+
def name
|
|
31
|
+
raise NotImplementedError, "#{self.class} must implement #name"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# List of tools provided by this plugin
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<Symbol>] Tool names (e.g., [:MemoryWrite, :MemoryRead])
|
|
37
|
+
def tools
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Create a tool instance
|
|
42
|
+
#
|
|
43
|
+
# @param tool_name [Symbol] Tool name (e.g., :MemoryWrite)
|
|
44
|
+
# @param context [Hash] Creation context
|
|
45
|
+
# - :agent_name [Symbol] Agent identifier
|
|
46
|
+
# - :storage [Object] Plugin storage instance (if created)
|
|
47
|
+
# - :agent_definition [Agent::Definition] Full agent definition
|
|
48
|
+
# - :chat [Agent::Chat] Chat instance (for tools that need it)
|
|
49
|
+
# - :tool_configurator [Swarm::ToolConfigurator] For tools that register other tools
|
|
50
|
+
# @return [RubyLLM::Tool] Tool instance
|
|
51
|
+
def create_tool(tool_name, context)
|
|
52
|
+
raise NotImplementedError, "#{self.class} must implement #create_tool"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Create plugin storage for an agent (optional)
|
|
56
|
+
#
|
|
57
|
+
# Called during agent initialization. Return nil if plugin doesn't need storage.
|
|
58
|
+
#
|
|
59
|
+
# @param agent_name [Symbol] Agent identifier
|
|
60
|
+
# @param config [Object] Plugin configuration from agent definition
|
|
61
|
+
# @return [Object, nil] Storage instance or nil
|
|
62
|
+
def create_storage(agent_name:, config:)
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Parse plugin configuration from agent definition
|
|
67
|
+
#
|
|
68
|
+
# @param raw_config [Object] Raw config (DSL object or Hash from YAML)
|
|
69
|
+
# @return [Object] Parsed configuration
|
|
70
|
+
def parse_config(raw_config)
|
|
71
|
+
raw_config
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Contribute to agent system prompt (optional)
|
|
75
|
+
#
|
|
76
|
+
# @param agent_definition [Agent::Definition] Agent definition
|
|
77
|
+
# @param storage [Object, nil] Plugin storage instance (if created)
|
|
78
|
+
# @return [String, nil] Prompt contribution or nil
|
|
79
|
+
def system_prompt_contribution(agent_definition:, storage:)
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Tools that should be marked immutable (optional)
|
|
84
|
+
#
|
|
85
|
+
# Immutable tools cannot be removed by other tools (e.g., LoadSkill).
|
|
86
|
+
#
|
|
87
|
+
# @return [Array<Symbol>] Tool names
|
|
88
|
+
def immutable_tools
|
|
89
|
+
[]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Agent storage enabled for this agent? (optional)
|
|
93
|
+
#
|
|
94
|
+
# @param agent_definition [Agent::Definition] Agent definition
|
|
95
|
+
# @return [Boolean] True if storage should be created
|
|
96
|
+
def storage_enabled?(agent_definition)
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Lifecycle: Called when agent is initialized
|
|
101
|
+
#
|
|
102
|
+
# @param agent_name [Symbol] Agent identifier
|
|
103
|
+
# @param agent [Agent::Chat] Chat instance
|
|
104
|
+
# @param context [Hash] Initialization context
|
|
105
|
+
# - :storage [Object, nil] Plugin storage
|
|
106
|
+
# - :agent_definition [Agent::Definition] Definition
|
|
107
|
+
# - :tool_configurator [Swarm::ToolConfigurator] Configurator
|
|
108
|
+
def on_agent_initialized(agent_name:, agent:, context:)
|
|
109
|
+
# Override if needed
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Lifecycle: Called when swarm starts
|
|
113
|
+
#
|
|
114
|
+
# @param swarm [Swarm] Swarm instance
|
|
115
|
+
def on_swarm_started(swarm:)
|
|
116
|
+
# Override if needed
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Lifecycle: Called when swarm stops
|
|
120
|
+
#
|
|
121
|
+
# @param swarm [Swarm] Swarm instance
|
|
122
|
+
def on_swarm_stopped(swarm:)
|
|
123
|
+
# Override if needed
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Lifecycle: Called on every user message
|
|
127
|
+
#
|
|
128
|
+
# Plugins can return system reminders to inject based on the user's prompt.
|
|
129
|
+
# This enables features like semantic skill discovery, context injection, etc.
|
|
130
|
+
#
|
|
131
|
+
# @param agent_name [Symbol] Agent identifier
|
|
132
|
+
# @param prompt [String] The user's message
|
|
133
|
+
# @param is_first_message [Boolean] True if this is the first message in the conversation
|
|
134
|
+
# @return [Array<String>] System reminders to inject (empty array if none)
|
|
135
|
+
#
|
|
136
|
+
# @example Semantic skill discovery
|
|
137
|
+
# def on_user_message(agent_name:, prompt:, is_first_message:)
|
|
138
|
+
# skills = semantic_search(prompt, threshold: 0.65)
|
|
139
|
+
# return [] if skills.empty?
|
|
140
|
+
#
|
|
141
|
+
# [build_skill_reminder(skills)]
|
|
142
|
+
# end
|
|
143
|
+
def on_user_message(agent_name:, prompt:, is_first_message:)
|
|
144
|
+
[]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Plugin registry for managing SwarmSDK extensions
|
|
5
|
+
#
|
|
6
|
+
# Plugins register themselves when loaded, providing tools, storage,
|
|
7
|
+
# and lifecycle hooks without SwarmSDK needing to know about them.
|
|
8
|
+
module PluginRegistry
|
|
9
|
+
@plugins = {}
|
|
10
|
+
@tool_map = {}
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Register a plugin
|
|
14
|
+
#
|
|
15
|
+
# @param plugin [Plugin] Plugin instance
|
|
16
|
+
# @raise [ArgumentError] If plugin with same name already registered
|
|
17
|
+
def register(plugin)
|
|
18
|
+
raise ArgumentError, "Plugin must inherit from SwarmSDK::Plugin" unless plugin.is_a?(Plugin)
|
|
19
|
+
|
|
20
|
+
name = plugin.name
|
|
21
|
+
raise ArgumentError, "Plugin name required" unless name
|
|
22
|
+
raise ArgumentError, "Plugin #{name} already registered" if @plugins.key?(name)
|
|
23
|
+
|
|
24
|
+
@plugins[name] = plugin
|
|
25
|
+
|
|
26
|
+
# Build tool → plugin mapping
|
|
27
|
+
plugin.tools.each do |tool_name|
|
|
28
|
+
if @tool_map.key?(tool_name)
|
|
29
|
+
raise ArgumentError, "Tool #{tool_name} already registered by #{@tool_map[tool_name].name}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@tool_map[tool_name] = plugin
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get plugin by name
|
|
37
|
+
#
|
|
38
|
+
# @param name [Symbol] Plugin name
|
|
39
|
+
# @return [Plugin, nil] Plugin instance or nil
|
|
40
|
+
def get(name)
|
|
41
|
+
@plugins[name]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get all registered plugins
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<Plugin>] All plugins
|
|
47
|
+
def all
|
|
48
|
+
@plugins.values
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check if plugin is registered
|
|
52
|
+
#
|
|
53
|
+
# @param name [Symbol] Plugin name
|
|
54
|
+
# @return [Boolean] True if registered
|
|
55
|
+
def registered?(name)
|
|
56
|
+
@plugins.key?(name)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get plugin that provides a tool
|
|
60
|
+
#
|
|
61
|
+
# @param tool_name [Symbol] Tool name
|
|
62
|
+
# @return [Plugin, nil] Plugin that provides tool or nil
|
|
63
|
+
def plugin_for_tool(tool_name)
|
|
64
|
+
@tool_map[tool_name]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Check if tool is provided by a plugin
|
|
68
|
+
#
|
|
69
|
+
# @param tool_name [Symbol] Tool name
|
|
70
|
+
# @return [Boolean] True if tool is plugin-provided
|
|
71
|
+
def plugin_tool?(tool_name)
|
|
72
|
+
@tool_map.key?(tool_name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get all tools provided by plugins
|
|
76
|
+
#
|
|
77
|
+
# @return [Hash<Symbol, Plugin>] Tool name → Plugin mapping
|
|
78
|
+
def tools
|
|
79
|
+
@tool_map.dup
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Clear all plugins (for testing)
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
def clear
|
|
86
|
+
@plugins.clear
|
|
87
|
+
@tool_map.clear
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Emit lifecycle event to all plugins
|
|
91
|
+
#
|
|
92
|
+
# @param event [Symbol] Event name
|
|
93
|
+
# @param args [Hash] Event arguments
|
|
94
|
+
def emit_event(event, **args)
|
|
95
|
+
@plugins.each_value do |plugin|
|
|
96
|
+
plugin.public_send(event, **args) if plugin.respond_to?(event)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|