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
@@ -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.61
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.61",
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
4
+
3
5
  module LanguageOperator
4
6
  # Common parameter validation utilities
5
7
  #
@@ -157,12 +159,62 @@ module LanguageOperator
157
159
  # @example Invalid paths
158
160
  # Validators.safe_path('') # => "Error: Path cannot be empty"
159
161
  # Validators.safe_path('../../../etc/passwd') # => "Error: Path contains..."
162
+ # Validators.safe_path('%2e%2e%2fetc%2fpasswd') # => "Error: Path contains..."
160
163
  # Validators.safe_path("file\0name") # => "Error: Path contains..."
161
164
  def self.safe_path(path)
162
165
  return 'Error: Path cannot be empty' if path.nil? || path.strip.empty?
163
166
 
164
- # Check for obvious traversal attempts
165
- return 'Error: Path contains invalid characters or directory traversal' if path.include?('..') || path.include?("\0")
167
+ # Check for null bytes first (before any decoding)
168
+ return 'Error: Path contains invalid characters or directory traversal' if path.include?("\0")
169
+
170
+ begin
171
+ # Decode URL-encoded paths to catch encoded traversal attempts
172
+ # Handle multiple layers of encoding by repeatedly decoding
173
+ decoded_path = path
174
+ 3.times do # Limit to prevent infinite loops
175
+ new_decoded = URI::DEFAULT_PARSER.unescape(decoded_path)
176
+ break if new_decoded == decoded_path # No more changes
177
+
178
+ decoded_path = new_decoded
179
+ rescue ArgumentError, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
180
+ # Invalid byte sequence - treat as potential attack
181
+ return 'Error: Path contains invalid characters or directory traversal'
182
+ end
183
+
184
+ # Check for directory traversal in both original and decoded paths
185
+ [path, decoded_path].each do |check_path|
186
+ # Check for obvious traversal patterns
187
+ return 'Error: Path contains invalid characters or directory traversal' if check_path.include?('..')
188
+
189
+ # Check for overlong UTF-8 sequences that decode to dangerous characters
190
+ # These are common in directory traversal attacks
191
+ if check_path.include?("\xC0\xAE") || check_path.include?("\xC0\xAF") ||
192
+ check_path.bytes.each_cons(2).any? { |a, b| a == 0xC0 && (0x80..0xBF).cover?(b) }
193
+ return 'Error: Path contains invalid characters or directory traversal'
194
+ end
195
+
196
+ # Canonicalize path to detect complex traversal attempts
197
+ begin
198
+ # Use current directory as base for relative paths
199
+ canonical_path = File.expand_path(check_path, Dir.pwd)
200
+
201
+ # For relative paths, ensure they don't escape current directory
202
+ unless check_path.start_with?('/')
203
+ current_dir_canonical = File.expand_path(Dir.pwd)
204
+ return 'Error: Path contains invalid characters or directory traversal' unless canonical_path.start_with?(current_dir_canonical)
205
+ end
206
+
207
+ # Additional check: ensure canonical path doesn't contain dangerous patterns
208
+ return 'Error: Path contains invalid characters or directory traversal' if canonical_path.include?('/../') || canonical_path.end_with?('/..')
209
+ rescue ArgumentError, Errno::ENOENT
210
+ # Path canonicalization failed, likely due to invalid characters
211
+ return 'Error: Path contains invalid characters or directory traversal'
212
+ end
213
+ end
214
+ rescue URI::InvalidURIError
215
+ # URL decoding failed, path might contain invalid sequences
216
+ return 'Error: Path contains invalid characters or directory traversal'
217
+ end
166
218
 
167
219
  nil
168
220
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LanguageOperator
4
- VERSION = '0.1.61'
4
+ VERSION = '0.1.62'
5
5
  end