language-operator 0.0.1 → 0.1.31
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/.rubocop.yml +125 -0
- data/CHANGELOG.md +88 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +229 -21
- data/Makefile +82 -0
- data/README.md +3 -11
- data/Rakefile +63 -0
- data/bin/aictl +7 -0
- data/completions/_aictl +232 -0
- data/completions/aictl.bash +121 -0
- data/completions/aictl.fish +114 -0
- data/docs/architecture/agent-runtime.md +585 -0
- data/docs/dsl/SCHEMA_VERSION.md +250 -0
- data/docs/dsl/agent-reference.md +604 -0
- data/docs/dsl/best-practices.md +1078 -0
- data/docs/dsl/chat-endpoints.md +895 -0
- data/docs/dsl/constraints.md +671 -0
- data/docs/dsl/mcp-integration.md +1177 -0
- data/docs/dsl/webhooks.md +932 -0
- data/docs/dsl/workflows.md +744 -0
- data/lib/language_operator/agent/base.rb +110 -0
- data/lib/language_operator/agent/executor.rb +440 -0
- data/lib/language_operator/agent/instrumentation.rb +54 -0
- data/lib/language_operator/agent/metrics_tracker.rb +183 -0
- data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
- data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
- data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
- data/lib/language_operator/agent/safety/content_filter.rb +93 -0
- data/lib/language_operator/agent/safety/manager.rb +207 -0
- data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
- data/lib/language_operator/agent/safety/safe_executor.rb +127 -0
- data/lib/language_operator/agent/scheduler.rb +183 -0
- data/lib/language_operator/agent/telemetry.rb +116 -0
- data/lib/language_operator/agent/web_server.rb +610 -0
- data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
- data/lib/language_operator/agent.rb +149 -0
- data/lib/language_operator/cli/commands/agent.rb +1205 -0
- data/lib/language_operator/cli/commands/cluster.rb +371 -0
- data/lib/language_operator/cli/commands/install.rb +404 -0
- data/lib/language_operator/cli/commands/model.rb +266 -0
- data/lib/language_operator/cli/commands/persona.rb +393 -0
- data/lib/language_operator/cli/commands/quickstart.rb +22 -0
- data/lib/language_operator/cli/commands/status.rb +143 -0
- data/lib/language_operator/cli/commands/system.rb +772 -0
- data/lib/language_operator/cli/commands/tool.rb +537 -0
- data/lib/language_operator/cli/commands/use.rb +47 -0
- data/lib/language_operator/cli/errors/handler.rb +180 -0
- data/lib/language_operator/cli/errors/suggestions.rb +176 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +77 -0
- data/lib/language_operator/cli/formatters/log_formatter.rb +288 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +49 -0
- data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +163 -0
- data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
- data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
- data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
- data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
- data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
- data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
- data/lib/language_operator/cli/main.rb +236 -0
- data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
- data/lib/language_operator/client/base.rb +214 -0
- data/lib/language_operator/client/config.rb +136 -0
- data/lib/language_operator/client/cost_calculator.rb +37 -0
- data/lib/language_operator/client/mcp_connector.rb +123 -0
- data/lib/language_operator/client.rb +19 -0
- data/lib/language_operator/config/cluster_config.rb +101 -0
- data/lib/language_operator/config/tool_patterns.yaml +57 -0
- data/lib/language_operator/config/tool_registry.rb +96 -0
- data/lib/language_operator/config.rb +138 -0
- data/lib/language_operator/dsl/adapter.rb +124 -0
- data/lib/language_operator/dsl/agent_context.rb +90 -0
- data/lib/language_operator/dsl/agent_definition.rb +427 -0
- data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
- data/lib/language_operator/dsl/config.rb +119 -0
- data/lib/language_operator/dsl/context.rb +50 -0
- data/lib/language_operator/dsl/execution_context.rb +47 -0
- data/lib/language_operator/dsl/helpers.rb +109 -0
- data/lib/language_operator/dsl/http.rb +184 -0
- data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
- data/lib/language_operator/dsl/parameter_definition.rb +124 -0
- data/lib/language_operator/dsl/registry.rb +36 -0
- data/lib/language_operator/dsl/schema.rb +1102 -0
- data/lib/language_operator/dsl/shell.rb +125 -0
- data/lib/language_operator/dsl/tool_definition.rb +112 -0
- data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
- data/lib/language_operator/dsl/webhook_definition.rb +106 -0
- data/lib/language_operator/dsl/workflow_definition.rb +259 -0
- data/lib/language_operator/dsl.rb +161 -0
- data/lib/language_operator/errors.rb +60 -0
- data/lib/language_operator/kubernetes/client.rb +279 -0
- data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
- data/lib/language_operator/loggable.rb +47 -0
- data/lib/language_operator/logger.rb +141 -0
- data/lib/language_operator/retry.rb +123 -0
- data/lib/language_operator/retryable.rb +132 -0
- data/lib/language_operator/templates/README.md +23 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -0
- data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
- data/lib/language_operator/templates/schema/.gitkeep +0 -0
- data/lib/language_operator/templates/schema/CHANGELOG.md +93 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -0
- data/lib/language_operator/tool_loader.rb +242 -0
- data/lib/language_operator/validators.rb +170 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +65 -3
- data/requirements/tasks/challenge.md +9 -0
- data/requirements/tasks/iterate.md +36 -0
- data/requirements/tasks/optimize.md +21 -0
- data/requirements/tasks/tag.md +5 -0
- data/test_agent_dsl.rb +108 -0
- metadata +507 -20
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module Agent
|
|
5
|
+
class MetricsTracker
|
|
6
|
+
attr_reader :metrics
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@metrics = {
|
|
11
|
+
total_input_tokens: 0,
|
|
12
|
+
total_output_tokens: 0,
|
|
13
|
+
total_cached_tokens: 0,
|
|
14
|
+
total_cache_creation_tokens: 0,
|
|
15
|
+
request_count: 0,
|
|
16
|
+
total_cost: 0.0,
|
|
17
|
+
requests: []
|
|
18
|
+
}
|
|
19
|
+
@pricing_cache = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Record token usage from an LLM response
|
|
23
|
+
# @param response [Object] RubyLLM response object
|
|
24
|
+
# @param model_id [String] Model identifier
|
|
25
|
+
def record_request(response, model_id)
|
|
26
|
+
return unless response
|
|
27
|
+
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
# Extract token counts with defensive checks
|
|
30
|
+
input_tokens = extract_token_count(response, :input_tokens)
|
|
31
|
+
output_tokens = extract_token_count(response, :output_tokens)
|
|
32
|
+
cached_tokens = extract_token_count(response, :cached_tokens)
|
|
33
|
+
cache_creation_tokens = extract_token_count(response, :cache_creation_tokens)
|
|
34
|
+
|
|
35
|
+
# Calculate cost for this request
|
|
36
|
+
cost = calculate_cost(input_tokens, output_tokens, model_id)
|
|
37
|
+
|
|
38
|
+
# Update cumulative metrics
|
|
39
|
+
@metrics[:total_input_tokens] += input_tokens
|
|
40
|
+
@metrics[:total_output_tokens] += output_tokens
|
|
41
|
+
@metrics[:total_cached_tokens] += cached_tokens
|
|
42
|
+
@metrics[:total_cache_creation_tokens] += cache_creation_tokens
|
|
43
|
+
@metrics[:request_count] += 1
|
|
44
|
+
@metrics[:total_cost] += cost
|
|
45
|
+
|
|
46
|
+
# Store per-request history (limited to last 100 requests)
|
|
47
|
+
@metrics[:requests] << {
|
|
48
|
+
timestamp: Time.now.iso8601,
|
|
49
|
+
model: model_id,
|
|
50
|
+
input_tokens: input_tokens,
|
|
51
|
+
output_tokens: output_tokens,
|
|
52
|
+
cached_tokens: cached_tokens,
|
|
53
|
+
cache_creation_tokens: cache_creation_tokens,
|
|
54
|
+
cost: cost.round(6)
|
|
55
|
+
}
|
|
56
|
+
@metrics[:requests].shift if @metrics[:requests].size > 100
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get cumulative statistics
|
|
61
|
+
# @return [Hash] Hash with totalTokens, estimatedCost, requestCount
|
|
62
|
+
def cumulative_stats
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
{
|
|
65
|
+
totalTokens: @metrics[:total_input_tokens] + @metrics[:total_output_tokens],
|
|
66
|
+
inputTokens: @metrics[:total_input_tokens],
|
|
67
|
+
outputTokens: @metrics[:total_output_tokens],
|
|
68
|
+
cachedTokens: @metrics[:total_cached_tokens],
|
|
69
|
+
cacheCreationTokens: @metrics[:total_cache_creation_tokens],
|
|
70
|
+
requestCount: @metrics[:request_count],
|
|
71
|
+
estimatedCost: @metrics[:total_cost].round(6)
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get recent request history
|
|
77
|
+
# @param limit [Integer] Number of recent requests to return
|
|
78
|
+
# @return [Array<Hash>] Array of request details
|
|
79
|
+
def recent_requests(limit = 10)
|
|
80
|
+
@mutex.synchronize do
|
|
81
|
+
@metrics[:requests].last(limit)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Reset all metrics (for testing)
|
|
86
|
+
def reset!
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
@metrics = {
|
|
89
|
+
total_input_tokens: 0,
|
|
90
|
+
total_output_tokens: 0,
|
|
91
|
+
total_cached_tokens: 0,
|
|
92
|
+
total_cache_creation_tokens: 0,
|
|
93
|
+
request_count: 0,
|
|
94
|
+
total_cost: 0.0,
|
|
95
|
+
requests: []
|
|
96
|
+
}
|
|
97
|
+
@pricing_cache = {}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Extract token count from response with defensive checks
|
|
104
|
+
# @param response [Object] Response object
|
|
105
|
+
# @param method [Symbol] Method name to call
|
|
106
|
+
# @return [Integer] Token count or 0 if unavailable
|
|
107
|
+
def extract_token_count(response, method)
|
|
108
|
+
return 0 unless response.respond_to?(method)
|
|
109
|
+
|
|
110
|
+
value = response.public_send(method)
|
|
111
|
+
value.is_a?(Integer) ? value : 0
|
|
112
|
+
rescue StandardError
|
|
113
|
+
0
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Calculate cost based on token usage and model pricing
|
|
117
|
+
# @param input_tokens [Integer] Number of input tokens
|
|
118
|
+
# @param output_tokens [Integer] Number of output tokens
|
|
119
|
+
# @param model_id [String] Model identifier
|
|
120
|
+
# @return [Float] Estimated cost in USD
|
|
121
|
+
def calculate_cost(input_tokens, output_tokens, model_id)
|
|
122
|
+
pricing = get_pricing(model_id)
|
|
123
|
+
return 0.0 unless pricing
|
|
124
|
+
|
|
125
|
+
input_cost = (input_tokens / 1_000_000.0) * pricing[:input]
|
|
126
|
+
output_cost = (output_tokens / 1_000_000.0) * pricing[:output]
|
|
127
|
+
input_cost + output_cost
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
LanguageOperator.logger.warn('Cost calculation failed',
|
|
130
|
+
model: model_id,
|
|
131
|
+
error: e.message)
|
|
132
|
+
0.0
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get pricing for a model (with caching)
|
|
136
|
+
# @param model_id [String] Model identifier
|
|
137
|
+
# @return [Hash, nil] Hash with :input and :output prices per million tokens
|
|
138
|
+
def get_pricing(model_id)
|
|
139
|
+
# Return cached pricing if available
|
|
140
|
+
return @pricing_cache[model_id] if @pricing_cache.key?(model_id)
|
|
141
|
+
|
|
142
|
+
# Try to fetch from RubyLLM registry
|
|
143
|
+
pricing = fetch_ruby_llm_pricing(model_id)
|
|
144
|
+
|
|
145
|
+
# Cache and return
|
|
146
|
+
@pricing_cache[model_id] = pricing
|
|
147
|
+
pricing
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
LanguageOperator.logger.warn('Pricing lookup failed',
|
|
150
|
+
model: model_id,
|
|
151
|
+
error: e.message)
|
|
152
|
+
# Cache nil to avoid repeated failures
|
|
153
|
+
@pricing_cache[model_id] = nil
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Fetch pricing from RubyLLM registry
|
|
158
|
+
# @param model_id [String] Model identifier
|
|
159
|
+
# @return [Hash, nil] Pricing hash or nil
|
|
160
|
+
def fetch_ruby_llm_pricing(model_id)
|
|
161
|
+
# Check if RubyLLM is available
|
|
162
|
+
return nil unless defined?(RubyLLM)
|
|
163
|
+
|
|
164
|
+
# Try to find model in registry
|
|
165
|
+
model_info = RubyLLM.models.find(model_id)
|
|
166
|
+
return nil unless model_info
|
|
167
|
+
|
|
168
|
+
# Extract pricing (assuming RubyLLM provides these attributes)
|
|
169
|
+
if model_info.respond_to?(:input_price_per_million) &&
|
|
170
|
+
model_info.respond_to?(:output_price_per_million)
|
|
171
|
+
{
|
|
172
|
+
input: model_info.input_price_per_million,
|
|
173
|
+
output: model_info.output_price_per_million
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
rescue StandardError
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parser/current'
|
|
4
|
+
|
|
5
|
+
module LanguageOperator
|
|
6
|
+
module Agent
|
|
7
|
+
module Safety
|
|
8
|
+
# Validates synthesized Ruby code for security before execution
|
|
9
|
+
# Performs static analysis to detect dangerous method calls
|
|
10
|
+
class ASTValidator
|
|
11
|
+
# Gems that are safe to require (allowlist)
|
|
12
|
+
# These are required for agent execution and are safe
|
|
13
|
+
ALLOWED_REQUIRES = %w[
|
|
14
|
+
language_operator
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
# Dangerous methods that should never be called in synthesized code
|
|
18
|
+
DANGEROUS_METHODS = %w[
|
|
19
|
+
system exec spawn fork ` eval instance_eval class_eval module_eval
|
|
20
|
+
require load autoload require_relative
|
|
21
|
+
send __send__ public_send method __method__
|
|
22
|
+
const_set const_get remove_const
|
|
23
|
+
define_method define_singleton_method
|
|
24
|
+
undef_method remove_method alias_method
|
|
25
|
+
exit exit! abort raise fail throw
|
|
26
|
+
trap at_exit
|
|
27
|
+
open
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
# Dangerous constants that should not be accessed
|
|
31
|
+
DANGEROUS_CONSTANTS = %w[
|
|
32
|
+
File Dir IO FileUtils Pathname
|
|
33
|
+
Process Kernel ObjectSpace GC
|
|
34
|
+
Thread Fiber Mutex ConditionVariable
|
|
35
|
+
Socket TCPSocket UDPSocket TCPServer UDPServer
|
|
36
|
+
STDIN STDOUT STDERR
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# Safe DSL methods that are allowed in agent definitions
|
|
40
|
+
SAFE_AGENT_METHODS = %w[
|
|
41
|
+
agent description persona schedule objectives objective
|
|
42
|
+
workflow step tool params depends_on prompt
|
|
43
|
+
constraints budget max_requests rate_limit content_filter
|
|
44
|
+
output mode webhook as_mcp_server as_chat_endpoint
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
# Safe DSL methods for tool definitions
|
|
48
|
+
SAFE_TOOL_METHODS = %w[
|
|
49
|
+
tool description parameter type required default
|
|
50
|
+
execute
|
|
51
|
+
].freeze
|
|
52
|
+
|
|
53
|
+
# Safe helper methods available in execute blocks
|
|
54
|
+
SAFE_HELPER_METHODS = %w[
|
|
55
|
+
HTTP Shell
|
|
56
|
+
validate_url validate_phone validate_email
|
|
57
|
+
env_required env_get
|
|
58
|
+
truncate parse_csv
|
|
59
|
+
error success
|
|
60
|
+
].freeze
|
|
61
|
+
|
|
62
|
+
# Safe Ruby built-in methods and classes
|
|
63
|
+
SAFE_BUILTINS = %w[
|
|
64
|
+
String Array Hash Integer Float Symbol
|
|
65
|
+
puts print p pp warn
|
|
66
|
+
true false nil
|
|
67
|
+
if unless case when then else elsif end
|
|
68
|
+
while until for break next redo retry return
|
|
69
|
+
begin rescue ensure
|
|
70
|
+
lambda proc block_given? yield
|
|
71
|
+
attr_reader attr_writer attr_accessor
|
|
72
|
+
private protected public
|
|
73
|
+
initialize new
|
|
74
|
+
].freeze
|
|
75
|
+
|
|
76
|
+
class SecurityError < StandardError; end
|
|
77
|
+
|
|
78
|
+
def initialize
|
|
79
|
+
@parser = Parser::CurrentRuby.new
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Validate code and raise SecurityError if dangerous methods found
|
|
83
|
+
# @param code [String] Ruby code to validate
|
|
84
|
+
# @param file_path [String] Path to file (for error messages)
|
|
85
|
+
# @raise [SecurityError] if code contains dangerous methods
|
|
86
|
+
def validate!(code, file_path = '(eval)')
|
|
87
|
+
ast = parse_code(code, file_path)
|
|
88
|
+
return if ast.nil? # Empty code is safe
|
|
89
|
+
|
|
90
|
+
violations = scan_ast(ast)
|
|
91
|
+
|
|
92
|
+
return if violations.empty?
|
|
93
|
+
|
|
94
|
+
raise SecurityError, format_violations(violations, file_path)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Validate code and return array of violations (non-raising version)
|
|
98
|
+
# @param code [String] Ruby code to validate
|
|
99
|
+
# @param file_path [String] Path to file (for error messages)
|
|
100
|
+
# @return [Array<Hash>] Array of violation hashes
|
|
101
|
+
def validate(code, file_path = '(eval)')
|
|
102
|
+
begin
|
|
103
|
+
ast = parse_code(code, file_path)
|
|
104
|
+
rescue SecurityError => e
|
|
105
|
+
# Convert SecurityError (which wraps Parser::SyntaxError) to violation
|
|
106
|
+
return [{ type: :syntax_error, message: e.message }]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
return [] if ast.nil?
|
|
110
|
+
|
|
111
|
+
scan_ast(ast)
|
|
112
|
+
rescue Parser::SyntaxError => e
|
|
113
|
+
[{ type: :syntax_error, message: e.message }]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def parse_code(code, file_path)
|
|
119
|
+
buffer = Parser::Source::Buffer.new(file_path)
|
|
120
|
+
buffer.source = code
|
|
121
|
+
@parser.parse(buffer)
|
|
122
|
+
rescue Parser::SyntaxError => e
|
|
123
|
+
raise SecurityError, "Syntax error in #{file_path}: #{e.message}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def scan_ast(node, violations = [])
|
|
127
|
+
return violations if node.nil?
|
|
128
|
+
|
|
129
|
+
case node.type
|
|
130
|
+
when :send
|
|
131
|
+
check_method_call(node, violations)
|
|
132
|
+
when :const
|
|
133
|
+
check_constant(node, violations)
|
|
134
|
+
when :gvar
|
|
135
|
+
check_global_variable(node, violations)
|
|
136
|
+
when :xstr
|
|
137
|
+
# Backtick string execution (e.g., `command`)
|
|
138
|
+
violations << {
|
|
139
|
+
type: :backtick_execution,
|
|
140
|
+
location: node.location.line,
|
|
141
|
+
message: 'Backtick command execution is not allowed'
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Recursively scan all child nodes
|
|
146
|
+
node.children.each do |child|
|
|
147
|
+
scan_ast(child, violations) if child.is_a?(Parser::AST::Node)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
violations
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def check_method_call(node, violations)
|
|
154
|
+
receiver, method_name, *args = node.children
|
|
155
|
+
|
|
156
|
+
method_str = method_name.to_s
|
|
157
|
+
|
|
158
|
+
# Special handling for require - check if it's in the allowlist
|
|
159
|
+
if %w[require require_relative].include?(method_str)
|
|
160
|
+
required_gem = extract_require_argument(args)
|
|
161
|
+
|
|
162
|
+
# Allow if in the allowlist
|
|
163
|
+
return if required_gem && ALLOWED_REQUIRES.include?(required_gem)
|
|
164
|
+
|
|
165
|
+
# Otherwise, add violation
|
|
166
|
+
violations << {
|
|
167
|
+
type: :dangerous_method,
|
|
168
|
+
method: method_str,
|
|
169
|
+
location: node.location.line,
|
|
170
|
+
message: "Dangerous method '#{method_str}' is not allowed"
|
|
171
|
+
}
|
|
172
|
+
return
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Check for other dangerous methods
|
|
176
|
+
if DANGEROUS_METHODS.include?(method_str)
|
|
177
|
+
violations << {
|
|
178
|
+
type: :dangerous_method,
|
|
179
|
+
method: method_str,
|
|
180
|
+
location: node.location.line,
|
|
181
|
+
message: "Dangerous method '#{method_str}' is not allowed"
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Check for File/Dir/IO operations
|
|
186
|
+
if receiver && receiver.type == :const
|
|
187
|
+
const_name = receiver.children[1].to_s
|
|
188
|
+
if DANGEROUS_CONSTANTS.include?(const_name)
|
|
189
|
+
violations << {
|
|
190
|
+
type: :dangerous_constant,
|
|
191
|
+
constant: const_name,
|
|
192
|
+
method: method_str,
|
|
193
|
+
location: node.location.line,
|
|
194
|
+
message: "Access to #{const_name}.#{method_str} is not allowed"
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Check for backtick execution (e.g., `command`)
|
|
200
|
+
# Note: backticks are represented as send with method name :`
|
|
201
|
+
return unless method_str == '`'
|
|
202
|
+
|
|
203
|
+
violations << {
|
|
204
|
+
type: :backtick_execution,
|
|
205
|
+
location: node.location.line,
|
|
206
|
+
message: 'Backtick command execution is not allowed'
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def check_constant(node, violations)
|
|
211
|
+
_, const_name = node.children
|
|
212
|
+
const_str = const_name.to_s
|
|
213
|
+
|
|
214
|
+
# Check for dangerous constants being accessed directly
|
|
215
|
+
return unless DANGEROUS_CONSTANTS.include?(const_str)
|
|
216
|
+
|
|
217
|
+
violations << {
|
|
218
|
+
type: :dangerous_constant_access,
|
|
219
|
+
constant: const_str,
|
|
220
|
+
location: node.location.line,
|
|
221
|
+
message: "Direct access to #{const_str} constant is not allowed"
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def check_global_variable(node, violations)
|
|
226
|
+
var_name = node.children[0].to_s
|
|
227
|
+
|
|
228
|
+
# Block access to dangerous global variables
|
|
229
|
+
dangerous_globals = %w[$0 $PROGRAM_NAME $LOAD_PATH $: $LOADED_FEATURES $"]
|
|
230
|
+
|
|
231
|
+
return unless dangerous_globals.include?(var_name)
|
|
232
|
+
|
|
233
|
+
violations << {
|
|
234
|
+
type: :dangerous_global,
|
|
235
|
+
variable: var_name,
|
|
236
|
+
location: node.location.line,
|
|
237
|
+
message: "Access to global variable #{var_name} is not allowed"
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def extract_require_argument(args)
|
|
242
|
+
# args is an array of AST nodes representing the arguments to require
|
|
243
|
+
# We're looking for a string literal like 'language_operator' or "language_operator"
|
|
244
|
+
return nil if args.empty?
|
|
245
|
+
|
|
246
|
+
arg_node = args.first
|
|
247
|
+
return nil unless arg_node
|
|
248
|
+
|
|
249
|
+
# Check if it's a string literal (:str node)
|
|
250
|
+
return arg_node.children[0] if arg_node.type == :str
|
|
251
|
+
|
|
252
|
+
# If it's not a string literal (e.g., dynamic require), we can't verify it
|
|
253
|
+
nil
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def format_violations(violations, file_path)
|
|
257
|
+
header = "Security violations detected in #{file_path}:\n\n"
|
|
258
|
+
|
|
259
|
+
violation_messages = violations.map do |v|
|
|
260
|
+
" Line #{v[:location]}: #{v[:message]}"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
footer = "\n\nSynthesized code must only use safe DSL methods and approved helpers."
|
|
264
|
+
footer += "\nSafe methods include: #{SAFE_AGENT_METHODS.join(', ')}, #{SAFE_TOOL_METHODS.join(', ')}"
|
|
265
|
+
footer += "\nSafe helpers include: HTTP.*, Shell.run, validate_*, env_*"
|
|
266
|
+
|
|
267
|
+
header + violation_messages.join("\n") + footer
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative '../../logger'
|
|
5
|
+
require_relative '../../loggable'
|
|
6
|
+
|
|
7
|
+
module LanguageOperator
|
|
8
|
+
module Agent
|
|
9
|
+
module Safety
|
|
10
|
+
# Audit Logger for safety events
|
|
11
|
+
#
|
|
12
|
+
# Logs all safety-related events (blocked requests, budget exceeded, etc.)
|
|
13
|
+
# for compliance and debugging.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# audit = AuditLogger.new
|
|
17
|
+
# audit.log_blocked_request(reason: 'Budget exceeded', details: {...})
|
|
18
|
+
class AuditLogger
|
|
19
|
+
include LanguageOperator::Loggable
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@audit_log_path = ENV['AUDIT_LOG_PATH'] || '/tmp/langop-audit.jsonl'
|
|
23
|
+
logger.info('Audit logger initialized', log_path: @audit_log_path)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Log a blocked request
|
|
27
|
+
#
|
|
28
|
+
# @param reason [String] Reason for blocking
|
|
29
|
+
# @param details [Hash] Additional details
|
|
30
|
+
def log_blocked_request(reason:, details: {})
|
|
31
|
+
log_event(
|
|
32
|
+
event_type: 'blocked_request',
|
|
33
|
+
reason: reason,
|
|
34
|
+
details: details
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Log a budget event
|
|
39
|
+
#
|
|
40
|
+
# @param event [String] Event description
|
|
41
|
+
# @param details [Hash] Budget details
|
|
42
|
+
def log_budget_event(event:, details: {})
|
|
43
|
+
log_event(
|
|
44
|
+
event_type: 'budget_event',
|
|
45
|
+
event: event,
|
|
46
|
+
details: details
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Log a rate limit event
|
|
51
|
+
#
|
|
52
|
+
# @param event [String] Event description
|
|
53
|
+
# @param details [Hash] Rate limit details
|
|
54
|
+
def log_rate_limit_event(event:, details: {})
|
|
55
|
+
log_event(
|
|
56
|
+
event_type: 'rate_limit_event',
|
|
57
|
+
event: event,
|
|
58
|
+
details: details
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Log a content filter event
|
|
63
|
+
#
|
|
64
|
+
# @param event [String] Event description
|
|
65
|
+
# @param details [Hash] Filter details
|
|
66
|
+
def log_content_filter_event(event:, details: {})
|
|
67
|
+
log_event(
|
|
68
|
+
event_type: 'content_filter_event',
|
|
69
|
+
event: event,
|
|
70
|
+
details: details
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def logger_component
|
|
77
|
+
'Safety::AuditLogger'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def log_event(event_data)
|
|
81
|
+
event = {
|
|
82
|
+
timestamp: Time.now.utc.iso8601,
|
|
83
|
+
agent_name: ENV['AGENT_NAME'] || 'unknown',
|
|
84
|
+
**event_data
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Log to standard logger
|
|
88
|
+
logger.info('Audit event', **event_data)
|
|
89
|
+
|
|
90
|
+
# Append to audit log file
|
|
91
|
+
begin
|
|
92
|
+
File.open(@audit_log_path, 'a') do |f|
|
|
93
|
+
f.puts(JSON.generate(event))
|
|
94
|
+
end
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
logger.error('Failed to write audit log',
|
|
97
|
+
error: e.message,
|
|
98
|
+
log_path: @audit_log_path)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|