language-operator 0.1.61 → 0.1.62

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/persona.md +9 -0
  3. data/.claude/commands/task.md +46 -1
  4. data/.rubocop.yml +13 -0
  5. data/.rubocop_custom/use_ux_helper.rb +44 -0
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile.lock +12 -1
  8. data/Makefile +26 -7
  9. data/Makefile.common +50 -0
  10. data/bin/aictl +8 -1
  11. data/components/agent/Gemfile +1 -1
  12. data/components/agent/bin/langop-agent +7 -0
  13. data/docs/README.md +58 -0
  14. data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
  15. data/docs/cli-reference.md +274 -0
  16. data/docs/{dsl/constraints.md → constraints.md} +5 -5
  17. data/docs/how-agents-work.md +156 -0
  18. data/docs/installation.md +218 -0
  19. data/docs/quickstart.md +299 -0
  20. data/docs/understanding-generated-code.md +265 -0
  21. data/docs/using-tools.md +457 -0
  22. data/docs/webhooks.md +509 -0
  23. data/examples/ux_helpers_demo.rb +296 -0
  24. data/lib/language_operator/agent/base.rb +11 -1
  25. data/lib/language_operator/agent/executor.rb +23 -6
  26. data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
  27. data/lib/language_operator/agent/task_executor.rb +346 -63
  28. data/lib/language_operator/agent/web_server.rb +110 -14
  29. data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
  30. data/lib/language_operator/agent.rb +88 -2
  31. data/lib/language_operator/cli/base_command.rb +17 -11
  32. data/lib/language_operator/cli/command_loader.rb +72 -0
  33. data/lib/language_operator/cli/commands/agent/base.rb +837 -0
  34. data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
  35. data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
  36. data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
  37. data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
  38. data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
  39. data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
  40. data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
  41. data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
  42. data/lib/language_operator/cli/commands/cluster.rb +129 -84
  43. data/lib/language_operator/cli/commands/install.rb +1 -1
  44. data/lib/language_operator/cli/commands/model/base.rb +215 -0
  45. data/lib/language_operator/cli/commands/model/test.rb +165 -0
  46. data/lib/language_operator/cli/commands/persona.rb +16 -34
  47. data/lib/language_operator/cli/commands/quickstart.rb +3 -2
  48. data/lib/language_operator/cli/commands/status.rb +40 -67
  49. data/lib/language_operator/cli/commands/system/base.rb +44 -0
  50. data/lib/language_operator/cli/commands/system/exec.rb +147 -0
  51. data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
  52. data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
  53. data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
  54. data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
  55. data/lib/language_operator/cli/commands/system/schema.rb +92 -0
  56. data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
  57. data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
  58. data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
  59. data/lib/language_operator/cli/commands/tool/base.rb +271 -0
  60. data/lib/language_operator/cli/commands/tool/install.rb +255 -0
  61. data/lib/language_operator/cli/commands/tool/search.rb +69 -0
  62. data/lib/language_operator/cli/commands/tool/test.rb +115 -0
  63. data/lib/language_operator/cli/commands/use.rb +29 -6
  64. data/lib/language_operator/cli/errors/handler.rb +20 -17
  65. data/lib/language_operator/cli/errors/suggestions.rb +3 -5
  66. data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
  67. data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
  68. data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
  69. data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
  70. data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
  71. data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
  72. data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
  73. data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
  74. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
  75. data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
  76. data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
  77. data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
  78. data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
  79. data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
  80. data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
  81. data/lib/language_operator/cli/main.rb +50 -40
  82. data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
  83. data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
  84. data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
  85. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
  86. data/lib/language_operator/client/base.rb +28 -0
  87. data/lib/language_operator/client/config.rb +4 -1
  88. data/lib/language_operator/client/mcp_connector.rb +1 -1
  89. data/lib/language_operator/config/cluster_config.rb +3 -2
  90. data/lib/language_operator/config.rb +38 -11
  91. data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
  92. data/lib/language_operator/constants.rb +13 -0
  93. data/lib/language_operator/dsl/http.rb +127 -10
  94. data/lib/language_operator/dsl.rb +153 -6
  95. data/lib/language_operator/errors.rb +50 -0
  96. data/lib/language_operator/kubernetes/client.rb +11 -6
  97. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  98. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  99. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  100. data/lib/language_operator/type_coercion.rb +118 -34
  101. data/lib/language_operator/utils/secure_path.rb +74 -0
  102. data/lib/language_operator/utils.rb +7 -0
  103. data/lib/language_operator/validators.rb +54 -2
  104. data/lib/language_operator/version.rb +1 -1
  105. data/synth/001/Makefile +10 -2
  106. data/synth/001/agent.rb +16 -15
  107. data/synth/001/output.log +27 -10
  108. data/synth/002/Makefile +10 -2
  109. data/synth/003/Makefile +1 -1
  110. data/synth/003/README.md +205 -133
  111. data/synth/003/agent.optimized.rb +66 -0
  112. data/synth/003/agent.synthesized.rb +41 -0
  113. metadata +111 -35
  114. data/docs/dsl/agent-reference.md +0 -604
  115. data/docs/dsl/mcp-integration.md +0 -1177
  116. data/docs/dsl/webhooks.md +0 -932
  117. data/docs/dsl/workflows.md +0 -744
  118. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  119. data/lib/language_operator/cli/commands/model.rb +0 -366
  120. data/lib/language_operator/cli/commands/system.rb +0 -1259
  121. data/lib/language_operator/cli/commands/tool.rb +0 -654
  122. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  123. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  124. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
  125. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
  126. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
  127. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
  128. data/lib/language_operator/learning/optimizer.rb +0 -319
  129. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  130. data/lib/language_operator/learning/task_synthesizer.rb +0 -288
  131. data/lib/language_operator/learning/trace_analyzer.rb +0 -285
  132. data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
  133. data/lib/language_operator/ux/base.rb +0 -81
  134. data/lib/language_operator/ux/concerns/README.md +0 -155
  135. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  136. data/lib/language_operator/ux/create_agent.rb +0 -255
  137. data/lib/language_operator/ux/create_model.rb +0 -267
  138. data/lib/language_operator/ux/quickstart.rb +0 -594
  139. data/synth/003/agent.rb +0 -41
  140. data/synth/003/output.log +0 -68
  141. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  142. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  143. /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Demo script showcasing all UxHelper components
