language-operator 0.1.30 → 0.1.35
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 +7 -8
- data/CHANGELOG.md +49 -0
- data/CI_STATUS.md +56 -0
- data/Gemfile.lock +2 -2
- data/Makefile +28 -7
- data/Rakefile +29 -0
- data/docs/dsl/SCHEMA_VERSION.md +250 -0
- data/docs/dsl/agent-reference.md +13 -0
- data/lib/language_operator/agent/base.rb +10 -6
- data/lib/language_operator/agent/executor.rb +19 -97
- data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
- data/lib/language_operator/agent/safety/safe_executor.rb +39 -2
- data/lib/language_operator/agent/scheduler.rb +60 -0
- data/lib/language_operator/agent/task_executor.rb +548 -0
- data/lib/language_operator/agent.rb +90 -27
- data/lib/language_operator/cli/base_command.rb +117 -0
- data/lib/language_operator/cli/commands/agent.rb +351 -466
- data/lib/language_operator/cli/commands/cluster.rb +276 -256
- data/lib/language_operator/cli/commands/install.rb +110 -119
- data/lib/language_operator/cli/commands/model.rb +284 -184
- data/lib/language_operator/cli/commands/persona.rb +220 -289
- data/lib/language_operator/cli/commands/quickstart.rb +4 -5
- data/lib/language_operator/cli/commands/status.rb +36 -53
- data/lib/language_operator/cli/commands/system.rb +760 -0
- data/lib/language_operator/cli/commands/tool.rb +356 -422
- data/lib/language_operator/cli/commands/use.rb +19 -22
- data/lib/language_operator/cli/formatters/code_formatter.rb +3 -7
- data/lib/language_operator/cli/formatters/log_formatter.rb +3 -5
- data/lib/language_operator/cli/formatters/progress_formatter.rb +3 -7
- data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +10 -26
- data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
- data/lib/language_operator/cli/main.rb +4 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
- data/lib/language_operator/client/config.rb +20 -21
- data/lib/language_operator/config.rb +115 -3
- data/lib/language_operator/constants.rb +54 -0
- data/lib/language_operator/dsl/agent_context.rb +7 -7
- data/lib/language_operator/dsl/agent_definition.rb +111 -26
- data/lib/language_operator/dsl/config.rb +30 -66
- data/lib/language_operator/dsl/main_definition.rb +114 -0
- data/lib/language_operator/dsl/schema.rb +1143 -0
- data/lib/language_operator/dsl/task_definition.rb +315 -0
- data/lib/language_operator/dsl.rb +1 -1
- data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
- data/lib/language_operator/logger.rb +4 -4
- data/lib/language_operator/synthesis_test_harness.rb +324 -0
- data/lib/language_operator/templates/README.md +23 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +133 -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 +119 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +494 -0
- data/lib/language_operator/type_coercion.rb +250 -0
- data/lib/language_operator/ux/base.rb +81 -0
- data/lib/language_operator/ux/concerns/README.md +155 -0
- data/lib/language_operator/ux/concerns/headings.rb +90 -0
- data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
- data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
- data/lib/language_operator/ux/create_agent.rb +252 -0
- data/lib/language_operator/ux/create_model.rb +267 -0
- data/lib/language_operator/ux/quickstart.rb +594 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +2 -0
- data/requirements/ARCHITECTURE.md +1 -0
- data/requirements/SCRATCH.md +153 -0
- data/requirements/dsl.md +0 -0
- data/requirements/features +1 -0
- data/requirements/personas +1 -0
- data/requirements/proposals +1 -0
- data/requirements/tasks/iterate.md +14 -15
- data/requirements/tasks/optimize.md +13 -4
- data/synth/001/Makefile +90 -0
- data/synth/001/agent.rb +26 -0
- data/synth/001/agent.yaml +7 -0
- data/synth/001/output.log +44 -0
- data/synth/Makefile +39 -0
- data/synth/README.md +342 -0
- metadata +49 -18
- data/examples/README.md +0 -569
- data/examples/agent_example.rb +0 -86
- data/examples/chat_endpoint_agent.rb +0 -118
- data/examples/github_webhook_agent.rb +0 -171
- data/examples/mcp_agent.rb +0 -158
- data/examples/oauth_callback_agent.rb +0 -296
- data/examples/stripe_webhook_agent.rb +0 -219
- data/examples/webhook_agent.rb +0 -80
- data/lib/language_operator/dsl/workflow_definition.rb +0 -259
- data/test_agent_dsl.rb +0 -108
|
@@ -56,21 +56,17 @@ module LanguageOperator
|
|
|
56
56
|
execute(enriched_instruction)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# Execute a single task
|
|
59
|
+
# Execute a single task
|
|
60
60
|
#
|
|
61
61
|
# @param task [String] The task to execute
|
|
62
|
-
# @param agent_definition [LanguageOperator::Dsl::AgentDefinition, nil] Optional agent definition
|
|
62
|
+
# @param agent_definition [LanguageOperator::Dsl::AgentDefinition, nil] Optional agent definition (unused in DSL v1)
|
|
63
63
|
# @return [String] The result
|
|
64
|
-
# rubocop:disable Metrics/BlockLength
|
|
65
64
|
def execute(task, agent_definition: nil)
|
|
66
65
|
with_span('agent.execute_goal', attributes: {
|
|
67
66
|
'agent.goal_description' => task[0...500]
|
|
68
67
|
}) do
|
|
69
68
|
@iteration_count += 1
|
|
70
69
|
|
|
71
|
-
# Route to workflow execution if agent has a workflow defined
|
|
72
|
-
return execute_workflow(agent_definition) if agent_definition&.workflow
|
|
73
|
-
|
|
74
70
|
# Standard instruction-based execution
|
|
75
71
|
logger.info('Starting iteration',
|
|
76
72
|
iteration: @iteration_count,
|
|
@@ -90,7 +86,7 @@ module LanguageOperator
|
|
|
90
86
|
)
|
|
91
87
|
end
|
|
92
88
|
|
|
93
|
-
logger.info('
|
|
89
|
+
logger.info('LLM request')
|
|
94
90
|
result = logger.timed('LLM response received') do
|
|
95
91
|
@agent.send_message(task)
|
|
96
92
|
end
|
|
@@ -110,12 +106,14 @@ module LanguageOperator
|
|
|
110
106
|
tokens: metrics[:totalTokens]
|
|
111
107
|
)
|
|
112
108
|
end
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
|
|
110
|
+
# Log the actual LLM response content (strip [THINK] blocks)
|
|
111
|
+
cleaned_response = result_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '').strip
|
|
112
|
+
response_preview = cleaned_response.length > 500 ? "#{cleaned_response[0..500]}..." : cleaned_response
|
|
113
|
+
puts "\e[1;35m·\e[0m #{response_preview}" unless response_preview.empty?
|
|
114
|
+
|
|
115
|
+
# Log iteration completion with green dot
|
|
116
|
+
puts "\e[1;32m·\e[0m Iteration completed (iteration=#{@iteration_count}, response_length=#{result_text.length}, total_tokens=#{metrics[:totalTokens]}, estimated_cost=$#{metrics[:estimatedCost]})"
|
|
119
117
|
|
|
120
118
|
result
|
|
121
119
|
rescue StandardError => e
|
|
@@ -130,7 +128,7 @@ module LanguageOperator
|
|
|
130
128
|
def run_loop
|
|
131
129
|
start_time = Time.now
|
|
132
130
|
|
|
133
|
-
logger.info('
|
|
131
|
+
logger.info('Starting execution')
|
|
134
132
|
logger.info('Configuration',
|
|
135
133
|
workspace: @agent.workspace_path,
|
|
136
134
|
mcp_servers: @agent.servers_info.length,
|
|
@@ -152,7 +150,9 @@ module LanguageOperator
|
|
|
152
150
|
ENV['AGENT_INSTRUCTIONS'] ||
|
|
153
151
|
'Monitor workspace and respond to changes'
|
|
154
152
|
|
|
155
|
-
|
|
153
|
+
# Log instructions with bold white formatting
|
|
154
|
+
instructions_preview = instructions[0..200]
|
|
155
|
+
puts "\e[1;37m·\e[0m \e[1;37m#{instructions_preview}\e[0m"
|
|
156
156
|
logger.info('Starting autonomous execution loop')
|
|
157
157
|
|
|
158
158
|
loop do
|
|
@@ -188,7 +188,7 @@ module LanguageOperator
|
|
|
188
188
|
# Log execution summary
|
|
189
189
|
total_duration = Time.now - start_time
|
|
190
190
|
metrics = @metrics_tracker.cumulative_stats
|
|
191
|
-
logger.info('
|
|
191
|
+
logger.info('Execution complete',
|
|
192
192
|
iterations: @iteration_count,
|
|
193
193
|
duration_s: total_duration.round(2),
|
|
194
194
|
total_requests: metrics[:requestCount],
|
|
@@ -203,84 +203,6 @@ module LanguageOperator
|
|
|
203
203
|
reason: 'Hit max_iterations limit')
|
|
204
204
|
end
|
|
205
205
|
|
|
206
|
-
# Execute a workflow-based agent
|
|
207
|
-
#
|
|
208
|
-
# @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
|
|
209
|
-
# @return [RubyLLM::Message] The final response
|
|
210
|
-
def execute_workflow(agent_def)
|
|
211
|
-
start_time = Time.now
|
|
212
|
-
|
|
213
|
-
logger.info("▶ Starting workflow execution: #{agent_def.name}")
|
|
214
|
-
|
|
215
|
-
# Log persona if defined
|
|
216
|
-
logger.info("👤 Loading persona: #{agent_def.persona}") if agent_def.persona
|
|
217
|
-
|
|
218
|
-
# Build orchestration prompt from agent definition
|
|
219
|
-
prompt = build_workflow_prompt(agent_def)
|
|
220
|
-
logger.debug('Workflow prompt', prompt: prompt[0..300])
|
|
221
|
-
|
|
222
|
-
# Register workflow steps as tools (placeholder - will implement after tool converter)
|
|
223
|
-
# For now, just execute with instructions
|
|
224
|
-
result = logger.timed('🤖 LLM request') do
|
|
225
|
-
@agent.send_message(prompt)
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Record metrics
|
|
229
|
-
model_id = @agent.config.dig('llm', 'model')
|
|
230
|
-
@metrics_tracker.record_request(result, model_id) if model_id
|
|
231
|
-
|
|
232
|
-
# Write output if configured
|
|
233
|
-
write_output(agent_def, result) if agent_def.output_config && result
|
|
234
|
-
|
|
235
|
-
# Log execution summary
|
|
236
|
-
total_duration = Time.now - start_time
|
|
237
|
-
metrics = @metrics_tracker.cumulative_stats
|
|
238
|
-
logger.info('✅ Workflow execution completed',
|
|
239
|
-
duration_s: total_duration.round(2),
|
|
240
|
-
total_tokens: metrics[:totalTokens],
|
|
241
|
-
estimated_cost: "$#{metrics[:estimatedCost]}")
|
|
242
|
-
result
|
|
243
|
-
rescue StandardError => e
|
|
244
|
-
logger.error('❌ Workflow execution failed', error: e.message)
|
|
245
|
-
handle_error(e)
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Build orchestration prompt from agent definition
|
|
249
|
-
#
|
|
250
|
-
# @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
|
|
251
|
-
# @return [String] The prompt
|
|
252
|
-
def build_workflow_prompt(agent_def)
|
|
253
|
-
prompt = "# Task: #{agent_def.description}\n\n"
|
|
254
|
-
|
|
255
|
-
if agent_def.objectives&.any?
|
|
256
|
-
prompt += "## Objectives:\n"
|
|
257
|
-
agent_def.objectives.each { |obj| prompt += "- #{obj}\n" }
|
|
258
|
-
prompt += "\n"
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
if agent_def.workflow&.steps&.any?
|
|
262
|
-
prompt += "## Workflow Steps:\n"
|
|
263
|
-
agent_def.workflow.step_order.each do |step_name|
|
|
264
|
-
step = agent_def.workflow.steps[step_name]
|
|
265
|
-
prompt += step_name.to_s.tr('_', ' ').capitalize.to_s
|
|
266
|
-
prompt += " (using tool: #{step.tool_name})" if step.tool_name
|
|
267
|
-
prompt += " - depends on: #{step.dependencies.join(', ')}" if step.dependencies&.any?
|
|
268
|
-
prompt += "\n"
|
|
269
|
-
end
|
|
270
|
-
prompt += "\n"
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
if agent_def.constraints
|
|
274
|
-
prompt += "## Constraints:\n"
|
|
275
|
-
prompt += "- Maximum iterations: #{agent_def.constraints[:max_iterations]}\n" if agent_def.constraints[:max_iterations]
|
|
276
|
-
prompt += "- Timeout: #{agent_def.constraints[:timeout]}\n" if agent_def.constraints[:timeout]
|
|
277
|
-
prompt += "\n"
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
prompt += 'Please complete this task following the workflow steps.'
|
|
281
|
-
prompt
|
|
282
|
-
end
|
|
283
|
-
|
|
284
206
|
# Write output to configured destinations
|
|
285
207
|
#
|
|
286
208
|
# @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
|
|
@@ -302,10 +224,10 @@ module LanguageOperator
|
|
|
302
224
|
fallback_path = File.join(@agent.workspace_path, 'output.txt')
|
|
303
225
|
begin
|
|
304
226
|
File.write(fallback_path, content)
|
|
305
|
-
logger.warn("
|
|
227
|
+
logger.warn("Could not write to #{workspace_path}, wrote to output.txt instead")
|
|
306
228
|
rescue StandardError => e2
|
|
307
|
-
logger.warn("
|
|
308
|
-
logger.info("
|
|
229
|
+
logger.warn("Could not write output to workspace: #{e2.message}")
|
|
230
|
+
logger.info("Output (first 500 chars): #{content[0..500]}")
|
|
309
231
|
end
|
|
310
232
|
end
|
|
311
233
|
end
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'prism'
|
|
4
4
|
|
|
5
5
|
module LanguageOperator
|
|
6
6
|
module Agent
|
|
7
7
|
module Safety
|
|
8
8
|
# Validates synthesized Ruby code for security before execution
|
|
9
9
|
# Performs static analysis to detect dangerous method calls
|
|
10
|
+
#
|
|
11
|
+
# Supports DSL v1 (task/main model) and validates both neural and symbolic
|
|
12
|
+
# task implementations to ensure they use only safe Ruby subset.
|
|
10
13
|
class ASTValidator
|
|
11
14
|
# Gems that are safe to require (allowlist)
|
|
12
15
|
# These are required for agent execution and are safe
|
|
@@ -36,10 +39,10 @@ module LanguageOperator
|
|
|
36
39
|
STDIN STDOUT STDERR
|
|
37
40
|
].freeze
|
|
38
41
|
|
|
39
|
-
# Safe DSL methods that are allowed in agent definitions
|
|
42
|
+
# Safe DSL methods that are allowed in agent definitions (DSL v1)
|
|
40
43
|
SAFE_AGENT_METHODS = %w[
|
|
41
44
|
agent description persona schedule objectives objective
|
|
42
|
-
|
|
45
|
+
task main execute_task inputs outputs instructions
|
|
43
46
|
constraints budget max_requests rate_limit content_filter
|
|
44
47
|
output mode webhook as_mcp_server as_chat_endpoint
|
|
45
48
|
].freeze
|
|
@@ -57,6 +60,7 @@ module LanguageOperator
|
|
|
57
60
|
env_required env_get
|
|
58
61
|
truncate parse_csv
|
|
59
62
|
error success
|
|
63
|
+
TypeCoercion
|
|
60
64
|
].freeze
|
|
61
65
|
|
|
62
66
|
# Safe Ruby built-in methods and classes
|
|
@@ -76,7 +80,7 @@ module LanguageOperator
|
|
|
76
80
|
class SecurityError < StandardError; end
|
|
77
81
|
|
|
78
82
|
def initialize
|
|
79
|
-
|
|
83
|
+
# Prism doesn't require initialization
|
|
80
84
|
end
|
|
81
85
|
|
|
82
86
|
# Validate code and raise SecurityError if dangerous methods found
|
|
@@ -102,62 +106,69 @@ module LanguageOperator
|
|
|
102
106
|
begin
|
|
103
107
|
ast = parse_code(code, file_path)
|
|
104
108
|
rescue SecurityError => e
|
|
105
|
-
# Convert SecurityError (which wraps
|
|
109
|
+
# Convert SecurityError (which wraps syntax error) to violation
|
|
106
110
|
return [{ type: :syntax_error, message: e.message }]
|
|
107
111
|
end
|
|
108
112
|
|
|
109
113
|
return [] if ast.nil?
|
|
110
114
|
|
|
111
115
|
scan_ast(ast)
|
|
112
|
-
rescue
|
|
116
|
+
rescue Prism::ParseError => e
|
|
113
117
|
[{ type: :syntax_error, message: e.message }]
|
|
114
118
|
end
|
|
115
119
|
|
|
116
120
|
private
|
|
117
121
|
|
|
118
122
|
def parse_code(code, file_path)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
result = Prism.parse(code, filepath: file_path)
|
|
124
|
+
|
|
125
|
+
# Prism is forgiving and creates an AST even with some syntax errors
|
|
126
|
+
# We'll allow parsing to proceed and only raise if there are FATAL errors
|
|
127
|
+
# that prevent AST creation entirely
|
|
128
|
+
if result.value.nil?
|
|
129
|
+
errors = result.errors.map(&:message).join('; ')
|
|
130
|
+
raise SecurityError, "Syntax error in #{file_path}: #{errors}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
result.value
|
|
134
|
+
rescue Prism::ParseError => e
|
|
123
135
|
raise SecurityError, "Syntax error in #{file_path}: #{e.message}"
|
|
124
136
|
end
|
|
125
137
|
|
|
126
138
|
def scan_ast(node, violations = [])
|
|
127
139
|
return violations if node.nil?
|
|
128
140
|
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
# Prism uses different node types
|
|
142
|
+
case node
|
|
143
|
+
when Prism::CallNode
|
|
131
144
|
check_method_call(node, violations)
|
|
132
|
-
when
|
|
145
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
133
146
|
check_constant(node, violations)
|
|
134
|
-
when
|
|
147
|
+
when Prism::GlobalVariableReadNode, Prism::GlobalVariableWriteNode
|
|
135
148
|
check_global_variable(node, violations)
|
|
136
|
-
when
|
|
149
|
+
when Prism::XStringNode
|
|
137
150
|
# Backtick string execution (e.g., `command`)
|
|
138
151
|
violations << {
|
|
139
152
|
type: :backtick_execution,
|
|
140
|
-
location: node.location.
|
|
153
|
+
location: node.location.start_line,
|
|
141
154
|
message: 'Backtick command execution is not allowed'
|
|
142
155
|
}
|
|
143
156
|
end
|
|
144
157
|
|
|
145
158
|
# Recursively scan all child nodes
|
|
146
|
-
node.
|
|
147
|
-
scan_ast(child, violations)
|
|
159
|
+
node.compact_child_nodes.each do |child|
|
|
160
|
+
scan_ast(child, violations)
|
|
148
161
|
end
|
|
149
162
|
|
|
150
163
|
violations
|
|
151
164
|
end
|
|
152
165
|
|
|
153
166
|
def check_method_call(node, violations)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
method_str = method_name.to_s
|
|
167
|
+
method_str = node.name.to_s
|
|
157
168
|
|
|
158
169
|
# Special handling for require - check if it's in the allowlist
|
|
159
170
|
if %w[require require_relative].include?(method_str)
|
|
160
|
-
required_gem = extract_require_argument(
|
|
171
|
+
required_gem = extract_require_argument(node)
|
|
161
172
|
|
|
162
173
|
# Allow if in the allowlist
|
|
163
174
|
return if required_gem && ALLOWED_REQUIRES.include?(required_gem)
|
|
@@ -166,7 +177,7 @@ module LanguageOperator
|
|
|
166
177
|
violations << {
|
|
167
178
|
type: :dangerous_method,
|
|
168
179
|
method: method_str,
|
|
169
|
-
location: node.location.
|
|
180
|
+
location: node.location.start_line,
|
|
170
181
|
message: "Dangerous method '#{method_str}' is not allowed"
|
|
171
182
|
}
|
|
172
183
|
return
|
|
@@ -177,20 +188,21 @@ module LanguageOperator
|
|
|
177
188
|
violations << {
|
|
178
189
|
type: :dangerous_method,
|
|
179
190
|
method: method_str,
|
|
180
|
-
location: node.location.
|
|
191
|
+
location: node.location.start_line,
|
|
181
192
|
message: "Dangerous method '#{method_str}' is not allowed"
|
|
182
193
|
}
|
|
183
194
|
end
|
|
184
195
|
|
|
185
196
|
# Check for File/Dir/IO operations
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
197
|
+
receiver = node.receiver
|
|
198
|
+
if receiver && (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode))
|
|
199
|
+
const_name = receiver.is_a?(Prism::ConstantReadNode) ? receiver.name.to_s : receiver.name
|
|
200
|
+
if DANGEROUS_CONSTANTS.include?(const_name.to_s)
|
|
189
201
|
violations << {
|
|
190
202
|
type: :dangerous_constant,
|
|
191
|
-
constant: const_name,
|
|
203
|
+
constant: const_name.to_s,
|
|
192
204
|
method: method_str,
|
|
193
|
-
location: node.location.
|
|
205
|
+
location: node.location.start_line,
|
|
194
206
|
message: "Access to #{const_name}.#{method_str} is not allowed"
|
|
195
207
|
}
|
|
196
208
|
end
|
|
@@ -202,14 +214,20 @@ module LanguageOperator
|
|
|
202
214
|
|
|
203
215
|
violations << {
|
|
204
216
|
type: :backtick_execution,
|
|
205
|
-
location: node.location.
|
|
217
|
+
location: node.location.start_line,
|
|
206
218
|
message: 'Backtick command execution is not allowed'
|
|
207
219
|
}
|
|
208
220
|
end
|
|
209
221
|
|
|
210
222
|
def check_constant(node, violations)
|
|
211
|
-
|
|
212
|
-
|
|
223
|
+
const_str = if node.is_a?(Prism::ConstantReadNode)
|
|
224
|
+
node.name.to_s
|
|
225
|
+
elsif node.is_a?(Prism::ConstantPathNode)
|
|
226
|
+
# For paths like Foo::Bar, get the last part
|
|
227
|
+
node.name.to_s
|
|
228
|
+
else
|
|
229
|
+
return
|
|
230
|
+
end
|
|
213
231
|
|
|
214
232
|
# Check for dangerous constants being accessed directly
|
|
215
233
|
return unless DANGEROUS_CONSTANTS.include?(const_str)
|
|
@@ -217,13 +235,13 @@ module LanguageOperator
|
|
|
217
235
|
violations << {
|
|
218
236
|
type: :dangerous_constant_access,
|
|
219
237
|
constant: const_str,
|
|
220
|
-
location: node.location.
|
|
238
|
+
location: node.location.start_line,
|
|
221
239
|
message: "Direct access to #{const_str} constant is not allowed"
|
|
222
240
|
}
|
|
223
241
|
end
|
|
224
242
|
|
|
225
243
|
def check_global_variable(node, violations)
|
|
226
|
-
var_name = node.
|
|
244
|
+
var_name = node.name.to_s
|
|
227
245
|
|
|
228
246
|
# Block access to dangerous global variables
|
|
229
247
|
dangerous_globals = %w[$0 $PROGRAM_NAME $LOAD_PATH $: $LOADED_FEATURES $"]
|
|
@@ -233,21 +251,22 @@ module LanguageOperator
|
|
|
233
251
|
violations << {
|
|
234
252
|
type: :dangerous_global,
|
|
235
253
|
variable: var_name,
|
|
236
|
-
location: node.location.
|
|
254
|
+
location: node.location.start_line,
|
|
237
255
|
message: "Access to global variable #{var_name} is not allowed"
|
|
238
256
|
}
|
|
239
257
|
end
|
|
240
258
|
|
|
241
|
-
def extract_require_argument(
|
|
242
|
-
#
|
|
243
|
-
# We're looking for a string literal like 'language_operator' or "language_operator"
|
|
244
|
-
|
|
259
|
+
def extract_require_argument(node)
|
|
260
|
+
# node is a CallNode for require/require_relative
|
|
261
|
+
# We're looking for a string literal argument like 'language_operator' or "language_operator"
|
|
262
|
+
args = node.arguments
|
|
263
|
+
return nil unless args&.arguments&.any?
|
|
245
264
|
|
|
246
|
-
arg_node = args.first
|
|
265
|
+
arg_node = args.arguments.first
|
|
247
266
|
return nil unless arg_node
|
|
248
267
|
|
|
249
|
-
# Check if it's a string literal (
|
|
250
|
-
return arg_node.
|
|
268
|
+
# Check if it's a string literal (StringNode)
|
|
269
|
+
return arg_node.unescaped if arg_node.is_a?(Prism::StringNode)
|
|
251
270
|
|
|
252
271
|
# If it's not a string literal (e.g., dynamic require), we can't verify it
|
|
253
272
|
nil
|
|
@@ -262,7 +281,7 @@ module LanguageOperator
|
|
|
262
281
|
|
|
263
282
|
footer = "\n\nSynthesized code must only use safe DSL methods and approved helpers."
|
|
264
283
|
footer += "\nSafe methods include: #{SAFE_AGENT_METHODS.join(', ')}, #{SAFE_TOOL_METHODS.join(', ')}"
|
|
265
|
-
footer += "\nSafe helpers include: HTTP.*, Shell.run, validate_*, env_
|
|
284
|
+
footer += "\nSafe helpers include: HTTP.*, Shell.run, validate_*, env_*, TypeCoercion.coerce"
|
|
266
285
|
|
|
267
286
|
header + violation_messages.join("\n") + footer
|
|
268
287
|
end
|
|
@@ -34,10 +34,32 @@ module LanguageOperator
|
|
|
34
34
|
# Step 2: Execute in sandboxed context
|
|
35
35
|
sandbox = SandboxProxy.new(@context, self)
|
|
36
36
|
|
|
37
|
-
# Step 3:
|
|
37
|
+
# Step 3: Prepend safe constant definitions to the code
|
|
38
|
+
# This makes Ruby type constants available in the evaluated scope
|
|
39
|
+
safe_constants_code = <<~RUBY
|
|
40
|
+
Numeric = ::Numeric
|
|
41
|
+
Integer = ::Integer
|
|
42
|
+
Float = ::Float
|
|
43
|
+
String = ::String
|
|
44
|
+
Array = ::Array
|
|
45
|
+
Hash = ::Hash
|
|
46
|
+
TrueClass = ::TrueClass
|
|
47
|
+
FalseClass = ::FalseClass
|
|
48
|
+
Time = ::Time
|
|
49
|
+
Date = ::Date
|
|
50
|
+
RUBY
|
|
51
|
+
|
|
52
|
+
# Step 4: Execute using instance_eval with safe constants prepended
|
|
38
53
|
# Note: We still use instance_eval but with validated code
|
|
39
54
|
# and wrapped context
|
|
40
|
-
|
|
55
|
+
#
|
|
56
|
+
# The string interpolation below evaluates to:
|
|
57
|
+
# sandbox.instance_eval("Numeric = ::Numeric\nInteger = ::Integer\nFloat = ::Float\n
|
|
58
|
+
# String = ::String\nArray = ::Array\nHash = ::Hash\nTrueClass = ::TrueClass\n
|
|
59
|
+
# FalseClass = ::FalseClass\nTime = ::Time\nDate = ::Date\n<user code>", __FILE__, __LINE__)
|
|
60
|
+
# rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
61
|
+
sandbox.instance_eval("#{safe_constants_code}\n#{code}", __FILE__, __LINE__)
|
|
62
|
+
# rubocop:enable Style/DocumentDynamicEvalDefinition
|
|
41
63
|
rescue ASTValidator::SecurityError => e
|
|
42
64
|
# Re-raise validation errors as executor errors for clarity
|
|
43
65
|
raise SecurityError, "Code validation failed: #{e.message}"
|
|
@@ -69,6 +91,18 @@ module LanguageOperator
|
|
|
69
91
|
# Log the call
|
|
70
92
|
@__executor__.log_call(@__context__, method_name, args)
|
|
71
93
|
|
|
94
|
+
# Special handling for require - allow only 'language_operator'
|
|
95
|
+
if %i[require require_relative].include?(method_name)
|
|
96
|
+
required_gem = args.first.to_s
|
|
97
|
+
return ::Kernel.require(required_gem) if required_gem == 'language_operator'
|
|
98
|
+
|
|
99
|
+
# Allow require 'language_operator'
|
|
100
|
+
|
|
101
|
+
::Kernel.raise ::LanguageOperator::Agent::Safety::SafeExecutor::SecurityError,
|
|
102
|
+
"Require '#{required_gem}' is not allowed. Only 'require \"language_operator\"' is permitted."
|
|
103
|
+
|
|
104
|
+
end
|
|
105
|
+
|
|
72
106
|
# Check if method is safe
|
|
73
107
|
unless safe_method?(method_name)
|
|
74
108
|
::Kernel.raise ::LanguageOperator::Agent::Safety::SafeExecutor::SecurityError,
|
|
@@ -92,6 +126,9 @@ module LanguageOperator
|
|
|
92
126
|
return ::LanguageOperator::Dsl::Shell
|
|
93
127
|
end
|
|
94
128
|
|
|
129
|
+
# Ruby type constants are now injected at eval time (see SafeExecutor#eval)
|
|
130
|
+
# but keep this as fallback for dynamic constant access
|
|
131
|
+
|
|
95
132
|
# Otherwise delegate to the context's module
|
|
96
133
|
@__context__.class.const_get(name)
|
|
97
134
|
rescue ::NameError
|
|
@@ -84,6 +84,51 @@ module LanguageOperator
|
|
|
84
84
|
@rufus_scheduler.join
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# Start the scheduler with a main block (DSL v1)
|
|
88
|
+
#
|
|
89
|
+
# @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition with main block
|
|
90
|
+
# @return [void]
|
|
91
|
+
def start_with_main(agent_def)
|
|
92
|
+
logger.info('Agent starting in scheduled mode with main block',
|
|
93
|
+
agent_name: agent_def.name,
|
|
94
|
+
task_count: agent_def.tasks.size)
|
|
95
|
+
logger.info("Workspace: #{@agent.workspace_path}")
|
|
96
|
+
logger.info("Connected to #{@agent.servers_info.length} MCP server(s)")
|
|
97
|
+
|
|
98
|
+
# Extract schedule from agent definition or use default
|
|
99
|
+
cron_schedule = agent_def.schedule&.cron || '0 6 * * *'
|
|
100
|
+
|
|
101
|
+
logger.info('Scheduling main block execution', cron: cron_schedule, agent: agent_def.name)
|
|
102
|
+
|
|
103
|
+
# Create task executor with constraints config
|
|
104
|
+
require_relative 'task_executor'
|
|
105
|
+
config = build_executor_config(agent_def)
|
|
106
|
+
task_executor = TaskExecutor.new(@agent, agent_def.tasks, config)
|
|
107
|
+
|
|
108
|
+
@rufus_scheduler.cron(cron_schedule) do
|
|
109
|
+
with_span('agent.scheduler.execute', attributes: {
|
|
110
|
+
'scheduler.cron_expression' => cron_schedule,
|
|
111
|
+
'agent.name' => agent_def.name,
|
|
112
|
+
'scheduler.task_type' => 'main_block'
|
|
113
|
+
}) do
|
|
114
|
+
logger.timed('Scheduled main block execution') do
|
|
115
|
+
logger.info('Executing scheduled main block', agent: agent_def.name)
|
|
116
|
+
|
|
117
|
+
# Get inputs from environment or default to empty hash
|
|
118
|
+
inputs = {}
|
|
119
|
+
|
|
120
|
+
# Execute main block
|
|
121
|
+
result = agent_def.main.call(inputs, task_executor)
|
|
122
|
+
|
|
123
|
+
logger.info('Main block completed', result: result)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
logger.info('Scheduler started, waiting for scheduled tasks')
|
|
129
|
+
@rufus_scheduler.join
|
|
130
|
+
end
|
|
131
|
+
|
|
87
132
|
# Stop the scheduler
|
|
88
133
|
#
|
|
89
134
|
# @return [void]
|
|
@@ -178,6 +223,21 @@ module LanguageOperator
|
|
|
178
223
|
|
|
179
224
|
logger.info('Scheduled: Daily at 6:00 AM')
|
|
180
225
|
end
|
|
226
|
+
|
|
227
|
+
# Build executor configuration from agent definition constraints
|
|
228
|
+
#
|
|
229
|
+
# @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
|
|
230
|
+
# @return [Hash] Executor configuration
|
|
231
|
+
def build_executor_config(agent_def)
|
|
232
|
+
config = {}
|
|
233
|
+
|
|
234
|
+
if agent_def.constraints
|
|
235
|
+
config[:timeout] = agent_def.constraints[:timeout] if agent_def.constraints[:timeout]
|
|
236
|
+
config[:max_retries] = agent_def.constraints[:max_retries] if agent_def.constraints[:max_retries]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
config
|
|
240
|
+
end
|
|
181
241
|
end
|
|
182
242
|
end
|
|
183
243
|
end
|