claude_swarm 1.0.1 → 1.0.4

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.
Files changed (267) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/release.md +1 -1
  3. data/.claude/hooks/lint-code-files.rb +65 -0
  4. data/.rubocop.yml +22 -2
  5. data/CHANGELOG.md +14 -1
  6. data/CLAUDE.md +1 -1
  7. data/CONTRIBUTING.md +69 -0
  8. data/README.md +27 -2
  9. data/Rakefile +71 -3
  10. data/analyze_coverage.rb +94 -0
  11. data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
  12. data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
  13. data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
  14. data/docs/v2/README.md +308 -0
  15. data/docs/v2/guides/claude-code-agents.md +262 -0
  16. data/docs/v2/guides/complete-tutorial.md +3088 -0
  17. data/docs/v2/guides/getting-started.md +1456 -0
  18. data/docs/v2/guides/memory-adapters.md +998 -0
  19. data/docs/v2/guides/plugins.md +816 -0
  20. data/docs/v2/guides/quick-start-cli.md +1745 -0
  21. data/docs/v2/guides/rails-integration.md +1902 -0
  22. data/docs/v2/guides/swarm-memory.md +599 -0
  23. data/docs/v2/reference/cli.md +729 -0
  24. data/docs/v2/reference/ruby-dsl.md +2154 -0
  25. data/docs/v2/reference/yaml.md +1835 -0
  26. data/docs-team-swarm.yml +2222 -0
  27. data/examples/learning-assistant/assistant.md +7 -0
  28. data/examples/learning-assistant/example-memories/concept-example.md +90 -0
  29. data/examples/learning-assistant/example-memories/experience-example.md +66 -0
  30. data/examples/learning-assistant/example-memories/fact-example.md +76 -0
  31. data/examples/learning-assistant/example-memories/memory-index.md +78 -0
  32. data/examples/learning-assistant/example-memories/skill-example.md +168 -0
  33. data/examples/learning-assistant/learning_assistant.rb +34 -0
  34. data/examples/learning-assistant/learning_assistant.yml +20 -0
  35. data/examples/v2/dsl/01_basic.rb +44 -0
  36. data/examples/v2/dsl/02_core_parameters.rb +59 -0
  37. data/examples/v2/dsl/03_capabilities.rb +71 -0
  38. data/examples/v2/dsl/04_llm_parameters.rb +56 -0
  39. data/examples/v2/dsl/05_advanced_flags.rb +73 -0
  40. data/examples/v2/dsl/06_permissions.rb +80 -0
  41. data/examples/v2/dsl/07_mcp_server.rb +62 -0
  42. data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
  43. data/examples/v2/dsl/09_agent_hooks.rb +67 -0
  44. data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
  45. data/examples/v2/dsl/11_delegation.rb +60 -0
  46. data/examples/v2/dsl/12_complete_integration.rb +137 -0
  47. data/examples/v2/file_tools_swarm.yml +102 -0
  48. data/examples/v2/hooks/01_basic_hooks.rb +133 -0
  49. data/examples/v2/hooks/02_usage_tracking.rb +201 -0
  50. data/examples/v2/hooks/03_production_monitoring.rb +429 -0
  51. data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
  52. data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
  53. data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
  54. data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
  55. data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
  56. data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
  57. data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
  58. data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
  59. data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
  60. data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
  61. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
  62. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
  63. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
  64. data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
  65. data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
  66. data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
  67. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
  68. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
  69. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
  70. data/examples/v2/hooks/swarm_summary.sh +44 -0
  71. data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
  72. data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
  73. data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
  74. data/examples/v2/hooks/validate_bash.rb +59 -0
  75. data/examples/v2/multi_directory_permissions.yml +221 -0
  76. data/examples/v2/node_context_demo.rb +127 -0
  77. data/examples/v2/node_workflow.rb +173 -0
  78. data/examples/v2/path_resolution_demo.rb +216 -0
  79. data/examples/v2/simple-swarm-v2.rb +90 -0
  80. data/examples/v2/simple-swarm-v2.yml +62 -0
  81. data/examples/v2/swarm.yml +71 -0
  82. data/examples/v2/swarm_with_hooks.yml +61 -0
  83. data/examples/v2/swarm_with_hooks_simple.yml +25 -0
  84. data/examples/v2/think_tool_demo.rb +62 -0
  85. data/exe/swarm +6 -0
  86. data/lib/claude_swarm/claude_mcp_server.rb +0 -6
  87. data/lib/claude_swarm/cli.rb +10 -3
  88. data/lib/claude_swarm/commands/ps.rb +19 -20
  89. data/lib/claude_swarm/commands/show.rb +1 -1
  90. data/lib/claude_swarm/configuration.rb +10 -12
  91. data/lib/claude_swarm/mcp_generator.rb +10 -1
  92. data/lib/claude_swarm/orchestrator.rb +73 -49
  93. data/lib/claude_swarm/system_utils.rb +37 -11
  94. data/lib/claude_swarm/version.rb +1 -1
  95. data/lib/claude_swarm/worktree_manager.rb +1 -0
  96. data/lib/claude_swarm/yaml_loader.rb +22 -0
  97. data/lib/claude_swarm.rb +7 -2
  98. data/lib/swarm_cli/cli.rb +201 -0
  99. data/lib/swarm_cli/command_registry.rb +61 -0
  100. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  101. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  102. data/lib/swarm_cli/commands/migrate.rb +55 -0
  103. data/lib/swarm_cli/commands/run.rb +173 -0
  104. data/lib/swarm_cli/config_loader.rb +97 -0
  105. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  106. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  107. data/lib/swarm_cli/interactive_repl.rb +918 -0
  108. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  109. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  110. data/lib/swarm_cli/migrate_options.rb +54 -0
  111. data/lib/swarm_cli/migrator.rb +132 -0
  112. data/lib/swarm_cli/options.rb +151 -0
  113. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  114. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  115. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  116. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  117. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  118. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  119. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  120. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  121. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  122. data/lib/swarm_cli/ui/icons.rb +59 -0
  123. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  124. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  125. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  126. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  127. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  128. data/lib/swarm_cli/version.rb +5 -0
  129. data/lib/swarm_cli.rb +44 -0
  130. data/lib/swarm_memory/adapters/base.rb +141 -0
  131. data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
  132. data/lib/swarm_memory/chat_extension.rb +34 -0
  133. data/lib/swarm_memory/cli/commands.rb +306 -0
  134. data/lib/swarm_memory/core/entry.rb +37 -0
  135. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  136. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  137. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  138. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  139. data/lib/swarm_memory/core/storage.rb +288 -0
  140. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  141. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  142. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  143. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  144. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  145. data/lib/swarm_memory/errors.rb +21 -0
  146. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  147. data/lib/swarm_memory/integration/configuration.rb +43 -0
  148. data/lib/swarm_memory/integration/registration.rb +31 -0
  149. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  150. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  151. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  152. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  153. data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
  154. data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
  155. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
  156. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  157. data/lib/swarm_memory/search/text_search.rb +42 -0
  158. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  159. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  160. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  161. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  162. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  163. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  164. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  165. data/lib/swarm_memory/tools/memory_glob.rb +160 -0
  166. data/lib/swarm_memory/tools/memory_grep.rb +247 -0
  167. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  168. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  169. data/lib/swarm_memory/tools/memory_write.rb +231 -0
  170. data/lib/swarm_memory/utils.rb +50 -0
  171. data/lib/swarm_memory/version.rb +5 -0
  172. data/lib/swarm_memory.rb +166 -0
  173. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  174. data/lib/swarm_sdk/agent/builder.rb +461 -0
  175. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  176. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  177. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  178. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  179. data/lib/swarm_sdk/agent/chat.rb +1159 -0
  180. data/lib/swarm_sdk/agent/context.rb +112 -0
  181. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  182. data/lib/swarm_sdk/agent/definition.rb +556 -0
  183. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  184. data/lib/swarm_sdk/configuration.rb +296 -0
  185. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  186. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  187. data/lib/swarm_sdk/context_compactor.rb +340 -0
  188. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  189. data/lib/swarm_sdk/hooks/context.rb +197 -0
  190. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  191. data/lib/swarm_sdk/hooks/error.rb +29 -0
  192. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  193. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  194. data/lib/swarm_sdk/hooks/result.rb +150 -0
  195. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  196. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  197. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  198. data/lib/swarm_sdk/log_collector.rb +51 -0
  199. data/lib/swarm_sdk/log_stream.rb +69 -0
  200. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  201. data/lib/swarm_sdk/model_aliases.json +5 -0
  202. data/lib/swarm_sdk/models.json +1 -0
  203. data/lib/swarm_sdk/models.rb +120 -0
  204. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  205. data/lib/swarm_sdk/node/builder.rb +439 -0
  206. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  207. data/lib/swarm_sdk/node_context.rb +170 -0
  208. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  209. data/lib/swarm_sdk/permissions/config.rb +239 -0
  210. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  211. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  212. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  213. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  214. data/lib/swarm_sdk/plugin.rb +147 -0
  215. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  216. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  217. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  218. data/lib/swarm_sdk/result.rb +97 -0
  219. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  220. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  221. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  222. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  223. data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
  224. data/lib/swarm_sdk/swarm.rb +982 -0
  225. data/lib/swarm_sdk/tools/bash.rb +274 -0
  226. data/lib/swarm_sdk/tools/clock.rb +44 -0
  227. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  228. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  229. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  230. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  231. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  232. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  233. data/lib/swarm_sdk/tools/edit.rb +150 -0
  234. data/lib/swarm_sdk/tools/glob.rb +158 -0
  235. data/lib/swarm_sdk/tools/grep.rb +228 -0
  236. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  237. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  238. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  239. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  240. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  241. data/lib/swarm_sdk/tools/read.rb +251 -0
  242. data/lib/swarm_sdk/tools/registry.rb +93 -0
  243. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  244. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  245. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  246. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  247. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  248. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  249. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  250. data/lib/swarm_sdk/tools/think.rb +95 -0
  251. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  252. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  253. data/lib/swarm_sdk/tools/write.rb +117 -0
  254. data/lib/swarm_sdk/utils.rb +50 -0
  255. data/lib/swarm_sdk/version.rb +5 -0
  256. data/lib/swarm_sdk.rb +157 -0
  257. data/llm.v2.txt +13407 -0
  258. data/rubocop/cop/security/no_reflection_methods.rb +47 -0
  259. data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
  260. data/swarm_cli.gemspec +57 -0
  261. data/swarm_memory.gemspec +28 -0
  262. data/swarm_sdk.gemspec +41 -0
  263. data/team.yml +1 -1
  264. data/team_full.yml +1875 -0
  265. data/{team_v2.yml → team_sdk.yml} +121 -52
  266. metadata +249 -6
  267. 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