5
+ #
6
+ # Usage:
7
+ # ruby examples/ux_helpers_demo.rb
8
+
9
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
10
+
11
+ require 'language_operator/cli/helpers/ux_helper'
12
+
13
+ # Demo class showing all UX helper features
14
+ class UxDemo
15
+ include LanguageOperator::CLI::Helpers::UxHelper
16
+
17
+ def run
18
+ show_header
19
+ demo_colors
20
+ demo_spinner
21
+ demo_table
22
+ demo_box
23
+ demo_highlighted_box
24
+ demo_list_box
25
+ demo_prompt if ARGV.include?('--interactive')
26
+ end
27
+
28
+ private
29
+
30
+ def show_header
31
+ puts "\n"
32
+ puts pastel.bold.cyan('=' * 60)
33
+ puts pastel.bold.cyan(' UxHelper Demo - All Available Components')
34
+ puts pastel.bold.cyan('=' * 60)
35
+ puts "\n"
36
+ end
37
+
38
+ def demo_colors
39
+ puts pastel.bold('1. Colors & Styles')
40
+ puts pastel.dim('-' * 40)
41
+ puts " #{pastel.green('✓')} Success message"
42
+ puts " #{pastel.yellow('⚠')} Warning message"
43
+ puts " #{pastel.red('✗')} Error message"
44
+ puts " #{pastel.cyan('ℹ')} Info message"
45
+ puts " #{pastel.bold('Bold text')}"
46
+ puts " #{pastel.dim('Dimmed text')}"
47
+ puts " #{pastel.red.bold('Combined: red + bold')}"
48
+ puts "\n"
49
+ end
50
+
51
+ def demo_spinner
52
+ puts pastel.bold('2. Spinners')
53
+ puts pastel.dim('-' * 40)
54
+
55
+ # Example 1: Success
56
+ spin = spinner('Loading configuration...')
57
+ spin.auto_spin
58
+ sleep 1.5
59
+ spin.success('Config loaded!')
60
+
61
+ # Example 2: Processing
62
+ spin = spinner('Processing data...')
63
+ spin.auto_spin
64
+ sleep 1
65
+ spin.success('Data processed!')
66
+
67
+ # Example 3: Error handling
68
+ spin = spinner('Connecting to cluster...')
69
+ spin.auto_spin
70
+ sleep 1
71
+ if rand > 0.5
72
+ spin.success('Connected!')
73
+ else
74
+ spin.error('Connection failed!')
75
+ end
76
+
77
+ puts "\n"
78
+ end
79
+
80
+ def demo_table
81
+ puts pastel.bold('3. Tables')
82
+ puts pastel.dim('-' * 40)
83
+
84
+ # Agent status table
85
+ agents = [
86
+ ['agent-web', pastel.green('running'), '2h 15m', '42.3 MB'],
87
+ ['agent-data', pastel.green('running'), '5h 02m', '128.7 MB'],
88
+ ['agent-sync', pastel.yellow('pending'), '0m', '-'],
89
+ ['agent-test', pastel.red('stopped'), '12m', '8.1 MB']
90
+ ]
91
+
92
+ puts table(%w[Name Status Uptime Memory], agents)
93
+ puts "\n"
94
+ end
95
+
96
+ def demo_box
97
+ puts pastel.bold('4. Boxes')
98
+ puts pastel.dim('-' * 40)
99
+ puts "\n"
100
+
101
+ # Simple box
102
+ puts box('Simple framed message')
103
+ puts "\n"
104
+
105
+ # Box with title
106
+ puts box(
107
+ 'Agent deployed successfully to cluster!',
108
+ title: 'Success'
109
+ )
110
+ puts "\n"
111
+
112
+ # Warning box
113
+ puts box(
114
+ 'This action cannot be undone.',
115
+ title: 'Warning',
116
+ border: :thick
117
+ )
118
+ puts "\n"
119
+
120
+ # Multi-line box
121
+ puts box(<<~MSG, title: 'Next Steps', padding: 2)
122
+ 1. Monitor logs: aictl agent logs my-agent
123
+ 2. Check status: aictl agent inspect my-agent
124
+ 3. View metrics: aictl agent metrics my-agent
125
+ 4. Scale replicas: aictl agent scale my-agent --replicas=3
126
+ MSG
127
+ puts "\n"
128
+ end
129
+
130
+ def demo_highlighted_box
131
+ puts pastel.bold('5. Highlighted Boxes')
132
+ puts pastel.dim('-' * 40)
133
+ puts "\n"
134
+
135
+ # Model details
136
+ highlighted_box(
137
+ title: 'LanguageModel Details',
138
+ rows: {
139
+ 'Name' => 'gpt-4-turbo',
140
+ 'Provider' => 'OpenAI',
141
+ 'Model' => 'gpt-4-turbo-preview',
142
+ 'Cluster' => 'production'
143
+ }
144
+ )
145
+ puts "\n"
146
+
147
+ # Agent configuration with nil values (skipped)
148
+ highlighted_box(
149
+ title: 'Agent Configuration',
150
+ rows: {
151
+ 'Name' => 'web-scraper',
152
+ 'Mode' => 'scheduled',
153
+ 'Schedule' => '0 */6 * * *',
154
+ 'Endpoint' => nil, # This will be skipped
155
+ 'Replicas' => '3',
156
+ 'Status' => pastel.green('Running')
157
+ }
158
+ )
159
+ puts "\n"
160
+
161
+ # Resource summary with custom character
162
+ highlighted_box(
163
+ title: 'Resource Summary',
164
+ rows: {
165
+ 'Created' => '5 agents',
166
+ 'Updated' => '2 models',
167
+ 'Deleted' => '0 personas'
168
+ },
169
+ title_char: '▶'
170
+ )
171
+ puts "\n"
172
+
173
+ # Deployment status
174
+ highlighted_box(
175
+ title: 'Deployment Status',
176
+ rows: {
177
+ 'Cluster' => 'production-us-west',
178
+ 'Namespace' => 'language-operator',
179
+ 'Available' => pastel.green('12/12'),
180
+ 'Ready' => pastel.green('12/12'),
181
+ 'Up-to-date' => pastel.green('12/12')
182
+ }
183
+ )
184
+ puts "\n"
185
+
186
+ # Custom colors
187
+ highlighted_box(
188
+ title: 'Success',
189
+ rows: {
190
+ 'Operation' => 'Model created',
191
+ 'Status' => 'Completed'
192
+ },
193
+ color: :green
194
+ )
195
+ puts "\n"
196
+
197
+ highlighted_box(
198
+ title: 'Error',
199
+ rows: {
200
+ 'Code' => '500',
201
+ 'Message' => 'Connection failed'
202
+ },
203
+ color: :red
204
+ )
205
+ puts "\n"
206
+ end
207
+
208
+ def demo_list_box
209
+ puts pastel.bold('6. List Boxes')
210
+ puts pastel.dim('-' * 40)
211
+ puts "\n"
212
+
213
+ # Simple list
214
+ list_box(
215
+ title: 'Models',
216
+ items: ['gpt-4-turbo', 'claude-3-opus', 'llama-3-70b']
217
+ )
218
+ puts "\n"
219
+
220
+ # Detailed list
221
+ list_box(
222
+ title: 'Agents',
223
+ items: [
224
+ { name: 'bash-agent', status: pastel.green('Running') },
225
+ { name: 'web-scraper', status: pastel.yellow('Pending') },
226
+ { name: 'data-processor', status: pastel.red('Stopped') }
227
+ ],
228
+ style: :detailed
229
+ )
230
+ puts "\n"
231
+
232
+ # Conditions style
233
+ list_box(
234
+ title: 'Conditions',
235
+ items: [
236
+ { type: 'Ready', status: 'True', message: 'Agent is ready' },
237
+ { type: 'Synthesized', status: 'True', message: 'Code synthesized successfully' },
238
+ { type: 'Validated', status: 'False', message: 'Validation pending' }
239
+ ],
240
+ style: :conditions
241
+ )
242
+ puts "\n"
243
+
244
+ # Key-value pairs
245
+ list_box(
246
+ title: 'Labels',
247
+ items: {
248
+ 'app' => 'language-operator',
249
+ 'env' => 'production',
250
+ 'version' => 'v1.0.0'
251
+ },
252
+ style: :key_value
253
+ )
254
+ puts "\n"
255
+
256
+ # Empty list
257
+ list_box(
258
+ title: 'Personas',
259
+ items: [],
260
+ empty_message: 'No personas configured'
261
+ )
262
+ puts "\n"
263
+ end
264
+
265
+ def demo_prompt
266
+ puts pastel.bold('7. Interactive Prompts')
267
+ puts pastel.dim('-' * 40)
268
+
269
+ name = prompt.ask('What is your name?')
270
+ puts " Hello, #{pastel.cyan(name)}!"
271
+
272
+ if prompt.yes?('Do you like the new UX helpers?')
273
+ puts " #{pastel.green('Great!')} We're glad you like them."
274
+ else
275
+ puts " #{pastel.yellow('Thanks for the feedback!')} We'll keep improving."
276
+ end
277
+
278
+ choice = prompt.select(
279
+ 'Which helper is your favorite?',
280
+ %w[pastel prompt spinner table box highlighted_box list_box]
281
+ )
282
+ puts " You selected: #{pastel.bold(choice)}"
283
+
284
+ puts "\n"
285
+ end
286
+ end
287
+
288
+ # Run the demo
289
+ if __FILE__ == $PROGRAM_NAME
290
+ demo = UxDemo.new
291
+ demo.run
292
+
293
+ puts demo.pastel.bold.cyan('Demo complete!')
294
+ puts demo.pastel.dim("Run with --interactive for prompt examples: ruby #{__FILE__} --interactive")
295
+ puts "\n"
296
+ end
@@ -38,7 +38,7 @@ module LanguageOperator
38
38
  logger.info "OpenTelemetry #{otel_enabled ? 'enabled' : 'disabled'}"
