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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -8
  3. data/CHANGELOG.md +49 -0
  4. data/CI_STATUS.md +56 -0
  5. data/Gemfile.lock +2 -2
  6. data/Makefile +28 -7
  7. data/Rakefile +29 -0
  8. data/docs/dsl/SCHEMA_VERSION.md +250 -0
  9. data/docs/dsl/agent-reference.md +13 -0
  10. data/lib/language_operator/agent/base.rb +10 -6
  11. data/lib/language_operator/agent/executor.rb +19 -97
  12. data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
  13. data/lib/language_operator/agent/safety/safe_executor.rb +39 -2
  14. data/lib/language_operator/agent/scheduler.rb +60 -0
  15. data/lib/language_operator/agent/task_executor.rb +548 -0
  16. data/lib/language_operator/agent.rb +90 -27
  17. data/lib/language_operator/cli/base_command.rb +117 -0
  18. data/lib/language_operator/cli/commands/agent.rb +351 -466
  19. data/lib/language_operator/cli/commands/cluster.rb +276 -256
  20. data/lib/language_operator/cli/commands/install.rb +110 -119
  21. data/lib/language_operator/cli/commands/model.rb +284 -184
  22. data/lib/language_operator/cli/commands/persona.rb +220 -289
  23. data/lib/language_operator/cli/commands/quickstart.rb +4 -5
  24. data/lib/language_operator/cli/commands/status.rb +36 -53
  25. data/lib/language_operator/cli/commands/system.rb +760 -0
  26. data/lib/language_operator/cli/commands/tool.rb +356 -422
  27. data/lib/language_operator/cli/commands/use.rb +19 -22
  28. data/lib/language_operator/cli/formatters/code_formatter.rb +3 -7
  29. data/lib/language_operator/cli/formatters/log_formatter.rb +3 -5
  30. data/lib/language_operator/cli/formatters/progress_formatter.rb +3 -7
  31. data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
  32. data/lib/language_operator/cli/formatters/table_formatter.rb +10 -26
  33. data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
  34. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
  35. data/lib/language_operator/cli/main.rb +4 -0
  36. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
  37. data/lib/language_operator/client/config.rb +20 -21
  38. data/lib/language_operator/config.rb +115 -3
  39. data/lib/language_operator/constants.rb +54 -0
  40. data/lib/language_operator/dsl/agent_context.rb +7 -7
  41. data/lib/language_operator/dsl/agent_definition.rb +111 -26
  42. data/lib/language_operator/dsl/config.rb +30 -66
  43. data/lib/language_operator/dsl/main_definition.rb +114 -0
  44. data/lib/language_operator/dsl/schema.rb +1143 -0
  45. data/lib/language_operator/dsl/task_definition.rb +315 -0
  46. data/lib/language_operator/dsl.rb +1 -1
  47. data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
  48. data/lib/language_operator/logger.rb +4 -4
  49. data/lib/language_operator/synthesis_test_harness.rb +324 -0
  50. data/lib/language_operator/templates/README.md +23 -0
  51. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +133 -0
  52. data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
  53. data/lib/language_operator/templates/schema/.gitkeep +0 -0
  54. data/lib/language_operator/templates/schema/CHANGELOG.md +119 -0
  55. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
  56. data/lib/language_operator/templates/schema/agent_dsl_schema.json +494 -0
  57. data/lib/language_operator/type_coercion.rb +250 -0
  58. data/lib/language_operator/ux/base.rb +81 -0
  59. data/lib/language_operator/ux/concerns/README.md +155 -0
  60. data/lib/language_operator/ux/concerns/headings.rb +90 -0
  61. data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
  62. data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
  63. data/lib/language_operator/ux/create_agent.rb +252 -0
  64. data/lib/language_operator/ux/create_model.rb +267 -0
  65. data/lib/language_operator/ux/quickstart.rb +594 -0
  66. data/lib/language_operator/version.rb +1 -1
  67. data/lib/language_operator.rb +2 -0
  68. data/requirements/ARCHITECTURE.md +1 -0
  69. data/requirements/SCRATCH.md +153 -0
  70. data/requirements/dsl.md +0 -0
  71. data/requirements/features +1 -0
  72. data/requirements/personas +1 -0
  73. data/requirements/proposals +1 -0
  74. data/requirements/tasks/iterate.md +14 -15
  75. data/requirements/tasks/optimize.md +13 -4
  76. data/synth/001/Makefile +90 -0
  77. data/synth/001/agent.rb +26 -0
  78. data/synth/001/agent.yaml +7 -0
  79. data/synth/001/output.log +44 -0
  80. data/synth/Makefile +39 -0
  81. data/synth/README.md +342 -0
  82. metadata +49 -18
  83. data/examples/README.md +0 -569
  84. data/examples/agent_example.rb +0 -86
  85. data/examples/chat_endpoint_agent.rb +0 -118
  86. data/examples/github_webhook_agent.rb +0 -171
  87. data/examples/mcp_agent.rb +0 -158
  88. data/examples/oauth_callback_agent.rb +0 -296
  89. data/examples/stripe_webhook_agent.rb +0 -219
  90. data/examples/webhook_agent.rb +0 -80
  91. data/lib/language_operator/dsl/workflow_definition.rb +0 -259
  92. 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 or workflow
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 with workflow
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('🤖 LLM request')
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
- logger.info('✓ Iteration completed',
114
- iteration: @iteration_count,
115
- response_length: result_text.length,
116
- total_tokens: metrics[:totalTokens],
117
- estimated_cost: "$#{metrics[:estimatedCost]}")
118
- logger.debug('Response preview', response: result_text[0..200])
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('Starting execution')
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
- logger.info('Instructions', instructions: instructions[0..200])
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('Execution complete',
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("⚠️ Could not write to #{workspace_path}, wrote to output.txt instead")
227
+ logger.warn("Could not write to #{workspace_path}, wrote to output.txt instead")
306
228
  rescue StandardError => e2
307
- logger.warn("⚠️ Could not write output to workspace: #{e2.message}")
308
- logger.info("📄 Output (first 500 chars): #{content[0..500]}")
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 'parser/current'
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
- workflow step tool params depends_on prompt
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
- @parser = Parser::CurrentRuby.new
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 Parser::SyntaxError) to violation
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 Parser::SyntaxError => e
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
- buffer = Parser::Source::Buffer.new(file_path)
120
- buffer.source = code
121
- @parser.parse(buffer)
122
- rescue Parser::SyntaxError => e
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
- case node.type
130
- when :send
141
+ # Prism uses different node types
142
+ case node
143
+ when Prism::CallNode
131
144
  check_method_call(node, violations)
132
- when :const
145
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
133
146
  check_constant(node, violations)
134
- when :gvar
147
+ when Prism::GlobalVariableReadNode, Prism::GlobalVariableWriteNode
135
148
  check_global_variable(node, violations)
136
- when :xstr
149
+ when Prism::XStringNode
137
150
  # Backtick string execution (e.g., `command`)
138
151
  violations << {
139
152
  type: :backtick_execution,
140
- location: node.location.line,
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.children.each do |child|
147
- scan_ast(child, violations) if child.is_a?(Parser::AST::Node)
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
- receiver, method_name, *args = node.children
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(args)
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.line,
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.line,
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
- if receiver && receiver.type == :const
187
- const_name = receiver.children[1].to_s
188
- if DANGEROUS_CONSTANTS.include?(const_name)
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.line,
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.line,
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
- _, const_name = node.children
212
- const_str = const_name.to_s
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.line,
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.children[0].to_s
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.line,
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(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?
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 (:str node)
250
- return arg_node.children[0] if arg_node.type == :str
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: Execute using instance_eval
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
- sandbox.instance_eval(code, file_path)
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