language-operator 0.1.59 → 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 (144) 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 +14 -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 +369 -68
  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 +31 -1
  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/task_definition.rb +7 -6
  95. data/lib/language_operator/dsl.rb +153 -6
  96. data/lib/language_operator/errors.rb +50 -0
  97. data/lib/language_operator/kubernetes/client.rb +11 -6
  98. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  99. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  100. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  101. data/lib/language_operator/type_coercion.rb +118 -34
  102. data/lib/language_operator/utils/secure_path.rb +74 -0
  103. data/lib/language_operator/utils.rb +7 -0
  104. data/lib/language_operator/validators.rb +54 -2
  105. data/lib/language_operator/version.rb +1 -1
  106. data/synth/001/Makefile +10 -2
  107. data/synth/001/agent.rb +16 -15
  108. data/synth/001/output.log +27 -10
  109. data/synth/002/Makefile +10 -2
  110. data/synth/003/Makefile +3 -3
  111. data/synth/003/README.md +205 -133
  112. data/synth/003/agent.optimized.rb +66 -0
  113. data/synth/003/agent.synthesized.rb +41 -0
  114. metadata +111 -35
  115. data/docs/dsl/agent-reference.md +0 -604
  116. data/docs/dsl/mcp-integration.md +0 -1177
  117. data/docs/dsl/webhooks.md +0 -932
  118. data/docs/dsl/workflows.md +0 -744
  119. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  120. data/lib/language_operator/cli/commands/model.rb +0 -366
  121. data/lib/language_operator/cli/commands/system.rb +0 -1259
  122. data/lib/language_operator/cli/commands/tool.rb +0 -654
  123. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  124. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  125. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -147
  126. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -218
  127. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -432
  128. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -236
  129. data/lib/language_operator/learning/optimizer.rb +0 -318
  130. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  131. data/lib/language_operator/learning/task_synthesizer.rb +0 -261
  132. data/lib/language_operator/learning/trace_analyzer.rb +0 -280
  133. data/lib/language_operator/templates/task_synthesis.tmpl +0 -97
  134. data/lib/language_operator/ux/base.rb +0 -81
  135. data/lib/language_operator/ux/concerns/README.md +0 -155
  136. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  137. data/lib/language_operator/ux/create_agent.rb +0 -255
  138. data/lib/language_operator/ux/create_model.rb +0 -267
  139. data/lib/language_operator/ux/quickstart.rb +0 -594
  140. data/synth/003/agent.rb +0 -41
  141. data/synth/003/output.log +0 -68
  142. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  143. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  144. /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
@@ -1,6 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LanguageOperator
4
+ # Base exception class for all Language Operator errors
5
+ class Error < StandardError; end
6
+
7
+ # File loading related errors
8
+ class FileLoadError < Error; end
9
+ class FileNotFoundError < FileLoadError; end
10
+ class FilePermissionError < FileLoadError; end
11
+ class FileSyntaxError < FileLoadError; end
12
+
13
+ # Security related errors
14
+ class SecurityError < Error; end
15
+ class PathTraversalError < SecurityError; end
16
+
4
17
  # Standardized error formatting module for consistent error messages across tools
5
18
  module Errors
6
19
  # Resource not found error
@@ -56,5 +69,42 @@ module LanguageOperator
56
69
  def self.empty_field(field_name)
57
70
  "Error: #{field_name} cannot be empty"
58
71
  end