39
39
 
40
40
  @workspace_path = ENV.fetch('WORKSPACE_PATH', '/workspace')
41
- @mode = ENV.fetch('AGENT_MODE', 'autonomous')
41
+ @mode = agent_mode_with_default
42
42
  @executor = nil
43
43
  end
44
44
 
@@ -134,6 +134,16 @@ module LanguageOperator
134
134
  rescue StandardError => e
135
135
  logger.warn("Failed to flush telemetry: #{e.message}")
136
136
  end
137
+
138
+ # Get AGENT_MODE with fallback to default, handling empty/whitespace values
139
+ #
140
+ # @return [String] The agent mode to use
141
+ def agent_mode_with_default
142
+ mode = ENV.fetch('AGENT_MODE', nil)
143
+ return 'autonomous' if mode.nil? || mode.strip.empty?
144
+
145
+ mode
146
+ end
137
147
  end
138
148
  end
139
149
  end
@@ -56,6 +56,16 @@ module LanguageOperator
56
56
  execute(enriched_instruction)
57
57
  end
58
58
 
59
+ # Cleanup executor resources including MCP connections
60
+ #
61
+ # This method delegates to the agent's connection cleanup to prevent
62
+ # resource leaks when executors are no longer needed.
63
+ #
64
+ # @return [void]
65
+ def cleanup_connections
66
+ @agent.cleanup_connections if @agent.respond_to?(:cleanup_connections)
67
+ end
68
+
59
69
  # Execute a single task
