shared_tools 0.2.1 → 0.3.0
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 +3 -0
- data/README.md +594 -42
- data/lib/shared_tools/{ruby_llm/mcp → mcp}/github_mcp_server.rb +31 -24
- data/lib/shared_tools/mcp/imcp.rb +28 -0
- data/lib/shared_tools/mcp/tavily_mcp_server.rb +44 -0
- data/lib/shared_tools/mcp.rb +24 -0
- data/lib/shared_tools/tools/browser/base_driver.rb +64 -0
- data/lib/shared_tools/tools/browser/base_tool.rb +50 -0
- data/lib/shared_tools/tools/browser/click_tool.rb +54 -0
- data/lib/shared_tools/tools/browser/elements/element_grouper.rb +73 -0
- data/lib/shared_tools/tools/browser/elements/nearby_element_detector.rb +109 -0
- data/lib/shared_tools/tools/browser/formatters/action_formatter.rb +37 -0
- data/lib/shared_tools/tools/browser/formatters/data_entry_formatter.rb +135 -0
- data/lib/shared_tools/tools/browser/formatters/element_formatter.rb +52 -0
- data/lib/shared_tools/tools/browser/formatters/input_formatter.rb +59 -0
- data/lib/shared_tools/tools/browser/inspect_tool.rb +87 -0
- data/lib/shared_tools/tools/browser/inspect_utils.rb +51 -0
- data/lib/shared_tools/tools/browser/page_inspect/button_summarizer.rb +140 -0
- data/lib/shared_tools/tools/browser/page_inspect/form_summarizer.rb +98 -0
- data/lib/shared_tools/tools/browser/page_inspect/html_summarizer.rb +37 -0
- data/lib/shared_tools/tools/browser/page_inspect/link_summarizer.rb +103 -0
- data/lib/shared_tools/tools/browser/page_inspect_tool.rb +55 -0
- data/lib/shared_tools/tools/browser/page_screenshot_tool.rb +39 -0
- data/lib/shared_tools/tools/browser/selector_generator/base_selectors.rb +28 -0
- data/lib/shared_tools/tools/browser/selector_generator/contextual_selectors.rb +140 -0
- data/lib/shared_tools/tools/browser/selector_generator.rb +73 -0
- data/lib/shared_tools/tools/browser/selector_inspect_tool.rb +67 -0
- data/lib/shared_tools/tools/browser/text_field_area_set_tool.rb +45 -0
- data/lib/shared_tools/tools/browser/visit_tool.rb +43 -0
- data/lib/shared_tools/tools/browser/watir_driver.rb +132 -0
- data/lib/shared_tools/tools/browser.rb +27 -0
- data/lib/shared_tools/tools/browser_tool.rb +255 -0
- data/lib/shared_tools/tools/calculator_tool.rb +169 -0
- data/lib/shared_tools/tools/composite_analysis_tool.rb +520 -0
- data/lib/shared_tools/tools/computer/base_driver.rb +177 -0
- data/lib/shared_tools/tools/computer/mac_driver.rb +103 -0
- data/lib/shared_tools/tools/computer.rb +21 -0
- data/lib/shared_tools/tools/computer_tool.rb +207 -0
- data/lib/shared_tools/tools/data_science_kit.rb +707 -0
- data/lib/shared_tools/tools/database/base_driver.rb +17 -0
- data/lib/shared_tools/tools/database/postgres_driver.rb +30 -0
- data/lib/shared_tools/tools/database/sqlite_driver.rb +29 -0
- data/lib/shared_tools/tools/database.rb +9 -0
- data/lib/shared_tools/tools/database_query_tool.rb +313 -0
- data/lib/shared_tools/tools/database_tool.rb +99 -0
- data/lib/shared_tools/tools/devops_toolkit.rb +420 -0
- data/lib/shared_tools/tools/disk/base_driver.rb +91 -0
- data/lib/shared_tools/tools/disk/base_tool.rb +20 -0
- data/lib/shared_tools/tools/disk/directory_create_tool.rb +39 -0
- data/lib/shared_tools/tools/disk/directory_delete_tool.rb +39 -0
- data/lib/shared_tools/tools/disk/directory_list_tool.rb +37 -0
- data/lib/shared_tools/tools/disk/directory_move_tool.rb +40 -0
- data/lib/shared_tools/tools/disk/file_create_tool.rb +38 -0
- data/lib/shared_tools/tools/disk/file_delete_tool.rb +40 -0
- data/lib/shared_tools/tools/disk/file_move_tool.rb +43 -0
- data/lib/shared_tools/tools/disk/file_read_tool.rb +40 -0
- data/lib/shared_tools/tools/disk/file_replace_tool.rb +44 -0
- data/lib/shared_tools/tools/disk/file_write_tool.rb +40 -0
- data/lib/shared_tools/tools/disk/local_driver.rb +91 -0
- data/lib/shared_tools/tools/disk.rb +17 -0
- data/lib/shared_tools/tools/disk_tool.rb +132 -0
- data/lib/shared_tools/tools/doc/pdf_reader_tool.rb +79 -0
- data/lib/shared_tools/tools/doc.rb +8 -0
- data/lib/shared_tools/tools/doc_tool.rb +109 -0
- data/lib/shared_tools/tools/docker/base_tool.rb +56 -0
- data/lib/shared_tools/tools/docker/compose_run_tool.rb +77 -0
- data/lib/shared_tools/tools/docker.rb +8 -0
- data/lib/shared_tools/tools/error_handling_tool.rb +403 -0
- data/lib/shared_tools/tools/eval/python_eval_tool.rb +209 -0
- data/lib/shared_tools/tools/eval/ruby_eval_tool.rb +93 -0
- data/lib/shared_tools/tools/eval/shell_eval_tool.rb +64 -0
- data/lib/shared_tools/tools/eval.rb +10 -0
- data/lib/shared_tools/tools/eval_tool.rb +139 -0
- data/lib/shared_tools/tools/secure_tool_template.rb +353 -0
- data/lib/shared_tools/tools/version.rb +7 -0
- data/lib/shared_tools/tools/weather_tool.rb +197 -0
- data/lib/shared_tools/tools/workflow_manager_tool.rb +312 -0
- data/lib/shared_tools/tools.rb +16 -0
- data/lib/shared_tools/version.rb +1 -1
- data/lib/shared_tools.rb +9 -33
- metadata +189 -68
- data/lib/shared_tools/llm_rb/run_shell_command.rb +0 -23
- data/lib/shared_tools/llm_rb.rb +0 -9
- data/lib/shared_tools/omniai.rb +0 -9
- data/lib/shared_tools/raix/what_is_the_weather.rb +0 -18
- data/lib/shared_tools/raix.rb +0 -9
- data/lib/shared_tools/ruby_llm/edit_file.rb +0 -71
- data/lib/shared_tools/ruby_llm/incomplete/calculator_tool.rb +0 -70
- data/lib/shared_tools/ruby_llm/incomplete/composite_analysis_tool.rb +0 -89
- data/lib/shared_tools/ruby_llm/incomplete/data_science_kit.rb +0 -128
- data/lib/shared_tools/ruby_llm/incomplete/database_query_tool.rb +0 -100
- data/lib/shared_tools/ruby_llm/incomplete/devops_toolkit.rb +0 -112
- data/lib/shared_tools/ruby_llm/incomplete/error_handling_tool.rb +0 -109
- data/lib/shared_tools/ruby_llm/incomplete/secure_tool_template.rb +0 -117
- data/lib/shared_tools/ruby_llm/incomplete/weather_tool.rb +0 -110
- data/lib/shared_tools/ruby_llm/incomplete/workflow_manager_tool.rb +0 -145
- data/lib/shared_tools/ruby_llm/list_files.rb +0 -49
- data/lib/shared_tools/ruby_llm/mcp/imcp.rb +0 -33
- data/lib/shared_tools/ruby_llm/mcp.rb +0 -10
- data/lib/shared_tools/ruby_llm/pdf_page_reader.rb +0 -59
- data/lib/shared_tools/ruby_llm/python_eval.rb +0 -194
- data/lib/shared_tools/ruby_llm/read_file.rb +0 -40
- data/lib/shared_tools/ruby_llm/ruby_eval.rb +0 -77
- data/lib/shared_tools/ruby_llm/run_shell_command.rb +0 -49
- data/lib/shared_tools/ruby_llm.rb +0 -12
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# secure_tool_template.rb - Comprehensive security best practices template
|
|
2
|
+
require 'ruby_llm/tool'
|
|
3
|
+
require 'timeout'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
module SharedTools
|
|
7
|
+
module Tools
|
|
8
|
+
class SecureToolTemplate < RubyLLM::Tool
|
|
9
|
+
def self.name = 'secure_tool_template'
|
|
10
|
+
|
|
11
|
+
description <<~'DESCRIPTION'
|
|
12
|
+
Reference template demonstrating comprehensive security best practices for safe tool development.
|
|
13
|
+
This tool serves as a complete security framework implementation that can be adapted for other
|
|
14
|
+
tools handling sensitive data or performing privileged operations. It demonstrates all essential
|
|
15
|
+
security mechanisms that should be considered when building production-ready AI tools.
|
|
16
|
+
|
|
17
|
+
Security features implemented:
|
|
18
|
+
- Input validation with whitelist filtering
|
|
19
|
+
- Output sanitization to prevent information leakage
|
|
20
|
+
- Permission and authorization checks
|
|
21
|
+
- Rate limiting to prevent abuse
|
|
22
|
+
- Comprehensive audit logging
|
|
23
|
+
- Timeout mechanisms for resource control
|
|
24
|
+
- Security violation tracking
|
|
25
|
+
- Error handling without information disclosure
|
|
26
|
+
|
|
27
|
+
This template can be used as a starting point for developing secure tools that interact with
|
|
28
|
+
sensitive systems, handle user data, or perform privileged operations.
|
|
29
|
+
DESCRIPTION
|
|
30
|
+
|
|
31
|
+
params do
|
|
32
|
+
string :user_input, description: <<~DESC.strip
|
|
33
|
+
User-provided input string that will be processed with comprehensive security validation.
|
|
34
|
+
The input undergoes multiple security checks:
|
|
35
|
+
- Length validation (maximum 1000 characters)
|
|
36
|
+
- Character whitelist (alphanumeric, spaces, hyphens, underscores, dots)
|
|
37
|
+
- Sanitization to remove potentially dangerous characters
|
|
38
|
+
- Rate limiting per user/session
|
|
39
|
+
All validation failures are logged for security monitoring and compliance.
|
|
40
|
+
DESC
|
|
41
|
+
|
|
42
|
+
string :operation_type, description: <<~DESC.strip, required: false
|
|
43
|
+
Type of operation to perform on the input. Options:
|
|
44
|
+
- 'read': Read-only operation (lowest security requirements)
|
|
45
|
+
- 'write': Data modification operation (moderate security requirements)
|
|
46
|
+
- 'admin': Administrative operation (highest security requirements)
|
|
47
|
+
Default is 'read'. Operations with higher security levels require additional validations.
|
|
48
|
+
DESC
|
|
49
|
+
|
|
50
|
+
integer :timeout_seconds, description: <<~DESC.strip, required: false
|
|
51
|
+
Maximum execution time in seconds for the operation. Range: 1-300 seconds.
|
|
52
|
+
Default: 30 seconds. Prevents resource exhaustion and ensures responsive operations.
|
|
53
|
+
Timeout enforces proper resource management and prevents hung operations.
|
|
54
|
+
DESC
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Rate limiting store (in production, use Redis or similar)
|
|
58
|
+
@rate_limit_store = {}
|
|
59
|
+
@rate_limit_mutex = Mutex.new
|
|
60
|
+
|
|
61
|
+
def initialize(logger: nil)
|
|
62
|
+
@logger = logger || RubyLLM.logger
|
|
63
|
+
@audit_log = []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def execute(user_input:, operation_type: 'read', timeout_seconds: 30)
|
|
67
|
+
execution_id = SecureRandom.uuid
|
|
68
|
+
start_time = Time.now
|
|
69
|
+
|
|
70
|
+
@logger.info("SecureToolTemplate#execute operation_type=#{operation_type} execution_id=#{execution_id}")
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
# 1. Validate input length
|
|
74
|
+
validate_input_length(user_input)
|
|
75
|
+
|
|
76
|
+
# 2. Sanitize inputs
|
|
77
|
+
sanitized_input = sanitize_input(user_input)
|
|
78
|
+
|
|
79
|
+
# 3. Validate permissions for operation type
|
|
80
|
+
validate_permissions(operation_type)
|
|
81
|
+
|
|
82
|
+
# 4. Check rate limits
|
|
83
|
+
check_rate_limits(execution_id)
|
|
84
|
+
|
|
85
|
+
# 5. Audit logging
|
|
86
|
+
log_tool_usage(execution_id, operation_type, sanitized_input)
|
|
87
|
+
|
|
88
|
+
# 6. Execute with timeout
|
|
89
|
+
timeout = validate_timeout(timeout_seconds)
|
|
90
|
+
result = execute_with_timeout(sanitized_input, operation_type, timeout)
|
|
91
|
+
|
|
92
|
+
# 7. Sanitize outputs
|
|
93
|
+
sanitized_result = sanitize_output(result)
|
|
94
|
+
|
|
95
|
+
execution_time = (Time.now - start_time).round(3)
|
|
96
|
+
@logger.info("Operation completed successfully in #{execution_time}s")
|
|
97
|
+
|
|
98
|
+
{
|
|
99
|
+
success: true,
|
|
100
|
+
result: sanitized_result,
|
|
101
|
+
operation_type: operation_type,
|
|
102
|
+
execution_id: execution_id,
|
|
103
|
+
execution_time_seconds: execution_time,
|
|
104
|
+
executed_at: Time.now.iso8601
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
rescue SecurityError => e
|
|
108
|
+
@logger.error("Security violation: #{e.message}")
|
|
109
|
+
log_security_violation(e, execution_id, user_input)
|
|
110
|
+
{
|
|
111
|
+
success: false,
|
|
112
|
+
error: "Security violation: Access denied",
|
|
113
|
+
error_type: "security",
|
|
114
|
+
violation_logged: true,
|
|
115
|
+
execution_id: execution_id
|
|
116
|
+
}
|
|
117
|
+
rescue Timeout::Error => e
|
|
118
|
+
@logger.error("Operation timeout after #{timeout_seconds}s")
|
|
119
|
+
{
|
|
120
|
+
success: false,
|
|
121
|
+
error: "Operation exceeded timeout of #{timeout_seconds} seconds",
|
|
122
|
+
error_type: "timeout",
|
|
123
|
+
execution_id: execution_id
|
|
124
|
+
}
|
|
125
|
+
rescue ArgumentError => e
|
|
126
|
+
@logger.error("Validation error: #{e.message}")
|
|
127
|
+
{
|
|
128
|
+
success: false,
|
|
129
|
+
error: e.message,
|
|
130
|
+
error_type: "validation",
|
|
131
|
+
execution_id: execution_id
|
|
132
|
+
}
|
|
133
|
+
rescue => e
|
|
134
|
+
@logger.error("Tool execution failed: #{e.class} - #{e.message}")
|
|
135
|
+
{
|
|
136
|
+
success: false,
|
|
137
|
+
error: "Tool execution failed: #{e.message}",
|
|
138
|
+
error_type: e.class.name,
|
|
139
|
+
execution_id: execution_id
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# Validate input length to prevent buffer overflow attacks
|
|
147
|
+
def validate_input_length(input)
|
|
148
|
+
if input.nil? || input.empty?
|
|
149
|
+
raise ArgumentError, "Input cannot be empty"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if input.length > 1000
|
|
153
|
+
raise ArgumentError, "Input too long: #{input.length} characters (maximum 1000)"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
@logger.debug("Input length validation passed: #{input.length} characters")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Sanitize input by removing potentially dangerous characters
|
|
160
|
+
def sanitize_input(input)
|
|
161
|
+
# Remove all characters except alphanumeric, spaces, hyphens, underscores, and dots
|
|
162
|
+
sanitized = input.gsub(/[^\w\s\-\.]/, '')
|
|
163
|
+
|
|
164
|
+
@logger.debug("Input sanitized (#{input.length} -> #{sanitized.length} characters)")
|
|
165
|
+
sanitized
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Validate user permissions based on operation type
|
|
169
|
+
def validate_permissions(operation_type)
|
|
170
|
+
# In production, this would check actual user permissions from auth system
|
|
171
|
+
valid_operations = ['read', 'write', 'admin']
|
|
172
|
+
|
|
173
|
+
unless valid_operations.include?(operation_type)
|
|
174
|
+
raise SecurityError, "Invalid operation type: #{operation_type}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Admin operations require elevated privileges
|
|
178
|
+
if operation_type == 'admin'
|
|
179
|
+
# In production: verify admin role from authentication context
|
|
180
|
+
@logger.warn("Admin operation requested - elevated privileges required")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
@logger.debug("Permission validation passed for operation: #{operation_type}")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Check rate limits to prevent abuse
|
|
187
|
+
def check_rate_limits(execution_id)
|
|
188
|
+
# Simple rate limiting (in production, use Redis with sliding windows)
|
|
189
|
+
self.class.instance_variable_get(:@rate_limit_mutex).synchronize do
|
|
190
|
+
store = self.class.instance_variable_get(:@rate_limit_store)
|
|
191
|
+
current_time = Time.now.to_i
|
|
192
|
+
|
|
193
|
+
# Clean old entries (older than 1 minute)
|
|
194
|
+
store.reject! { |k, v| v < current_time - 60 }
|
|
195
|
+
|
|
196
|
+
# Count requests in last minute (max 30 per minute)
|
|
197
|
+
recent_requests = store.values.count { |time| time > current_time - 60 }
|
|
198
|
+
|
|
199
|
+
if recent_requests >= 30
|
|
200
|
+
raise SecurityError, "Rate limit exceeded: #{recent_requests} requests in last minute"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Record this request
|
|
204
|
+
store[execution_id] = current_time
|
|
205
|
+
|
|
206
|
+
@logger.debug("Rate limit check passed: #{recent_requests + 1}/30 requests")
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Log tool usage for audit trail
|
|
211
|
+
def log_tool_usage(execution_id, operation_type, sanitized_input)
|
|
212
|
+
audit_entry = {
|
|
213
|
+
execution_id: execution_id,
|
|
214
|
+
operation_type: operation_type,
|
|
215
|
+
input_preview: sanitized_input[0..50],
|
|
216
|
+
timestamp: Time.now.iso8601,
|
|
217
|
+
user_context: "system" # In production: actual user ID from auth context
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@audit_log << audit_entry
|
|
221
|
+
@logger.debug("Audit log entry created: #{execution_id}")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Validate and normalize timeout value
|
|
225
|
+
def validate_timeout(timeout)
|
|
226
|
+
timeout = timeout.to_i
|
|
227
|
+
|
|
228
|
+
if timeout < 1
|
|
229
|
+
@logger.warn("Timeout #{timeout} too low, adjusting to 1")
|
|
230
|
+
return 1
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
if timeout > 300
|
|
234
|
+
@logger.warn("Timeout #{timeout} too high, adjusting to 300")
|
|
235
|
+
return 300
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
timeout
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Execute operation with timeout protection
|
|
242
|
+
def execute_with_timeout(input, operation_type, timeout)
|
|
243
|
+
@logger.debug("Executing operation with #{timeout}s timeout")
|
|
244
|
+
|
|
245
|
+
Timeout::timeout(timeout) do
|
|
246
|
+
# Simulate actual tool logic based on operation type
|
|
247
|
+
case operation_type
|
|
248
|
+
when 'read'
|
|
249
|
+
perform_read_operation(input)
|
|
250
|
+
when 'write'
|
|
251
|
+
perform_write_operation(input)
|
|
252
|
+
when 'admin'
|
|
253
|
+
perform_admin_operation(input)
|
|
254
|
+
else
|
|
255
|
+
raise ArgumentError, "Unknown operation type: #{operation_type}"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Simulate read operation
|
|
261
|
+
def perform_read_operation(input)
|
|
262
|
+
# In production: actual read logic here
|
|
263
|
+
{
|
|
264
|
+
operation: 'read',
|
|
265
|
+
data: {
|
|
266
|
+
input_received: input,
|
|
267
|
+
character_count: input.length,
|
|
268
|
+
word_count: input.split.length,
|
|
269
|
+
processed: true
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Simulate write operation
|
|
275
|
+
def perform_write_operation(input)
|
|
276
|
+
# In production: actual write logic here
|
|
277
|
+
# This would have additional security checks
|
|
278
|
+
{
|
|
279
|
+
operation: 'write',
|
|
280
|
+
data: {
|
|
281
|
+
written: true,
|
|
282
|
+
input_length: input.length,
|
|
283
|
+
timestamp: Time.now.iso8601
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Simulate admin operation
|
|
289
|
+
def perform_admin_operation(input)
|
|
290
|
+
# In production: actual admin logic here
|
|
291
|
+
# This would have the strictest security checks
|
|
292
|
+
{
|
|
293
|
+
operation: 'admin',
|
|
294
|
+
data: {
|
|
295
|
+
executed: true,
|
|
296
|
+
admin_action: 'completed',
|
|
297
|
+
requires_review: true
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Sanitize output to prevent information leakage
|
|
303
|
+
def sanitize_output(output)
|
|
304
|
+
# Remove any potentially sensitive information from output
|
|
305
|
+
# In production: redact PII, credentials, internal paths, etc.
|
|
306
|
+
|
|
307
|
+
if output.is_a?(Hash)
|
|
308
|
+
# Recursively sanitize hash values
|
|
309
|
+
output.transform_values { |v| sanitize_value(v) }
|
|
310
|
+
else
|
|
311
|
+
sanitize_value(output)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Sanitize individual value
|
|
316
|
+
def sanitize_value(value)
|
|
317
|
+
case value
|
|
318
|
+
when Hash
|
|
319
|
+
value.transform_values { |v| sanitize_value(v) }
|
|
320
|
+
when Array
|
|
321
|
+
value.map { |v| sanitize_value(v) }
|
|
322
|
+
when String
|
|
323
|
+
# Remove any patterns that look like credentials, tokens, or keys
|
|
324
|
+
value.gsub(/\b[A-Za-z0-9_-]{20,}\b/, '[REDACTED]')
|
|
325
|
+
else
|
|
326
|
+
value
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Log security violations for monitoring
|
|
331
|
+
def log_security_violation(error, execution_id, input)
|
|
332
|
+
violation_entry = {
|
|
333
|
+
execution_id: execution_id,
|
|
334
|
+
violation_type: error.class.name,
|
|
335
|
+
message: error.message,
|
|
336
|
+
input_preview: input[0..50],
|
|
337
|
+
timestamp: Time.now.iso8601,
|
|
338
|
+
severity: 'high'
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
@audit_log << violation_entry
|
|
342
|
+
@logger.error("SECURITY VIOLATION: #{violation_entry.to_json}")
|
|
343
|
+
|
|
344
|
+
# In production: send alert to security monitoring system
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Accessor for audit log (for testing)
|
|
348
|
+
def audit_log
|
|
349
|
+
@audit_log
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# weather_tool.rb - API integration example
|
|
2
|
+
require 'ruby_llm/tool'
|
|
3
|
+
require 'openweathermap'
|
|
4
|
+
|
|
5
|
+
module SharedTools
|
|
6
|
+
module Tools
|
|
7
|
+
class WeatherTool < RubyLLM::Tool
|
|
8
|
+
def self.name = 'weather_tool'
|
|
9
|
+
|
|
10
|
+
description <<~'DESCRIPTION'
|
|
11
|
+
Retrieve comprehensive current weather information for any city worldwide using the OpenWeatherMap API.
|
|
12
|
+
This tool provides real-time weather data including temperature, atmospheric conditions, humidity,
|
|
13
|
+
and wind information. It supports multiple temperature units and can optionally include extended
|
|
14
|
+
forecast data. The tool requires a valid OpenWeatherMap API key to be configured in the
|
|
15
|
+
OPENWEATHER_API_KEY environment variable. All weather data is fetched in real-time and includes
|
|
16
|
+
timestamps for accuracy verification.
|
|
17
|
+
|
|
18
|
+
Example usage:
|
|
19
|
+
tool = SharedTools::Tools::WeatherTool.new
|
|
20
|
+
result = tool.execute(city: "London,UK", units: "metric")
|
|
21
|
+
puts "Temperature: #{result[:current][:temperature]}°C"
|
|
22
|
+
DESCRIPTION
|
|
23
|
+
|
|
24
|
+
params do
|
|
25
|
+
string :city, description: <<~DESC.strip
|
|
26
|
+
Name of the city for weather lookup. Can include city name only (e.g., 'London')
|
|
27
|
+
or city with country code for better accuracy (e.g., 'London,UK' or 'Paris,FR').
|
|
28
|
+
For cities with common names in multiple countries, including the country code
|
|
29
|
+
is recommended to ensure accurate results. The API will attempt to find the
|
|
30
|
+
closest match if an exact match is not found.
|
|
31
|
+
DESC
|
|
32
|
+
|
|
33
|
+
string :units, description: <<~DESC.strip, required: false
|
|
34
|
+
Temperature unit system for the weather data. Options are:
|
|
35
|
+
- 'metric': Temperature in Celsius, wind speed in m/s, pressure in hPa
|
|
36
|
+
- 'imperial': Temperature in Fahrenheit, wind speed in mph, pressure in hPa
|
|
37
|
+
- 'kelvin': Temperature in Kelvin (scientific standard), wind speed in m/s
|
|
38
|
+
Default is 'metric' which is most commonly used internationally.
|
|
39
|
+
DESC
|
|
40
|
+
|
|
41
|
+
boolean :include_forecast, description: <<~DESC.strip, required: false
|
|
42
|
+
Boolean flag to include a 3-day weather forecast in addition to current conditions.
|
|
43
|
+
When set to true, the response will include forecast data with daily high/low temperatures,
|
|
44
|
+
precipitation probability, and general weather conditions for the next three days.
|
|
45
|
+
This requires additional API calls and may increase response time slightly.
|
|
46
|
+
DESC
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param logger [Logger] optional logger
|
|
50
|
+
def initialize(logger: nil)
|
|
51
|
+
@logger = logger || RubyLLM.logger
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Execute weather lookup for specified city
|
|
55
|
+
#
|
|
56
|
+
# @param city [String] City name, optionally with country code
|
|
57
|
+
# @param units [String] Unit system: metric, imperial, or kelvin
|
|
58
|
+
# @param include_forecast [Boolean] Whether to include 3-day forecast
|
|
59
|
+
#
|
|
60
|
+
# @return [Hash] Weather data with success status
|
|
61
|
+
def execute(city:, units: "metric", include_forecast: false)
|
|
62
|
+
@logger.info("WeatherTool#execute city=#{city.inspect} units=#{units} include_forecast=#{include_forecast}")
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
api_key = ENV['OPENWEATHER_API_KEY']
|
|
66
|
+
unless api_key
|
|
67
|
+
@logger.error("OpenWeather API key not configured in OPENWEATHER_API_KEY environment variable")
|
|
68
|
+
raise "OpenWeather API key not configured"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Create API client with units mapping
|
|
72
|
+
api_units = map_units_to_api(units)
|
|
73
|
+
api = OpenWeatherMap::API.new(api_key, 'en', api_units)
|
|
74
|
+
|
|
75
|
+
current_weather = fetch_current_weather(api, city)
|
|
76
|
+
result = {
|
|
77
|
+
success: true,
|
|
78
|
+
city: city,
|
|
79
|
+
current: current_weather,
|
|
80
|
+
units: units,
|
|
81
|
+
timestamp: Time.now.iso8601
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if include_forecast
|
|
85
|
+
@logger.debug("Fetching forecast data for #{city}")
|
|
86
|
+
forecast_data = fetch_forecast(api, city)
|
|
87
|
+
result[:forecast] = forecast_data
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@logger.info("Weather data retrieved successfully for #{city}")
|
|
91
|
+
result
|
|
92
|
+
rescue => e
|
|
93
|
+
@logger.error("Weather lookup failed for #{city}: #{e.message}")
|
|
94
|
+
{
|
|
95
|
+
success: false,
|
|
96
|
+
error: e.message,
|
|
97
|
+
city: city,
|
|
98
|
+
suggestion: "Verify city name and API key configuration"
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Map our unit names to OpenWeatherMap API unit names
|
|
106
|
+
#
|
|
107
|
+
# @param units [String] Our unit system name
|
|
108
|
+
# @return [String] API unit system name
|
|
109
|
+
def map_units_to_api(units)
|
|
110
|
+
case units.to_s.downcase
|
|
111
|
+
when 'imperial'
|
|
112
|
+
'imperial'
|
|
113
|
+
when 'kelvin'
|
|
114
|
+
'standard'
|
|
115
|
+
else
|
|
116
|
+
'metric'
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Fetch current weather conditions for a city
|
|
121
|
+
#
|
|
122
|
+
# @param api [OpenWeatherMap::API] API client
|
|
123
|
+
# @param city [String] City name with optional country code
|
|
124
|
+
#
|
|
125
|
+
# @return [Hash] Current weather data
|
|
126
|
+
def fetch_current_weather(api, city)
|
|
127
|
+
@logger.debug("Fetching current weather for #{city}")
|
|
128
|
+
|
|
129
|
+
data = api.current(city)
|
|
130
|
+
@logger.debug("Current weather data received for #{city}")
|
|
131
|
+
|
|
132
|
+
conditions = data.weather_conditions
|
|
133
|
+
wind = conditions.wind || {}
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
temperature: conditions.temperature,
|
|
137
|
+
feels_like: conditions.temperature, # API doesn't provide feels_like separately
|
|
138
|
+
description: conditions.description,
|
|
139
|
+
humidity: conditions.humidity,
|
|
140
|
+
pressure: conditions.pressure,
|
|
141
|
+
wind_speed: wind[:speed] || wind['speed'] || 0,
|
|
142
|
+
wind_direction: wind[:direction] || wind['direction'] || 0,
|
|
143
|
+
cloudiness: conditions.clouds,
|
|
144
|
+
visibility: 0 # Not provided by this gem
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Fetch 3-day weather forecast for a city
|
|
149
|
+
#
|
|
150
|
+
# @param api [OpenWeatherMap::API] API client
|
|
151
|
+
# @param city [String] City name with optional country code
|
|
152
|
+
#
|
|
153
|
+
# @return [Array<Hash>] Array of forecast data for next 3 days
|
|
154
|
+
def fetch_forecast(api, city)
|
|
155
|
+
@logger.debug("Fetching 3-day forecast for #{city}")
|
|
156
|
+
|
|
157
|
+
data = api.forecast(city)
|
|
158
|
+
@logger.debug("Forecast data received for #{city}")
|
|
159
|
+
|
|
160
|
+
# Group forecasts by date and extract daily summaries
|
|
161
|
+
forecasts_by_date = {}
|
|
162
|
+
|
|
163
|
+
data.forecast.each do |forecast|
|
|
164
|
+
date = forecast.time.strftime('%Y-%m-%d')
|
|
165
|
+
|
|
166
|
+
forecasts_by_date[date] ||= {
|
|
167
|
+
date: date,
|
|
168
|
+
temperatures: [],
|
|
169
|
+
conditions: [],
|
|
170
|
+
humidity: [],
|
|
171
|
+
wind_speeds: []
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
wind = forecast.wind || {}
|
|
175
|
+
forecasts_by_date[date][:temperatures] << forecast.temperature
|
|
176
|
+
forecasts_by_date[date][:conditions] << forecast.description
|
|
177
|
+
forecasts_by_date[date][:humidity] << forecast.humidity
|
|
178
|
+
forecasts_by_date[date][:wind_speeds] << (wind[:speed] || wind['speed'] || 0)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Calculate daily summaries
|
|
182
|
+
forecasts_by_date.values.map do |day|
|
|
183
|
+
temps = day[:temperatures]
|
|
184
|
+
{
|
|
185
|
+
date: day[:date],
|
|
186
|
+
temp_min: temps.min.round(1),
|
|
187
|
+
temp_max: temps.max.round(1),
|
|
188
|
+
temp_avg: (temps.sum / temps.size).round(1),
|
|
189
|
+
conditions: day[:conditions].max_by { |c| day[:conditions].count(c) },
|
|
190
|
+
avg_humidity: (day[:humidity].sum / day[:humidity].size).round(0),
|
|
191
|
+
avg_wind_speed: (day[:wind_speeds].sum / day[:wind_speeds].size).round(1)
|
|
192
|
+
}
|
|
193
|
+
end.take(3) # Return only 3 days
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|