72
+
73
+ # File not found error
74
+ # @param file_path [String] Path to the file that wasn't found
75
+ # @param context [String] Additional context about what the file is for
76
+ # @return [String] Formatted error message
77
+ def self.file_not_found(file_path, context = 'file')
78
+ "Error: #{context.capitalize} not found at '#{file_path}'. " \
79
+ 'Please check the file path exists and is accessible.'
80
+ end
81
+
82
+ # File permission error
83
+ # @param file_path [String] Path to the file with permission issues
84
+ # @param context [String] Additional context about what the file is for
85
+ # @return [String] Formatted error message
86
+ def self.file_permission_denied(file_path, context = 'file')
87
+ "Error: Permission denied reading #{context} '#{file_path}'. " \
88
+ 'Please check file permissions or run with appropriate access rights.'
89
+ end
90
+
91
+ # File syntax error
92
+ # @param file_path [String] Path to the file with syntax errors
93
+ # @param original_error [String] Original error message from parser
94
+ # @param context [String] Additional context about what the file is for
95
+ # @return [String] Formatted error message
96
+ def self.file_syntax_error(file_path, original_error, context = 'file')
97
+ "Error: Syntax error in #{context} '#{file_path}': #{original_error}. " \
98
+ 'Please check the file for valid Ruby syntax.'
99
+ end
100
+
101
+ # Path traversal security error
102
+ # @param context [String] Context about what operation was attempted
103
+ # @return [String] Formatted error message
104
+ def self.path_traversal_blocked(context = 'file operation')
105
+ "Error: Path traversal attempt blocked during #{context}. " \
106
+ 'File path must be within allowed directories. ' \
107
+ 'Use relative paths or configure LANGOP_ALLOWED_PATHS if needed.'
108
+ end
59
109
  end
60
110
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'k8s-ruby'
4
4
  require 'yaml'
5
+ require_relative '../utils/secure_path'
5
6
 
6
7
  module LanguageOperator
7
8
  module Kubernetes
@@ -30,7 +31,7 @@ module LanguageOperator
30
31
 
31
32
  def initialize(kubeconfig: nil, context: nil, in_cluster: false)
32
33
  @in_cluster = in_cluster
33
- @kubeconfig = kubeconfig || ENV.fetch('KUBECONFIG', File.expand_path('~/.kube/config'))
34
+ @kubeconfig = kubeconfig || ENV.fetch('KUBECONFIG', LanguageOperator::Utils::SecurePath.expand_home_path('.kube/config'))
34
35
  @context = context
35
36
  @client = build_client
36
37
  end
@@ -45,7 +46,11 @@ module LanguageOperator
45
46
  nil
46
47
  end
47
48
 
48
- # Get the current namespace from the context
49
+ # Get the current namespace from the context.
50
+ # Returns the namespace from service account (in-cluster) or kubeconfig context.
51
+ # Gracefully handles all filesystem errors and returns nil on failure.
52
+ #
53
+ # @return [String, nil] the current namespace, or nil if unable to determine
49
54
  def current_namespace
50
55
  if @in_cluster
51
56
  # In-cluster: read from service account namespace
@@ -56,7 +61,7 @@ module LanguageOperator
56
61
  context_obj = config.context(context_name)
57
62
  context_obj&.namespace
58
63
  end
59
- rescue Errno::ENOENT
64
+ rescue SystemCallError, IOError
60
65
  nil
61
66
  end
62
67
 
@@ -171,8 +176,8 @@ module LanguageOperator
171
176
  def operator_version
172
177
  deployment = @client.api('apps/v1')
173
178
  .resource('deployments', namespace: 'kube-system')
174
- .get('language-operator')
175
- deployment.dig('metadata', 'labels', 'app.kubernetes.io/version') || 'unknown'
179
+ .get(Constants::KubernetesLabels::PROJECT_NAME)
180
+ deployment.dig('metadata', 'labels', Constants::KubernetesLabels::VERSION) || 'unknown'
176
181
  rescue K8s::Error::NotFound
177
182
  nil
178
183
  end
@@ -275,7 +280,7 @@ module LanguageOperator
275
280
  'v1'
276
281
  when 'deployment', 'statefulset'
277
282
  'apps/v1'
278
- when 'cronjob'
283
+ when 'cronjob', 'job'
279
284
  'batch/v1'
280
285
  else
281
286
  'v1'
@@ -1,37 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../constants/kubernetes_labels'
4
+
3
5
  module LanguageOperator
4
6
  module Kubernetes
5
7
  # Builds Kubernetes resource manifests for language-operator
6
8
  class ResourceBuilder
7
9
  class << self
8
10
  # Build a LanguageCluster resource