60
70
  #
61
71
  # @param task [String] The task to execute
@@ -282,23 +292,30 @@ module LanguageOperator
282
292
 
283
293
  def parse_float_env(key)
284
294
  val = ENV.fetch(key, nil)
285
- return nil unless val
295
+ return nil unless val && !val.strip.empty?
286
296
 
287
- val.to_f
297
+ Float(val.strip)
298
+ rescue ArgumentError
299
+ logger.warn("Invalid float value for #{key}: #{val}. Ignoring.")
300
+ nil
288
301
  end
289
302
 
290
303
  def parse_int_env(key)
291
304
  val = ENV.fetch(key, nil)
292
- return nil unless val
305
+ return nil unless val && !val.strip.empty?
293
306
 
294
- val.to_i
307
+ Integer(val.strip)
308
+ rescue ArgumentError
309
+ logger.warn("Invalid integer value for #{key}: #{val}. Ignoring.")
310
+ nil
295
311
  end
296
312
 
297
313
  def parse_array_env(key)
298
314
  val = ENV.fetch(key, nil)
299
- return nil unless val
315
+ return nil unless val && !val.strip.empty?
300
316
 
301
- val.split(',').map(&:strip)
317
+ result = val.split(',').map(&:strip).reject(&:empty?)
318
+ result.empty? ? nil : result
302
319
  end
