language-operator 0.0.1 → 0.1.31
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/.rubocop.yml +125 -0
- data/CHANGELOG.md +88 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +229 -21
- data/Makefile +82 -0
- data/README.md +3 -11
- data/Rakefile +63 -0
- data/bin/aictl +7 -0
- data/completions/_aictl +232 -0
- data/completions/aictl.bash +121 -0
- data/completions/aictl.fish +114 -0
- data/docs/architecture/agent-runtime.md +585 -0
- data/docs/dsl/SCHEMA_VERSION.md +250 -0
- data/docs/dsl/agent-reference.md +604 -0
- data/docs/dsl/best-practices.md +1078 -0
- data/docs/dsl/chat-endpoints.md +895 -0
- data/docs/dsl/constraints.md +671 -0
- data/docs/dsl/mcp-integration.md +1177 -0
- data/docs/dsl/webhooks.md +932 -0
- data/docs/dsl/workflows.md +744 -0
- data/lib/language_operator/agent/base.rb +110 -0
- data/lib/language_operator/agent/executor.rb +440 -0
- data/lib/language_operator/agent/instrumentation.rb +54 -0
- data/lib/language_operator/agent/metrics_tracker.rb +183 -0
- data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
- data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
- data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
- data/lib/language_operator/agent/safety/content_filter.rb +93 -0
- data/lib/language_operator/agent/safety/manager.rb +207 -0
- data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
- data/lib/language_operator/agent/safety/safe_executor.rb +127 -0
- data/lib/language_operator/agent/scheduler.rb +183 -0
- data/lib/language_operator/agent/telemetry.rb +116 -0
- data/lib/language_operator/agent/web_server.rb +610 -0
- data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
- data/lib/language_operator/agent.rb +149 -0
- data/lib/language_operator/cli/commands/agent.rb +1205 -0
- data/lib/language_operator/cli/commands/cluster.rb +371 -0
- data/lib/language_operator/cli/commands/install.rb +404 -0
- data/lib/language_operator/cli/commands/model.rb +266 -0
- data/lib/language_operator/cli/commands/persona.rb +393 -0
- data/lib/language_operator/cli/commands/quickstart.rb +22 -0
- data/lib/language_operator/cli/commands/status.rb +143 -0
- data/lib/language_operator/cli/commands/system.rb +772 -0
- data/lib/language_operator/cli/commands/tool.rb +537 -0
- data/lib/language_operator/cli/commands/use.rb +47 -0
- data/lib/language_operator/cli/errors/handler.rb +180 -0
- data/lib/language_operator/cli/errors/suggestions.rb +176 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +77 -0
- data/lib/language_operator/cli/formatters/log_formatter.rb +288 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +49 -0
- data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +163 -0
- data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
- data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
- data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
- data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
- data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
- data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
- data/lib/language_operator/cli/main.rb +236 -0
- data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
- data/lib/language_operator/client/base.rb +214 -0
- data/lib/language_operator/client/config.rb +136 -0
- data/lib/language_operator/client/cost_calculator.rb +37 -0
- data/lib/language_operator/client/mcp_connector.rb +123 -0
- data/lib/language_operator/client.rb +19 -0
- data/lib/language_operator/config/cluster_config.rb +101 -0
- data/lib/language_operator/config/tool_patterns.yaml +57 -0
- data/lib/language_operator/config/tool_registry.rb +96 -0
- data/lib/language_operator/config.rb +138 -0
- data/lib/language_operator/dsl/adapter.rb +124 -0
- data/lib/language_operator/dsl/agent_context.rb +90 -0
- data/lib/language_operator/dsl/agent_definition.rb +427 -0
- data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
- data/lib/language_operator/dsl/config.rb +119 -0
- data/lib/language_operator/dsl/context.rb +50 -0
- data/lib/language_operator/dsl/execution_context.rb +47 -0
- data/lib/language_operator/dsl/helpers.rb +109 -0
- data/lib/language_operator/dsl/http.rb +184 -0
- data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
- data/lib/language_operator/dsl/parameter_definition.rb +124 -0
- data/lib/language_operator/dsl/registry.rb +36 -0
- data/lib/language_operator/dsl/schema.rb +1102 -0
- data/lib/language_operator/dsl/shell.rb +125 -0
- data/lib/language_operator/dsl/tool_definition.rb +112 -0
- data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
- data/lib/language_operator/dsl/webhook_definition.rb +106 -0
- data/lib/language_operator/dsl/workflow_definition.rb +259 -0
- data/lib/language_operator/dsl.rb +161 -0
- data/lib/language_operator/errors.rb +60 -0
- data/lib/language_operator/kubernetes/client.rb +279 -0
- data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
- data/lib/language_operator/loggable.rb +47 -0
- data/lib/language_operator/logger.rb +141 -0
- data/lib/language_operator/retry.rb +123 -0
- data/lib/language_operator/retryable.rb +132 -0
- data/lib/language_operator/templates/README.md +23 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -0
- data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
- data/lib/language_operator/templates/schema/.gitkeep +0 -0
- data/lib/language_operator/templates/schema/CHANGELOG.md +93 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -0
- data/lib/language_operator/tool_loader.rb +242 -0
- data/lib/language_operator/validators.rb +170 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +65 -3
- data/requirements/tasks/challenge.md +9 -0
- data/requirements/tasks/iterate.md +36 -0
- data/requirements/tasks/optimize.md +21 -0
- data/requirements/tasks/tag.md +5 -0
- data/test_agent_dsl.rb +108 -0
- metadata +507 -20
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'timeout'
|
|
6
|
+
|
|
7
|
+
module LanguageOperator
|
|
8
|
+
module Dsl
|
|
9
|
+
# Safe shell command execution for MCP tools
|
|
10
|
+
#
|
|
11
|
+
# Provides methods for executing shell commands safely with automatic
|
|
12
|
+
# argument escaping to prevent injection attacks. All methods are class methods.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# result = Shell.run('ls', '-la', '/tmp')
|
|
16
|
+
# if result[:success]
|
|
17
|
+
# puts result[:output]
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Safe user input
|
|
21
|
+
# # User input is automatically escaped
|
|
22
|
+
# result = Shell.run('grep', user_input, '/etc/hosts')
|
|
23
|
+
module Shell
|
|
24
|
+
# Run a shell command with properly escaped arguments
|
|
25
|
+
#
|
|
26
|
+
# This is safer than using backticks as it prevents shell injection.
|
|
27
|
+
# Arguments are automatically escaped using Shellwords.
|
|
28
|
+
#
|
|
29
|
+
# @param cmd [String] Command to execute
|
|
30
|
+
# @param args [Array<String>] Arguments (will be escaped)
|
|
31
|
+
# @param env [Hash] Environment variables to set
|
|
32
|
+
# @param chdir [String, nil] Working directory
|
|
33
|
+
# @param timeout [Integer] Timeout in seconds (default: 30)
|
|
34
|
+
# @return [Hash] Result with :success, :output, :error, :exitcode, :timeout keys
|
|
35
|
+
def self.run(cmd, *args, env: {}, chdir: nil, timeout: 30)
|
|
36
|
+
# Escape all arguments
|
|
37
|
+
escaped_args = args.map { |arg| Shellwords.escape(arg.to_s) }
|
|
38
|
+
full_cmd = "#{cmd} #{escaped_args.join(' ')}"
|
|
39
|
+
|
|
40
|
+
# Execute with timeout
|
|
41
|
+
stdout = nil
|
|
42
|
+
stderr = nil
|
|
43
|
+
status = nil
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
Timeout.timeout(timeout) do
|
|
47
|
+
stdout, stderr, status = Open3.capture3(env, full_cmd, chdir: chdir)
|
|
48
|
+
end
|
|
49
|
+
rescue Timeout::Error
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
output: '',
|
|
53
|
+
error: "Command timed out after #{timeout} seconds",
|
|
54
|
+
exitcode: -1,
|
|
55
|
+
timeout: true
|
|
56
|
+
}
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
output: '',
|
|
61
|
+
error: e.message,
|
|
62
|
+
exitcode: -1
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
success: status.success?,
|
|
68
|
+
output: stdout,
|
|
69
|
+
error: stderr,
|
|
70
|
+
exitcode: status.exitstatus,
|
|
71
|
+
timeout: false
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Run a command and return only stdout (like backticks)
|
|
76
|
+
# Returns nil if the command fails
|
|
77
|
+
def self.capture(cmd, *, **)
|
|
78
|
+
result = run(cmd, *, **)
|
|
79
|
+
result[:success] ? result[:output] : nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Run a command and return stdout, raising on failure
|
|
83
|
+
def self.capture!(cmd, *, **)
|
|
84
|
+
result = run(cmd, *, **)
|
|
85
|
+
raise "Command failed (exit #{result[:exitcode]}): #{result[:error]}" unless result[:success]
|
|
86
|
+
|
|
87
|
+
result[:output]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if a command exists in PATH
|
|
91
|
+
def self.command_exists?(cmd)
|
|
92
|
+
result = run('which', cmd)
|
|
93
|
+
result[:success]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Run a command in the background and return immediately
|
|
97
|
+
# @deprecated This method has been removed for security reasons
|
|
98
|
+
# @raise [SecurityError] Always raises an error
|
|
99
|
+
def self.spawn(_cmd, *_args, env: {}, chdir: nil)
|
|
100
|
+
raise SecurityError, 'Shell.spawn has been removed for security reasons. Background process execution is not allowed in synthesized code.'
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Execute raw shell command (REMOVED FOR SECURITY)
|
|
104
|
+
# This method has been removed as it allowed arbitrary shell execution
|
|
105
|
+
# with pipes, redirects, etc. which is a security risk in synthesized code.
|
|
106
|
+
# @deprecated This method has been removed for security reasons
|
|
107
|
+
# @raise [SecurityError] Always raises an error
|
|
108
|
+
def self.raw(_command, env: {}, chdir: nil, timeout: 30)
|
|
109
|
+
raise SecurityError, 'Shell.raw has been removed for security reasons. Use Shell.run with explicit arguments instead.'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Safely build a command string with escaped arguments
|
|
113
|
+
# Useful when you need to construct a command but not execute it yet
|
|
114
|
+
def self.build(cmd, *args)
|
|
115
|
+
escaped_args = args.map { |arg| Shellwords.escape(arg.to_s) }
|
|
116
|
+
"#{cmd} #{escaped_args.join(' ')}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Escape a single argument for shell usage
|
|
120
|
+
def self.escape(arg)
|
|
121
|
+
Shellwords.escape(arg.to_s)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'parameter_definition'
|
|
4
|
+
require_relative 'http'
|
|
5
|
+
require_relative 'shell'
|
|
6
|
+
require_relative 'helpers'
|
|
7
|
+
|
|
8
|
+
module LanguageOperator
|
|
9
|
+
module Dsl
|
|
10
|
+
# Tool definition for MCP tools
|
|
11
|
+
#
|
|
12
|
+
# Defines an MCP tool with parameters, description, and execution logic.
|
|
13
|
+
# Used within the DSL to create tools that can be registered and served.
|
|
14
|
+
#
|
|
15
|
+
# @example Define a simple tool
|
|
16
|
+
# tool "greet" do
|
|
17
|
+
# description "Greet a user by name"
|
|
18
|
+
#
|
|
19
|
+
# parameter :name do
|
|
20
|
+
# type :string
|
|
21
|
+
# required true
|
|
22
|
+
# description "Name to greet"
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# execute do |params|
|
|
26
|
+
# "Hello, #{params['name']}!"
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
class ToolDefinition
|
|
30
|
+
include LanguageOperator::Dsl::Helpers
|
|
31
|
+
|
|
32
|
+
# Provide access to HTTP and Shell helper classes as constants
|
|
33
|
+
HTTP = LanguageOperator::Dsl::HTTP
|
|
34
|
+
Shell = LanguageOperator::Dsl::Shell
|
|
35
|
+
|
|
36
|
+
attr_reader :name, :parameters, :execute_block
|
|
37
|
+
|
|
38
|
+
def initialize(name)
|
|
39
|
+
@name = name
|
|
40
|
+
@parameters = {}
|
|
41
|
+
@execute_block = nil
|
|
42
|
+
@description = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def description(val = nil)
|
|
46
|
+
return @description if val.nil?
|
|
47
|
+
|
|
48
|
+
@description = val
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parameter(name, &)
|
|
52
|
+
param = ParameterDefinition.new(name)
|
|
53
|
+
param.instance_eval(&) if block_given?
|
|
54
|
+
@parameters[name.to_s] = param
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def execute(&block)
|
|
58
|
+
@execute_block = block
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def call(params)
|
|
62
|
+
log_debug "Calling tool '#{@name}' with params: #{params.inspect}"
|
|
63
|
+
|
|
64
|
+
# Apply default values for missing optional parameters
|
|
65
|
+
@parameters.each do |name, param_def|
|
|
66
|
+
default_value = param_def.instance_variable_get(:@default)
|
|
67
|
+
params[name] = default_value if !params.key?(name) && !default_value.nil?
|
|
68
|
+
|
|
69
|
+
# Validate required parameters
|
|
70
|
+
raise ArgumentError, "Missing required parameter: #{name}" if param_def.required? && !params.key?(name)
|
|
71
|
+
|
|
72
|
+
# Validate parameter format if validator is set and value is present
|
|
73
|
+
if params.key?(name)
|
|
74
|
+
error = param_def.validate_value(params[name])
|
|
75
|
+
raise ArgumentError, error if error
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Call the execute block with parameters
|
|
80
|
+
result = @execute_block.call(params) if @execute_block
|
|
81
|
+
|
|
82
|
+
log_debug "Tool '#{@name}' completed: #{truncate_for_log(result)}"
|
|
83
|
+
result
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_schema
|
|
87
|
+
{
|
|
88
|
+
'name' => @name,
|
|
89
|
+
'description' => @description,
|
|
90
|
+
'inputSchema' => {
|
|
91
|
+
'type' => 'object',
|
|
92
|
+
'properties' => @parameters.transform_values(&:to_schema),
|
|
93
|
+
'required' => @parameters.select { |_, p| p.required? }.keys
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def log_debug(message)
|
|
101
|
+
puts "[DEBUG] #{message}" if ENV['DEBUG'] || ENV['MCP_DEBUG']
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def truncate_for_log(text)
|
|
105
|
+
return text.inspect if text.nil?
|
|
106
|
+
|
|
107
|
+
str = text.to_s
|
|
108
|
+
str.length > 100 ? "#{str[0..100]}..." : str
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module Dsl
|
|
5
|
+
# Defines authentication configuration for webhooks
|
|
6
|
+
class WebhookAuthentication
|
|
7
|
+
attr_reader :type, :config
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@type = nil
|
|
11
|
+
@config = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Verify HMAC signature (GitHub/Stripe style)
|
|
15
|
+
# @param header [String] Header name containing the signature
|
|
16
|
+
# @param secret [String] Secret key for HMAC
|
|
17
|
+
# @param algorithm [Symbol] Hash algorithm (:sha1, :sha256, :sha512)
|
|
18
|
+
# @param prefix [String, nil] Optional prefix to strip from signature (e.g., "sha256=")
|
|
19
|
+
def verify_signature(header:, secret:, algorithm: :sha256, prefix: nil)
|
|
20
|
+
if @current_methods
|
|
21
|
+
# Inside any_of/all_of - create child auth object
|
|
22
|
+
auth = WebhookAuthentication.new
|
|
23
|
+
auth.verify_signature(header: header, secret: secret, algorithm: algorithm, prefix: prefix)
|
|
24
|
+
@current_methods << auth
|
|
25
|
+
else
|
|
26
|
+
@type = :signature
|
|
27
|
+
@config[:header] = header
|
|
28
|
+
@config[:secret] = secret
|
|
29
|
+
@config[:algorithm] = algorithm
|
|
30
|
+
@config[:prefix] = prefix
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Verify API key from header
|
|
35
|
+
# @param header [String] Header name containing the API key
|
|
36
|
+
# @param key [String] Expected API key value
|
|
37
|
+
def verify_api_key(header:, key:)
|
|
38
|
+
if @current_methods
|
|
39
|
+
auth = WebhookAuthentication.new
|
|
40
|
+
auth.verify_api_key(header: header, key: key)
|
|
41
|
+
@current_methods << auth
|
|
42
|
+
else
|
|
43
|
+
@type = :api_key
|
|
44
|
+
@config[:header] = header
|
|
45
|
+
@config[:key] = key
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Verify bearer token
|
|
50
|
+
# @param token [String] Expected bearer token value
|
|
51
|
+
def verify_bearer_token(token:)
|
|
52
|
+
if @current_methods
|
|
53
|
+
auth = WebhookAuthentication.new
|
|
54
|
+
auth.verify_bearer_token(token: token)
|
|
55
|
+
@current_methods << auth
|
|
56
|
+
else
|
|
57
|
+
@type = :bearer_token
|
|
58
|
+
@config[:token] = token
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Verify basic auth credentials
|
|
63
|
+
# @param username [String] Expected username
|
|
64
|
+
# @param password [String] Expected password
|
|
65
|
+
def verify_basic_auth(username:, password:)
|
|
66
|
+
if @current_methods
|
|
67
|
+
auth = WebhookAuthentication.new
|
|
68
|
+
auth.verify_basic_auth(username: username, password: password)
|
|
69
|
+
@current_methods << auth
|
|
70
|
+
else
|
|
71
|
+
@type = :basic_auth
|
|
72
|
+
@config[:username] = username
|
|
73
|
+
@config[:password] = password
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Custom authentication callback
|
|
78
|
+
# @yield [context] Block receives request context hash
|
|
79
|
+
# @yieldreturn [Boolean] true if authenticated, false otherwise
|
|
80
|
+
def verify_custom(&block)
|
|
81
|
+
if @current_methods
|
|
82
|
+
auth = WebhookAuthentication.new
|
|
83
|
+
auth.verify_custom(&block)
|
|
84
|
+
@current_methods << auth
|
|
85
|
+
else
|
|
86
|
+
@type = :custom
|
|
87
|
+
@config[:callback] = block
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Allow multiple authentication methods (any can succeed)
|
|
92
|
+
def any_of(&block)
|
|
93
|
+
@type = :any_of
|
|
94
|
+
@config[:methods] = []
|
|
95
|
+
@current_methods = @config[:methods]
|
|
96
|
+
instance_eval(&block) if block
|
|
97
|
+
@current_methods = nil
|
|
98
|
+
# Restore type in case it was overwritten by method calls
|
|
99
|
+
@type = :any_of
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Allow multiple authentication methods (all must succeed)
|
|
103
|
+
def all_of(&block)
|
|
104
|
+
@type = :all_of
|
|
105
|
+
@config[:methods] = []
|
|
106
|
+
@current_methods = @config[:methods]
|
|
107
|
+
instance_eval(&block) if block
|
|
108
|
+
@current_methods = nil
|
|
109
|
+
# Restore type in case it was overwritten by method calls
|
|
110
|
+
@type = :all_of
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'webhook_authentication'
|
|
4
|
+
|
|
5
|
+
module LanguageOperator
|
|
6
|
+
module Dsl
|
|
7
|
+
# Webhook Definition
|
|
8
|
+
#
|
|
9
|
+
# Defines webhook endpoints for reactive agents.
|
|
10
|
+
#
|
|
11
|
+
# @example Define a webhook with authentication
|
|
12
|
+
# webhook "/github/pr-opened" do
|
|
13
|
+
# method :post
|
|
14
|
+
# authenticate do
|
|
15
|
+
# verify_signature header: "X-Hub-Signature-256",
|
|
16
|
+
# secret: ENV['GITHUB_WEBHOOK_SECRET'],
|
|
17
|
+
# algorithm: :sha256,
|
|
18
|
+
# prefix: "sha256="
|
|
19
|
+
# end
|
|
20
|
+
# on_request do |context|
|
|
21
|
+
# # Handle the webhook
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
class WebhookDefinition
|
|
25
|
+
attr_reader :path, :http_method, :handler, :authentication, :validations
|
|
26
|
+
|
|
27
|
+
# Initialize webhook definition
|
|
28
|
+
#
|
|
29
|
+
# @param path [String] URL path for the webhook
|
|
30
|
+
def initialize(path)
|
|
31
|
+
@path = path
|
|
32
|
+
@http_method = :post
|
|
33
|
+
@handler = nil
|
|
34
|
+
@authentication = nil
|
|
35
|
+
@validations = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Set HTTP method
|
|
39
|
+
#
|
|
40
|
+
# @param method_name [Symbol] HTTP method (:get, :post, :put, :delete, :patch)
|
|
41
|
+
# @return [void]
|
|
42
|
+
def method(method_name)
|
|
43
|
+
@http_method = method_name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Define authentication for this webhook
|
|
47
|
+
#
|
|
48
|
+
# @yield Authentication configuration block
|
|
49
|
+
# @return [void]
|
|
50
|
+
def authenticate(&block)
|
|
51
|
+
@authentication = WebhookAuthentication.new
|
|
52
|
+
@authentication.instance_eval(&block) if block
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Require specific headers
|
|
56
|
+
#
|
|
57
|
+
# @param headers [Hash] Required headers and their expected values (nil = just check presence)
|
|
58
|
+
# @return [void]
|
|
59
|
+
def require_headers(headers)
|
|
60
|
+
@validations << { type: :headers, config: headers }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Require specific content type
|
|
64
|
+
#
|
|
65
|
+
# @param content_type [String, Array<String>] Allowed content type(s)
|
|
66
|
+
# @return [void]
|
|
67
|
+
def require_content_type(*content_types)
|
|
68
|
+
@validations << { type: :content_type, config: content_types.flatten }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Add custom validation
|
|
72
|
+
#
|
|
73
|
+
# @yield [context] Validation block
|
|
74
|
+
# @yieldreturn [Boolean, String] true if valid, or error message string if invalid
|
|
75
|
+
# @return [void]
|
|
76
|
+
def validate(&block)
|
|
77
|
+
@validations << { type: :custom, config: block }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Define the request handler
|
|
81
|
+
#
|
|
82
|
+
# @yield [context] Request handler block
|
|
83
|
+
# @yieldparam context [Hash] Request context with :path, :method, :headers, :params, :body
|
|
84
|
+
# @return [void]
|
|
85
|
+
def on_request(&block)
|
|
86
|
+
@handler = block
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Register this webhook with a web server
|
|
90
|
+
#
|
|
91
|
+
# @param web_server [LanguageOperator::Agent::WebServer] Web server instance
|
|
92
|
+
# @return [void]
|
|
93
|
+
def register(web_server)
|
|
94
|
+
return unless @handler
|
|
95
|
+
|
|
96
|
+
web_server.register_route(
|
|
97
|
+
@path,
|
|
98
|
+
method: @http_method,
|
|
99
|
+
authentication: @authentication,
|
|
100
|
+
validations: @validations,
|
|
101
|
+
&@handler
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|