9
- def language_cluster(name, namespace: nil, labels: {})
10
- {
11
- 'apiVersion' => 'langop.io/v1alpha1',
12
- 'kind' => 'LanguageCluster',
13
- 'metadata' => {
14
- 'name' => name,
15
- 'namespace' => namespace || 'default',
16
- 'labels' => default_labels.merge(labels)
17
- },
18
- 'spec' => {
19
- 'namespace' => namespace || name,
20
- 'resourceQuota' => default_resource_quota,
21
- 'networkPolicy' => default_network_policy
22
- }
11
+ def language_cluster(name, namespace: nil, domain: nil, labels: {})
12
+ spec = {
13
+ 'namespace' => namespace || name,
14
+ 'resourceQuota' => default_resource_quota,
15
+ 'networkPolicy' => default_network_policy
23
16
  }
17
+ spec['domain'] = domain if domain && !domain.empty?
18
+
19
+ build_resource('LanguageCluster', name, spec, namespace: namespace, labels: labels)
24
20
  end
25
21
 
26
22
  # Build a LanguageAgent resource
27
- def language_agent(name, instructions:, cluster: nil, schedule: nil, persona: nil, tools: [], models: [],
23
+ def language_agent(name, instructions:, cluster: nil, cluster_ref: nil, schedule: nil, persona: nil, tools: [], models: [],
28
24
  mode: nil, workspace: true, labels: {})
29
25
  # Determine mode: reactive, scheduled, or autonomous
30
26
  spec_mode = mode || (schedule ? 'scheduled' : 'autonomous')
31
27
 
32
28
  spec = {
33
29
  'instructions' => instructions,
34
- 'mode' => spec_mode,
30
+ 'executionMode' => spec_mode,
35
31
  'image' => 'ghcr.io/language-operator/agent:latest'
36
32
  }
37
33
 
@@ -44,86 +40,41 @@ module LanguageOperator
44
40
  # Enable workspace by default for state persistence
45
41
  spec['workspace'] = { 'enabled' => workspace } if workspace
46
42
 
47
- {
48
- 'apiVersion' => 'langop.io/v1alpha1',
49
- 'kind' => 'LanguageAgent',
50
- 'metadata' => {
51
- 'name' => name,
52
- 'namespace' => cluster || 'default',
53
- 'labels' => default_labels.merge(labels)
54
- },
55
- 'spec' => spec
56
- }
43
+ build_resource('LanguageAgent', name, spec, namespace: cluster, cluster_ref: cluster_ref, labels: labels)
57
44
  end
58
45
 
59
46
  # Build a LanguageTool resource
60
- def language_tool(name, type:, config: {}, cluster: nil, labels: {})
61
- {
62
- 'apiVersion' => 'langop.io/v1alpha1',
63
- 'kind' => 'LanguageTool',
64
- 'metadata' => {
65
- 'name' => name,
66
- 'namespace' => cluster || 'default',
67
- 'labels' => default_labels.merge(labels)
68
- },
69
- 'spec' => {
70
- 'type' => type,
71
- 'config' => config
72
- }
73
- }
47
+ def language_tool(name, type:, config: {}, cluster: nil, cluster_ref: nil, labels: {})
48
+ build_resource('LanguageTool', name, {
49
+ 'type' => type,
50
+ 'config' => config
51
+ }, namespace: cluster, cluster_ref: cluster_ref, labels: labels)
74
52
  end
75
53
 
76
54
  # Build a LanguageModel resource
77
- def language_model(name, provider:, model:, endpoint: nil, cluster: nil, labels: {})
55
+ def language_model(name, provider:, model:, endpoint: nil, cluster: nil, cluster_ref: nil, labels: {})
78
56
  spec = {
79
57
  'provider' => provider,
80
58
  'modelName' => model
81
59
  }
82
60
  spec['endpoint'] = endpoint if endpoint
83
61
 
84
- {
85
- 'apiVersion' => 'langop.io/v1alpha1',
86
- 'kind' => 'LanguageModel',
87
- 'metadata' => {
88
- 'name' => name,
89
- 'namespace' => cluster || 'default',
90
- 'labels' => default_labels.merge(labels)
91
- },
92
- 'spec' => spec
93
- }
62
+ build_resource('LanguageModel', name, spec, namespace: cluster, cluster_ref: cluster_ref, labels: labels)
94
63
  end
95
64
 
96
65
  # Build a LanguagePersona resource
97
66
  def language_persona(name, description:, tone:, system_prompt:, cluster: nil, labels: {})
98
- {
99
- 'apiVersion' => 'langop.io/v1alpha1',
100
- 'kind' => 'LanguagePersona',
101
- 'metadata' => {
102
- 'name' => name,
103
- 'namespace' => cluster || 'default',
104
- 'labels' => default_labels.merge(labels)
105
- },
106
- 'spec' => {
107
- 'displayName' => name.split('-').map(&:capitalize).join(' '),
108
- 'description' => description,
109
- 'tone' => tone,
110
- 'systemPrompt' => system_prompt
111
- }
112
- }
67
+ build_resource('LanguagePersona', name, {
68
+ 'displayName' => name.split('-').map(&:capitalize).join(' '),
69
+ 'description' => description,
70
+ 'tone' => tone,
71
+ 'systemPrompt' => system_prompt
72
+ }, namespace: cluster, labels: labels)
113
73
  end
114
74
 
115
75
  # Build a LanguagePersona resource with full spec control
116
76
  def build_persona(name:, spec:, namespace: nil, labels: {})
117
- {
118
- 'apiVersion' => 'langop.io/v1alpha1',
119
- 'kind' => 'LanguagePersona',
120
- 'metadata' => {
121
- 'name' => name,
122
- 'namespace' => namespace || 'default',
123
- 'labels' => default_labels.merge(labels)
124
- },
125
- 'spec' => spec
126
- }
77
+ build_resource('LanguagePersona', name, spec, namespace: namespace, labels: labels)
127
78
  end
128
79
 
129
80
  # Build a Kubernetes Service resource for a reactive agent
@@ -141,16 +92,15 @@ module LanguageOperator
141
92
  'name' => agent_name,
142
93
  'namespace' => namespace || 'default',
143
94
  'labels' => default_labels.merge(
144
- 'app.kubernetes.io/name' => agent_name,
145
- 'app.kubernetes.io/component' => 'agent'
95
+ Constants::KubernetesLabels.agent_labels(agent_name)
146
96
  ).merge(labels)
147
97
  },
148
98
  'spec' => {
149
99
  'type' => 'ClusterIP',
150
- 'selector' => {
151
- 'app.kubernetes.io/name' => agent_name,
152
- 'app.kubernetes.io/component' => 'agent'
153
- },
100
+ 'selector' => Constants::KubernetesLabels.agent_labels(agent_name).slice(
101
+ Constants::KubernetesLabels::NAME,
102
+ Constants::KubernetesLabels::COMPONENT
103
+ ),
154
104
  'ports' => [
155
105
  {
156
106
  'name' => 'http',
@@ -165,10 +115,34 @@ module LanguageOperator
165
115
 
166
116
  private
167
117
 
118
+ # Build a standard language-operator Kubernetes resource
119
+ #
120
+ # @param kind [String] The Kubernetes resource kind
121
+ # @param name [String] The resource name
122
+ # @param spec [Hash] The resource spec
123
+ # @param namespace [String, nil] The namespace (defaults to 'default')
124
+ # @param cluster_ref [String, nil] The cluster reference for lifecycle management
125
+ # @param labels [Hash] Additional labels to merge with defaults
126
+ # @return [Hash] Complete Kubernetes resource manifest
127
+ def build_resource(kind, name, spec, namespace: nil, cluster_ref: nil, labels: {})
128
+ # Add clusterRef to spec if provided for proper lifecycle management
129
+ spec = spec.merge('clusterRef' => cluster_ref) if cluster_ref
130
+ {
131
+ 'apiVersion' => 'langop.io/v1alpha1',
132
+ 'kind' => kind,
133
+ 'metadata' => {
134
+ 'name' => name,
135
+ 'namespace' => namespace || 'default',
136
+ 'labels' => default_labels.merge(labels)
137
+ },
138
+ 'spec' => spec
139
+ }
140
+ end
141
+
168
142
  def default_labels
169
143
  {
170
- 'app.kubernetes.io/managed-by' => 'aictl',
171
- 'app.kubernetes.io/part-of' => 'language-operator'
144
+ Constants::KubernetesLabels::MANAGED_BY => Constants::KubernetesLabels::MANAGED_BY_AICTL,
145
+ Constants::KubernetesLabels::PART_OF => Constants::KubernetesLabels::PROJECT_NAME
172
146
  }
173
147
  end
174
148
 
@@ -2,7 +2,7 @@
2
2
  :openapi: 3.0.3
3
3
  :info:
4
4
  :title: Language Operator Agent API
5
- :version: 0.1.59
5
+ :version: 0.1.62
6
6
  :description: HTTP API endpoints exposed by Language Operator reactive agents
7
7
  :contact:
8
8
  :name: Language Operator
@@ -3,7 +3,7 @@
3
3
  "$id": "https://github.com/language-operator/language-operator-gem/schema/agent-dsl.json",
4
4
  "title": "Language Operator Agent DSL",
5
5
  "description": "Schema for defining autonomous AI agents using the Language Operator DSL",
6
- "version": "0.1.59",
6
+ "version": "0.1.62",
7
7
  "type": "object",
8
8
  "properties": {
9
9
  "name": {
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'lru_redux'
4
+
3
5
  module LanguageOperator
4
6
  # Type coercion system for task inputs and outputs
5
7
  #
@@ -7,6 +9,12 @@ module LanguageOperator
7
9
  # and clear error messages when coercion is not possible. This enables
8
10
  # flexible type handling while maintaining type safety.
9
11
  #
12
+ # Performance optimizations:
13
+ # - Fast-path checks for already-correct types
14
+ # - Bounded LRU memoization cache for expensive string coercions (prevents memory leaks)
15
+ # - Pre-compiled regexes for boolean parsing
16
+ # - Thread-safe cache operations with mutex protection
17
+ #
10
18
  # Supported types:
11
19
  # - integer: Coerces String, Integer, Float to Integer
12
20
  # - number: Coerces String, Integer, Float to Float
@@ -35,41 +43,113 @@ module LanguageOperator
35
43
  # TypeCoercion.coerce([1, 2], "array") # => [1, 2]
36
44
  # TypeCoercion.coerce({a: 1}, "array") # raises ArgumentError
37
45
  module TypeCoercion
38
- # Coerce a value to the specified type
39
- #
40
- # @param value [Object] Value to coerce
41
- # @param type [String] Target type (see COERCION_RULES for valid types)
42
- # @return [Object] Coerced value
43
- # @raise [ArgumentError] If coercion fails or type is unknown
44
- #
45
- # @example
46
- # TypeCoercion.coerce("345", "integer") # => 345
47
- # TypeCoercion.coerce("true", "boolean") # => true
48
- def self.coerce(value, type)
49
- case type
50
- when 'integer'
51
- coerce_integer(value)
52
- when 'number'
53
- coerce_number(value)
54
- when 'string'
55
- coerce_string(value)
56
- when 'boolean'
57
- coerce_boolean(value)
58
- when 'array'
59
- validate_array(value)
60
- when 'hash'
61
- validate_hash(value)
62
- when 'any'
63
- value
64
- else
65
- raise ArgumentError, "Unknown type: #{type}"
46
+ # Memory-safe LRU cache for expensive coercions with bounded size to prevent memory leaks
47
+ # - Cache automatically evicts least recently used entries when limit is reached
48
+ # - Default cache size: 1000 entries (configurable via TYPE_COERCION_CACHE_SIZE environment variable)
49
+ # - Thread-safe operations protected by mutex
50
+ # - Caches both successful and failed coercion attempts to avoid repeated expensive operations
51
+ DEFAULT_CACHE_SIZE = 1000
52
+ @cache_size = ENV.fetch('TYPE_COERCION_CACHE_SIZE', DEFAULT_CACHE_SIZE).to_i
53
+ @coercion_cache = LruRedux::Cache.new(@cache_size)
54
+ @cache_mutex = Mutex.new
55
+ @cache_hits = 0
56
+ @cache_misses = 0
57
+
58
+ # Boolean patterns - pre-compiled for performance
59
+ TRUTHY_PATTERNS = %w[true 1 yes t y].freeze
60
+ FALSY_PATTERNS = %w[false 0 no f n].freeze
61
+
62
+ class << self
63
+ # Get current cache size limit
64
+ attr_reader :cache_size
65
+
66
+ # Coerce a value to the specified type
67
+ #
68
+ # @param value [Object] Value to coerce
69
+ # @param type [String] Target type (see COERCION_RULES for valid types)
70
+ # @return [Object] Coerced value
71
+ # @raise [ArgumentError] If coercion fails or type is unknown
72
+ #
73
+ # @example
74
+ # TypeCoercion.coerce("345", "integer") # => 345
75
+ # TypeCoercion.coerce("true", "boolean") # => true
76
+ def coerce(value, type)
77
+ # Fast path - check cache first for expensive string coercions
78
+ if value.is_a?(String) && %w[integer number boolean].include?(type)
79
+ cache_key = [value, type]
80
+ cached = @cache_mutex.synchronize { @coercion_cache[cache_key] }
81
+ if cached
82
+ @cache_hits += 1
83
+ return cached[:result] if cached[:success]
84
+
85
+ raise ArgumentError, cached[:error_message]
86
+ end
87
+ @cache_misses += 1
88
+ end
89
+
90
+ # Perform coercion
91
+ result = case type
92
+ when 'integer'
93
+ coerce_integer(value)
94
+ when 'number'
95
+ coerce_number(value)
96
+ when 'string'
97
+ coerce_string(value)
98
+ when 'boolean'
99
+ coerce_boolean(value)
100
+ when 'array'
101
+ validate_array(value)
102
+ when 'hash'
103
+ validate_hash(value)
104
+ when 'any'
105
+ value
106
+ else
107
+ raise ArgumentError, "Unknown type: #{type}"
108
+ end
109
+
110
+ # Cache successful string coercion results
111
+ if value.is_a?(String) && %w[integer number boolean].include?(type)
112
+ cache_entry = { success: true, result: result }
113
+ @cache_mutex.synchronize { @coercion_cache[[value, type]] = cache_entry }
114
+ end
115
+
116
+ result
117
+ rescue ArgumentError => e
118
+ # Cache failed coercion attempts to avoid repeating expensive failures
119
+ if value.is_a?(String) && %w[integer number boolean].include?(type)
120
+ cache_entry = { success: false, error_message: e.message }
121
+ @cache_mutex.synchronize { @coercion_cache[[value, type]] = cache_entry }
122
+ end
123
+ raise
124
+ end
125
+
126
+ # Get cache statistics for monitoring
127
+ def cache_stats
128
+ @cache_mutex.synchronize do
129
+ {
130
+ size: @coercion_cache.count,
131
+ max_size: @cache_size,
132
+ hits: @cache_hits,
133
+ misses: @cache_misses,
134
+ hit_rate: @cache_hits.zero? ? 0.0 : @cache_hits.to_f / (@cache_hits + @cache_misses)
135
+ }
136
+ end
137
+ end
138
+
139
+ # Clear the cache (for testing or memory management)
140
+ def clear_cache
141
+ @cache_mutex.synchronize do
142
+ @coercion_cache.clear
143
+ @cache_hits = 0
144
+ @cache_misses = 0
145
+ end
66
146
  end
67
147
  end
68
148
 
69
149
  # Coerce value to Integer
70
150
  #
71
151
  # Accepts: String, Integer, Float
72
- # Coercion: Uses Ruby's Integer() method
152
+ # Coercion: Uses Ruby's Integer() method with fast-path optimization
73
153
  # Errors: Cannot parse as integer
74
154
  #
75
155
  # @param value [Object] Value to coerce
@@ -82,6 +162,7 @@ module LanguageOperator
82
162
  # coerce_integer(123.0) # => 123
83
163
  # coerce_integer("abc") # raises ArgumentError
84
164
  def self.coerce_integer(value)
165
+ # Fast path for already-correct types
85
166
  return value if value.is_a?(Integer)
86
167
 
87
168
  Integer(value)
@@ -92,7 +173,7 @@ module LanguageOperator
92
173
  # Coerce value to Float (number)
93
174
  #
94
175
  # Accepts: String, Integer, Float
95
- # Coercion: Uses Ruby's Float() method
176
+ # Coercion: Uses Ruby's Float() method with fast-path optimization
96
177
  # Errors: Cannot parse as number
97
178
  #
98
179
  # @param value [Object] Value to coerce
@@ -105,7 +186,8 @@ module LanguageOperator
105
186
  # coerce_number(3.14) # => 3.14
106
187
  # coerce_number("not a num") # raises ArgumentError
107
188
  def self.coerce_number(value)
108
- return value if value.is_a?(Numeric)
189
+ # Fast path for already-correct types
190
+ return value.to_f if value.is_a?(Numeric)
109
191
 
110
192
  Float(value)
111
193
  rescue ArgumentError, TypeError => e
@@ -132,7 +214,7 @@ module LanguageOperator
132
214
  # Coerce value to Boolean
133
215
  #
134
216
  # Accepts: Boolean, String (explicit values only)
135
- # Coercion: Case-insensitive string matching
217
+ # Coercion: Case-insensitive string matching with optimized pattern lookup
136
218
  # Truthy: "true", "1", "yes", "t", "y"
137
219
  # Falsy: "false", "0", "no", "f", "n"
138
220
  # Errors: Ambiguous values (e.g., "maybe", "unknown")
@@ -152,14 +234,16 @@ module LanguageOperator
152
234
  # coerce_boolean("no") # => false
153
235
  # coerce_boolean("maybe") # raises ArgumentError
154
236
  def self.coerce_boolean(value)
237
+ # Fast path for already-correct types
155
238
  return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
156
239
 
157
240
  # Only allow string values for coercion (not integers or other types)
158
241
  raise ArgumentError, "Cannot coerce #{value.inspect} to boolean" unless value.is_a?(String)
159
242
 
243
+ # Optimized pattern matching using pre-compiled arrays
160
244
  str = value.strip.downcase
161
- return true if %w[true 1 yes t y].include?(str)
162
- return false if %w[false 0 no f n].include?(str)
245
+ return true if TRUTHY_PATTERNS.include?(str)
246
+ return false if FALSY_PATTERNS.include?(str)
163
247
 
164
248
  raise ArgumentError, "Cannot coerce #{value.inspect} to boolean"
165
249
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module LanguageOperator
6
+ module Utils
7
+ # Secure path utilities to prevent path traversal attacks
8
+ # when expanding user home directory paths
9
+ class SecurePath
10
+ # Default fallback directory for untrusted HOME environments
11
+ DEFAULT_HOME = '/tmp'
12
+
13
+ class << self
14
+ # Securely expand a path relative to user home directory
15
+ # Prevents path traversal attacks via malicious HOME environment variable
16
+ #
17
+ # @param relative_path [String] Path relative to home (e.g., '.kube/config')
18
+ # @return [String] Safe absolute path
19
+ def expand_home_path(relative_path)
20
+ home_dir = secure_home_directory
21
+ File.join(home_dir, relative_path)
22
+ end
23
+
24
+ # Get user home directory with security validation
25
+ # Falls back to safe default if HOME environment variable is suspicious
26
+ #
27
+ # @return [String] Validated home directory path
28
+ def secure_home_directory
29
+ home = ENV.fetch('HOME', DEFAULT_HOME)
30
+
31
+ # Validate HOME is safe to use
32
+ return DEFAULT_HOME unless home_directory_safe?(home)
33
+
34
+ home
35
+ end
36
+
37
+ private
38
+
39
+ # Validate that a home directory path is safe to use
40
+ # Prevents path traversal and access to sensitive system directories
41
+ #
42
+ # @param path [String] Home directory path to validate
43
+ # @return [Boolean] true if safe to use
44
+ def home_directory_safe?(path)
45
+ # Basic safety checks
46
+ return false if path.nil? || path.empty?
47
+ return false if path.include?('../') # Path traversal
48
+ return false if path.include?('/..') # Path traversal
49
+ return false unless Pathname.new(path).absolute? # Must be absolute
50
+
51
+ # Dangerous system paths
52
+ dangerous_prefixes = [
53
+ '/etc', # System configuration
54
+ '/proc', # Process information
55
+ '/sys', # System information
56
+ '/dev', # Device files
57
+ '/boot', # Boot files
58
+ '/root' # Root user home (should use /root/.kube not accessible via ~)
59
+ ]
60
+
61
+ dangerous_prefixes.each do |prefix|
62
+ return false if path.start_with?(prefix)
63
+ end
64
+
65
+ # Directory must exist and be readable
66
+ File.directory?(path) && File.readable?(path)
67
+ rescue SystemCallError
68
+ # If we can't check the directory, assume it's unsafe
69
+ false
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LanguageOperator
4
+ # Utility modules for common functionality
5
+ module Utils
6
+ end
7
+ end