303
320
 
304
321
  def estimate_tokens(text)
@@ -34,32 +34,26 @@ module LanguageOperator
34
34
  # Step 2: Execute in sandboxed context
35
35
  sandbox = SandboxProxy.new(@context, self)
36
36
 
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
53
- # Note: We still use instance_eval but with validated code
54
- # and wrapped context
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
37
+ # Step 3: Execute using instance_eval with smart constant injection
38
+ # Only inject constants that won't conflict with user-defined ones
39
+ safe_constants = %w[Numeric Integer Float String Array Hash TrueClass FalseClass Time Date]
40
+
41
+ # Find which constants user code defines to avoid redefinition warnings
42
+ user_defined_constants = safe_constants.select { |const| code.include?("#{const} =") }
43
+
44
+ # Only inject constants that user code doesn't define
45
+ constants_to_inject = safe_constants - user_defined_constants
46
+
47
+ if constants_to_inject.any?
48
+ # Inject only safe constants that won't conflict
49
+ safe_setup = constants_to_inject.map { |name| "#{name} = ::#{name}" }.join("\n")
50
+ # rubocop:disable Style/DocumentDynamicEvalDefinition
51
+ sandbox.instance_eval("#{safe_setup}\n#{code}", __FILE__, __LINE__)
52
+ # rubocop:enable Style/DocumentDynamicEvalDefinition
53
+ else
54
+ # User defines all constants, just run their code
55
+ sandbox.instance_eval(code, file_path, 1)
56
+ end
63
57
  rescue ASTValidator::SecurityError => e
64
58
  # Re-raise validation errors as executor errors for clarity
65
59
  raise SecurityError, "Code validation failed: #{e.message}"
@@ -119,20 +113,28 @@ module LanguageOperator
119
113
 
120
114
  # Provide access to safe constants from the context
121
115
  def const_missing(name)
122
- # Allow access to HTTP and Shell helper classes
123
- if name == :HTTP
124
- return ::LanguageOperator::Dsl::HTTP
125
- elsif name == :Shell
126
- return ::LanguageOperator::Dsl::Shell
116
+ # Define allowed constants explicitly (security-by-default)
117
+ # This allowlist must be kept in sync with ASTValidator safe constants
118
+ case name
119
+ when :HTTP
120
+ ::LanguageOperator::Dsl::HTTP
121
+ when :Shell
122
+ ::LanguageOperator::Dsl::Shell
123
+ when :String, :Array, :Hash, :Integer, :Float, :Numeric, :Symbol
124
+ # Allow safe Ruby built-in types
125
+ ::Object.const_get(name)
126
+ when :Time, :Date
127
+ # Allow safe time/date types
128
+ ::Object.const_get(name)
129
+ when :TrueClass, :FalseClass, :NilClass
130
+ # Allow boolean and nil types
131
+ ::Object.const_get(name)
132
+ else
133
+ # Security-by-default: explicitly deny access to any other constants
134
+ # This prevents sandbox bypass through const_missing fallback
135
+ ::Kernel.raise ::LanguageOperator::Agent::Safety::SafeExecutor::SecurityError,
136
+ "Access to constant '#{name}' is not allowed in sandbox (security restriction)"
127
137
  end
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
-
132
- # Otherwise delegate to the context's module
133
- @__context__.class.const_get(name)
134
- rescue ::NameError
135
- ::Kernel.raise ::NameError, "uninitialized constant #{name}"
136
138
  end
137
139
 
138
140
  private