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.
- checksums.yaml +4 -4
- data/.claude/commands/persona.md +9 -0
- data/.claude/commands/task.md +46 -1
- data/.rubocop.yml +13 -0
- data/.rubocop_custom/use_ux_helper.rb +44 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +12 -1
- data/Makefile +26 -7
- data/Makefile.common +50 -0
- data/bin/aictl +8 -1
- data/components/agent/Gemfile +1 -1
- data/components/agent/bin/langop-agent +7 -0
- data/docs/README.md +58 -0
- data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
- data/docs/cli-reference.md +274 -0
- data/docs/{dsl/constraints.md → constraints.md} +5 -5
- data/docs/how-agents-work.md +156 -0
- data/docs/installation.md +218 -0
- data/docs/quickstart.md +299 -0
- data/docs/understanding-generated-code.md +265 -0
- data/docs/using-tools.md +457 -0
- data/docs/webhooks.md +509 -0
- data/examples/ux_helpers_demo.rb +296 -0
- data/lib/language_operator/agent/base.rb +11 -1
- data/lib/language_operator/agent/executor.rb +23 -6
- data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
- data/lib/language_operator/agent/task_executor.rb +346 -63
- data/lib/language_operator/agent/web_server.rb +110 -14
- data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
- data/lib/language_operator/agent.rb +88 -2
- data/lib/language_operator/cli/base_command.rb +17 -11
- data/lib/language_operator/cli/command_loader.rb +72 -0
- data/lib/language_operator/cli/commands/agent/base.rb +837 -0
- data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
- data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
- data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
- data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
- data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
- data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
- data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
- data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
- data/lib/language_operator/cli/commands/cluster.rb +129 -84
- data/lib/language_operator/cli/commands/install.rb +1 -1
- data/lib/language_operator/cli/commands/model/base.rb +215 -0
- data/lib/language_operator/cli/commands/model/test.rb +165 -0
- data/lib/language_operator/cli/commands/persona.rb +16 -34
- data/lib/language_operator/cli/commands/quickstart.rb +3 -2
- data/lib/language_operator/cli/commands/status.rb +40 -67
- data/lib/language_operator/cli/commands/system/base.rb +44 -0
- data/lib/language_operator/cli/commands/system/exec.rb +147 -0
- data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
- data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
- data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
- data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
- data/lib/language_operator/cli/commands/system/schema.rb +92 -0
- data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
- data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
- data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
- data/lib/language_operator/cli/commands/tool/base.rb +271 -0
- data/lib/language_operator/cli/commands/tool/install.rb +255 -0
- data/lib/language_operator/cli/commands/tool/search.rb +69 -0
- data/lib/language_operator/cli/commands/tool/test.rb +115 -0
- data/lib/language_operator/cli/commands/use.rb +29 -6
- data/lib/language_operator/cli/errors/handler.rb +20 -17
- data/lib/language_operator/cli/errors/suggestions.rb +3 -5
- data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
- data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
- data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
- data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
- data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
- data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
- data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
- data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
- data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
- data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
- data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
- data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
- data/lib/language_operator/cli/main.rb +50 -40
- data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
- data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
- data/lib/language_operator/client/base.rb +28 -0
- data/lib/language_operator/client/config.rb +4 -1
- data/lib/language_operator/client/mcp_connector.rb +1 -1
- data/lib/language_operator/config/cluster_config.rb +3 -2
- data/lib/language_operator/config.rb +38 -11
- data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
- data/lib/language_operator/constants.rb +13 -0
- data/lib/language_operator/dsl/http.rb +127 -10
- data/lib/language_operator/dsl.rb +153 -6
- data/lib/language_operator/errors.rb +50 -0
- data/lib/language_operator/kubernetes/client.rb +11 -6
- data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/type_coercion.rb +118 -34
- data/lib/language_operator/utils/secure_path.rb +74 -0
- data/lib/language_operator/utils.rb +7 -0
- data/lib/language_operator/validators.rb +54 -2
- data/lib/language_operator/version.rb +1 -1
- data/synth/001/Makefile +10 -2
- data/synth/001/agent.rb +16 -15
- data/synth/001/output.log +27 -10
- data/synth/002/Makefile +10 -2
- data/synth/003/Makefile +1 -1
- data/synth/003/README.md +205 -133
- data/synth/003/agent.optimized.rb +66 -0
- data/synth/003/agent.synthesized.rb +41 -0
- metadata +111 -35
- data/docs/dsl/agent-reference.md +0 -604
- data/docs/dsl/mcp-integration.md +0 -1177
- data/docs/dsl/webhooks.md +0 -932
- data/docs/dsl/workflows.md +0 -744
- data/lib/language_operator/cli/commands/agent.rb +0 -1712
- data/lib/language_operator/cli/commands/model.rb +0 -366
- data/lib/language_operator/cli/commands/system.rb +0 -1259
- data/lib/language_operator/cli/commands/tool.rb +0 -654
- data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
- data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
- data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
- data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
- data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
- data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
- data/lib/language_operator/learning/optimizer.rb +0 -319
- data/lib/language_operator/learning/pattern_detector.rb +0 -260
- data/lib/language_operator/learning/task_synthesizer.rb +0 -288
- data/lib/language_operator/learning/trace_analyzer.rb +0 -285
- data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
- data/lib/language_operator/ux/base.rb +0 -81
- data/lib/language_operator/ux/concerns/README.md +0 -155
- data/lib/language_operator/ux/concerns/headings.rb +0 -90
- data/lib/language_operator/ux/create_agent.rb +0 -255
- data/lib/language_operator/ux/create_model.rb +0 -267
- data/lib/language_operator/ux/quickstart.rb +0 -594
- data/synth/003/agent.rb +0 -41
- data/synth/003/output.log +0 -68
- /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
- /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
- /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',
|
|
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
|
|
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(
|
|
175
|
-
deployment.dig('metadata', 'labels',
|
|
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
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
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
|
-
'
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
|
|
@@ -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.
|
|
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
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
162
|
-
return false if
|
|
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
|
|
@@ -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
|
|
165
|
-
return 'Error: Path contains invalid characters or directory traversal' if path.include?(
|
|
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
|