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.
- 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 +14 -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 +369 -68
- 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 +31 -1
- 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/task_definition.rb +7 -6
- 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 +3 -3
- 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 -147
- data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -218
- data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -432
- data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -236
- data/lib/language_operator/learning/optimizer.rb +0 -318
- data/lib/language_operator/learning/pattern_detector.rb +0 -260
- data/lib/language_operator/learning/task_synthesizer.rb +0 -261
- data/lib/language_operator/learning/trace_analyzer.rb +0 -280
- data/lib/language_operator/templates/task_synthesis.tmpl +0 -97
- 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
|
@@ -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',
|
|
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
|