language-operator 0.0.1 → 0.1.30
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 +53 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +229 -21
- data/Makefile +77 -0
- data/README.md +3 -11
- data/Rakefile +34 -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/agent-reference.md +591 -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/examples/README.md +569 -0
- data/examples/agent_example.rb +86 -0
- data/examples/chat_endpoint_agent.rb +118 -0
- data/examples/github_webhook_agent.rb +171 -0
- data/examples/mcp_agent.rb +158 -0
- data/examples/oauth_callback_agent.rb +296 -0
- data/examples/stripe_webhook_agent.rb +219 -0
- data/examples/webhook_agent.rb +80 -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 +115 -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 +1252 -0
- data/lib/language_operator/cli/commands/cluster.rb +335 -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 +396 -0
- data/lib/language_operator/cli/commands/quickstart.rb +22 -0
- data/lib/language_operator/cli/commands/status.rb +156 -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 +81 -0
- data/lib/language_operator/cli/formatters/log_formatter.rb +290 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +53 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +179 -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/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 +232 -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/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 +160 -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/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 +503 -20
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# OAuth Callback Agent Example
|
|
5
|
+
#
|
|
6
|
+
# This example shows how to create an agent that handles OAuth 2.0 callbacks
|
|
7
|
+
# from various providers (GitHub, Google, etc.).
|
|
8
|
+
#
|
|
9
|
+
# Setup:
|
|
10
|
+
# 1. Set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET environment variables
|
|
11
|
+
# 2. Configure OAuth provider redirect URI to https://<agent-url>/oauth/callback
|
|
12
|
+
# 3. Set OAUTH_STATE_SECRET for state parameter verification
|
|
13
|
+
#
|
|
14
|
+
# OAuth 2.0 flow:
|
|
15
|
+
# 1. User clicks "Login with Provider" -> redirected to provider
|
|
16
|
+
# 2. User authorizes -> provider redirects to /oauth/callback with code
|
|
17
|
+
# 3. Agent exchanges code for access token
|
|
18
|
+
# 4. Agent fetches user info and creates session
|
|
19
|
+
|
|
20
|
+
require 'bundler/setup'
|
|
21
|
+
require 'language_operator'
|
|
22
|
+
require 'net/http'
|
|
23
|
+
require 'uri'
|
|
24
|
+
|
|
25
|
+
LanguageOperator::Dsl.define_agents do
|
|
26
|
+
agent 'oauth-handler' do
|
|
27
|
+
description 'Handles OAuth callbacks and manages user authentication'
|
|
28
|
+
|
|
29
|
+
# Set to reactive mode to receive callbacks
|
|
30
|
+
mode :reactive
|
|
31
|
+
|
|
32
|
+
# GitHub OAuth callback
|
|
33
|
+
webhook '/oauth/github/callback' do
|
|
34
|
+
method :get
|
|
35
|
+
|
|
36
|
+
# Validate state parameter to prevent CSRF attacks
|
|
37
|
+
validate do |context|
|
|
38
|
+
state = context[:params]['state']
|
|
39
|
+
return 'Missing state parameter' unless state
|
|
40
|
+
|
|
41
|
+
# In production, verify state was generated by your app
|
|
42
|
+
# and hasn't been used before (store in Redis/database)
|
|
43
|
+
expected_state = ENV.fetch('OAUTH_STATE_SECRET', nil)
|
|
44
|
+
return 'Invalid state parameter' unless state == expected_state
|
|
45
|
+
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
on_request do |context|
|
|
50
|
+
code = context[:params]['code']
|
|
51
|
+
error = context[:params]['error']
|
|
52
|
+
|
|
53
|
+
# Handle OAuth errors
|
|
54
|
+
if error
|
|
55
|
+
return {
|
|
56
|
+
status: 'error',
|
|
57
|
+
error: error,
|
|
58
|
+
error_description: context[:params]['error_description'],
|
|
59
|
+
message: 'OAuth authorization failed'
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Exchange authorization code for access token
|
|
64
|
+
token_response = exchange_code_for_token(
|
|
65
|
+
code: code,
|
|
66
|
+
client_id: ENV.fetch('GITHUB_CLIENT_ID', nil),
|
|
67
|
+
client_secret: ENV.fetch('GITHUB_CLIENT_SECRET', nil),
|
|
68
|
+
token_url: 'https://github.com/login/oauth/access_token'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if token_response[:error]
|
|
72
|
+
return {
|
|
73
|
+
status: 'error',
|
|
74
|
+
error: token_response[:error],
|
|
75
|
+
message: 'Failed to exchange code for token'
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
access_token = token_response[:access_token]
|
|
80
|
+
|
|
81
|
+
# Fetch user info from GitHub
|
|
82
|
+
user_info = fetch_github_user(access_token)
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
status: 'success',
|
|
86
|
+
message: 'OAuth authentication successful',
|
|
87
|
+
user: {
|
|
88
|
+
id: user_info['id'],
|
|
89
|
+
username: user_info['login'],
|
|
90
|
+
email: user_info['email'],
|
|
91
|
+
name: user_info['name']
|
|
92
|
+
},
|
|
93
|
+
# In production, create session/JWT here
|
|
94
|
+
session_created: true
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Google OAuth callback
|
|
100
|
+
webhook '/oauth/google/callback' do
|
|
101
|
+
method :get
|
|
102
|
+
|
|
103
|
+
validate do |context|
|
|
104
|
+
state = context[:params]['state']
|
|
105
|
+
return 'Missing state parameter' unless state
|
|
106
|
+
|
|
107
|
+
expected_state = ENV.fetch('OAUTH_STATE_SECRET', nil)
|
|
108
|
+
return 'Invalid state parameter' unless state == expected_state
|
|
109
|
+
|
|
110
|
+
true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
on_request do |context|
|
|
114
|
+
code = context[:params]['code']
|
|
115
|
+
error = context[:params]['error']
|
|
116
|
+
|
|
117
|
+
if error
|
|
118
|
+
return {
|
|
119
|
+
status: 'error',
|
|
120
|
+
error: error,
|
|
121
|
+
message: 'OAuth authorization failed'
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Exchange code for token
|
|
126
|
+
token_response = exchange_code_for_token(
|
|
127
|
+
code: code,
|
|
128
|
+
client_id: ENV.fetch('GOOGLE_CLIENT_ID', nil),
|
|
129
|
+
client_secret: ENV.fetch('GOOGLE_CLIENT_SECRET', nil),
|
|
130
|
+
token_url: 'https://oauth2.googleapis.com/token',
|
|
131
|
+
redirect_uri: ENV.fetch('GOOGLE_REDIRECT_URI', nil)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if token_response[:error]
|
|
135
|
+
return {
|
|
136
|
+
status: 'error',
|
|
137
|
+
error: token_response[:error],
|
|
138
|
+
message: 'Failed to exchange code for token'
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
access_token = token_response[:access_token]
|
|
143
|
+
token_response[:id_token]
|
|
144
|
+
|
|
145
|
+
# Fetch user info from Google
|
|
146
|
+
user_info = fetch_google_user(access_token)
|
|
147
|
+
|
|
148
|
+
{
|
|
149
|
+
status: 'success',
|
|
150
|
+
message: 'OAuth authentication successful',
|
|
151
|
+
user: {
|
|
152
|
+
id: user_info['id'],
|
|
153
|
+
email: user_info['email'],
|
|
154
|
+
name: user_info['name'],
|
|
155
|
+
picture: user_info['picture']
|
|
156
|
+
},
|
|
157
|
+
session_created: true
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Generic OAuth callback with bearer token auth
|
|
163
|
+
# (for testing or internal use)
|
|
164
|
+
webhook '/oauth/test/callback' do
|
|
165
|
+
method :post
|
|
166
|
+
|
|
167
|
+
# Require bearer token for security
|
|
168
|
+
authenticate do
|
|
169
|
+
verify_bearer_token(token: ENV.fetch('OAUTH_TEST_TOKEN', nil))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
require_content_type 'application/json'
|
|
173
|
+
|
|
174
|
+
on_request do |context|
|
|
175
|
+
data = JSON.parse(context[:body])
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
status: 'success',
|
|
179
|
+
message: 'Test callback received',
|
|
180
|
+
data: data
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# OAuth initiation endpoint (starts the flow)
|
|
186
|
+
webhook '/oauth/github/initiate' do
|
|
187
|
+
method :get
|
|
188
|
+
|
|
189
|
+
on_request do |_context|
|
|
190
|
+
# Generate state parameter for CSRF protection
|
|
191
|
+
SecureRandom.hex(32)
|
|
192
|
+
|
|
193
|
+
# In production, store state in Redis/database with expiration
|
|
194
|
+
# For demo purposes, we're just using a static value
|
|
195
|
+
|
|
196
|
+
# Build GitHub authorization URL
|
|
197
|
+
params = {
|
|
198
|
+
client_id: ENV.fetch('GITHUB_CLIENT_ID', nil),
|
|
199
|
+
redirect_uri: ENV.fetch('GITHUB_REDIRECT_URI', nil),
|
|
200
|
+
scope: 'user:email',
|
|
201
|
+
state: ENV.fetch('OAUTH_STATE_SECRET', nil) # Use proper state in production
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
auth_url = "https://github.com/login/oauth/authorize?#{URI.encode_www_form(params)}"
|
|
205
|
+
|
|
206
|
+
# Return redirect response
|
|
207
|
+
[
|
|
208
|
+
302,
|
|
209
|
+
{ 'Location' => auth_url },
|
|
210
|
+
['Redirecting to GitHub...']
|
|
211
|
+
]
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Session verification endpoint
|
|
216
|
+
webhook '/oauth/verify' do
|
|
217
|
+
method :get
|
|
218
|
+
|
|
219
|
+
# Verify bearer token
|
|
220
|
+
authenticate do
|
|
221
|
+
verify_bearer_token(token: ENV.fetch('SESSION_TOKEN', nil))
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
on_request do |_context|
|
|
225
|
+
{
|
|
226
|
+
status: 'valid',
|
|
227
|
+
message: 'Session is valid',
|
|
228
|
+
authenticated: true
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Helper methods
|
|
236
|
+
|
|
237
|
+
def exchange_code_for_token(code:, client_id:, client_secret:, token_url:, redirect_uri: nil)
|
|
238
|
+
uri = URI(token_url)
|
|
239
|
+
|
|
240
|
+
params = {
|
|
241
|
+
code: code,
|
|
242
|
+
client_id: client_id,
|
|
243
|
+
client_secret: client_secret,
|
|
244
|
+
grant_type: 'authorization_code'
|
|
245
|
+
}
|
|
246
|
+
params[:redirect_uri] = redirect_uri if redirect_uri
|
|
247
|
+
|
|
248
|
+
request = Net::HTTP::Post.new(uri)
|
|
249
|
+
request.set_form_data(params)
|
|
250
|
+
request['Accept'] = 'application/json'
|
|
251
|
+
|
|
252
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
253
|
+
http.request(request)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
257
|
+
rescue StandardError => e
|
|
258
|
+
{ error: e.message }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def fetch_github_user(access_token)
|
|
262
|
+
uri = URI('https://api.github.com/user')
|
|
263
|
+
|
|
264
|
+
request = Net::HTTP::Get.new(uri)
|
|
265
|
+
request['Authorization'] = "Bearer #{access_token}"
|
|
266
|
+
request['Accept'] = 'application/vnd.github.v3+json'
|
|
267
|
+
|
|
268
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
269
|
+
http.request(request)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
JSON.parse(response.body)
|
|
273
|
+
rescue StandardError => e
|
|
274
|
+
{ error: e.message }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def fetch_google_user(access_token)
|
|
278
|
+
uri = URI('https://www.googleapis.com/oauth2/v2/userinfo')
|
|
279
|
+
|
|
280
|
+
request = Net::HTTP::Get.new(uri)
|
|
281
|
+
request['Authorization'] = "Bearer #{access_token}"
|
|
282
|
+
|
|
283
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
284
|
+
http.request(request)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
JSON.parse(response.body)
|
|
288
|
+
rescue StandardError => e
|
|
289
|
+
{ error: e.message }
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Run the agent if this file is executed directly
|
|
293
|
+
if __FILE__ == $PROGRAM_NAME
|
|
294
|
+
agent = LanguageOperator::Dsl.agent_registry.get('oauth-handler')
|
|
295
|
+
agent.run!
|
|
296
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Stripe Webhook Agent Example
|
|
5
|
+
#
|
|
6
|
+
# This example shows how to create an agent that receives Stripe webhook events
|
|
7
|
+
# with proper signature verification for security.
|
|
8
|
+
#
|
|
9
|
+
# Setup:
|
|
10
|
+
# 1. Set STRIPE_WEBHOOK_SECRET environment variable (from Stripe Dashboard)
|
|
11
|
+
# 2. Configure Stripe webhook to send events to https://<agent-url>/stripe/events
|
|
12
|
+
# 3. Select events you want to receive (e.g., payment_intent.succeeded, customer.created)
|
|
13
|
+
#
|
|
14
|
+
# Stripe webhook documentation:
|
|
15
|
+
# https://stripe.com/docs/webhooks
|
|
16
|
+
|
|
17
|
+
require 'bundler/setup'
|
|
18
|
+
require 'language_operator'
|
|
19
|
+
|
|
20
|
+
LanguageOperator::Dsl.define_agents do
|
|
21
|
+
agent 'stripe-payment-processor' do
|
|
22
|
+
description 'Processes Stripe payment events and triggers fulfillment'
|
|
23
|
+
|
|
24
|
+
# Set to reactive mode to receive webhooks
|
|
25
|
+
mode :reactive
|
|
26
|
+
|
|
27
|
+
# Stripe webhook endpoint
|
|
28
|
+
webhook '/stripe/events' do
|
|
29
|
+
method :post
|
|
30
|
+
|
|
31
|
+
# Verify Stripe webhook signature
|
|
32
|
+
# Stripe sends signature in Stripe-Signature header
|
|
33
|
+
# Format: "t=<timestamp>,v1=<signature>"
|
|
34
|
+
# Note: For simplicity, we're verifying the v1 signature
|
|
35
|
+
# Production should also check timestamp to prevent replay attacks
|
|
36
|
+
authenticate do
|
|
37
|
+
verify_custom do |context|
|
|
38
|
+
signature_header = context[:headers]['Stripe-Signature']
|
|
39
|
+
return false unless signature_header
|
|
40
|
+
|
|
41
|
+
# Parse signature header
|
|
42
|
+
# Format: t=1614556800,v1=abc123,v0=def456
|
|
43
|
+
sig_parts = {}
|
|
44
|
+
signature_header.split(',').each do |part|
|
|
45
|
+
key, value = part.split('=', 2)
|
|
46
|
+
sig_parts[key] = value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
timestamp = sig_parts['t']
|
|
50
|
+
signature = sig_parts['v1']
|
|
51
|
+
return false unless timestamp && signature
|
|
52
|
+
|
|
53
|
+
# Construct signed payload
|
|
54
|
+
signed_payload = "#{timestamp}.#{context[:body]}"
|
|
55
|
+
|
|
56
|
+
# Compute expected signature
|
|
57
|
+
secret = ENV.fetch('STRIPE_WEBHOOK_SECRET', nil)
|
|
58
|
+
expected = OpenSSL::HMAC.hexdigest('sha256', secret, signed_payload)
|
|
59
|
+
|
|
60
|
+
# Compare signatures (constant-time)
|
|
61
|
+
signature == expected
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Validate request format
|
|
66
|
+
require_content_type 'application/json'
|
|
67
|
+
|
|
68
|
+
on_request do |context|
|
|
69
|
+
event = JSON.parse(context[:body])
|
|
70
|
+
event_type = event['type']
|
|
71
|
+
event_data = event['data']['object']
|
|
72
|
+
|
|
73
|
+
case event_type
|
|
74
|
+
when 'payment_intent.succeeded'
|
|
75
|
+
handle_payment_succeeded(event_data)
|
|
76
|
+
when 'payment_intent.payment_failed'
|
|
77
|
+
handle_payment_failed(event_data)
|
|
78
|
+
when 'customer.created'
|
|
79
|
+
handle_customer_created(event_data)
|
|
80
|
+
when 'customer.subscription.created'
|
|
81
|
+
handle_subscription_created(event_data)
|
|
82
|
+
when 'customer.subscription.deleted'
|
|
83
|
+
handle_subscription_deleted(event_data)
|
|
84
|
+
when 'invoice.payment_succeeded'
|
|
85
|
+
handle_invoice_paid(event_data)
|
|
86
|
+
when 'invoice.payment_failed'
|
|
87
|
+
handle_invoice_failed(event_data)
|
|
88
|
+
when 'charge.refunded'
|
|
89
|
+
handle_refund(event_data)
|
|
90
|
+
else
|
|
91
|
+
{ status: 'ignored', event_type: event_type }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Alternative endpoint with API key authentication
|
|
97
|
+
# Useful for testing or internal triggers
|
|
98
|
+
webhook '/stripe/manual-trigger' do
|
|
99
|
+
method :post
|
|
100
|
+
|
|
101
|
+
# Verify API key
|
|
102
|
+
authenticate do
|
|
103
|
+
verify_api_key(
|
|
104
|
+
header: 'X-API-Key',
|
|
105
|
+
key: ENV.fetch('STRIPE_INTERNAL_API_KEY', nil)
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
require_content_type 'application/json'
|
|
110
|
+
|
|
111
|
+
on_request do |context|
|
|
112
|
+
data = JSON.parse(context[:body])
|
|
113
|
+
action = data['action']
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
status: 'processed',
|
|
117
|
+
action: action,
|
|
118
|
+
message: "Manual trigger received: #{action}"
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Helper methods for event handling
|
|
126
|
+
|
|
127
|
+
def handle_payment_succeeded(payment_intent)
|
|
128
|
+
{
|
|
129
|
+
status: 'processed',
|
|
130
|
+
event: 'payment_succeeded',
|
|
131
|
+
amount: payment_intent['amount'],
|
|
132
|
+
currency: payment_intent['currency'],
|
|
133
|
+
customer: payment_intent['customer'],
|
|
134
|
+
payment_intent_id: payment_intent['id'],
|
|
135
|
+
message: "Payment of #{payment_intent['amount']} #{payment_intent['currency']} succeeded"
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def handle_payment_failed(payment_intent)
|
|
140
|
+
{
|
|
141
|
+
status: 'processed',
|
|
142
|
+
event: 'payment_failed',
|
|
143
|
+
amount: payment_intent['amount'],
|
|
144
|
+
currency: payment_intent['currency'],
|
|
145
|
+
customer: payment_intent['customer'],
|
|
146
|
+
error: payment_intent['last_payment_error']&.dig('message'),
|
|
147
|
+
message: 'Payment failed'
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def handle_customer_created(customer)
|
|
152
|
+
{
|
|
153
|
+
status: 'processed',
|
|
154
|
+
event: 'customer_created',
|
|
155
|
+
customer_id: customer['id'],
|
|
156
|
+
email: customer['email'],
|
|
157
|
+
message: "New customer created: #{customer['email']}"
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def handle_subscription_created(subscription)
|
|
162
|
+
{
|
|
163
|
+
status: 'processed',
|
|
164
|
+
event: 'subscription_created',
|
|
165
|
+
subscription_id: subscription['id'],
|
|
166
|
+
customer: subscription['customer'],
|
|
167
|
+
plan: subscription['items']['data'].first['price']['id'],
|
|
168
|
+
message: "New subscription created for customer #{subscription['customer']}"
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def handle_subscription_deleted(subscription)
|
|
173
|
+
{
|
|
174
|
+
status: 'processed',
|
|
175
|
+
event: 'subscription_deleted',
|
|
176
|
+
subscription_id: subscription['id'],
|
|
177
|
+
customer: subscription['customer'],
|
|
178
|
+
message: "Subscription deleted for customer #{subscription['customer']}"
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def handle_invoice_paid(invoice)
|
|
183
|
+
{
|
|
184
|
+
status: 'processed',
|
|
185
|
+
event: 'invoice_paid',
|
|
186
|
+
invoice_id: invoice['id'],
|
|
187
|
+
amount: invoice['amount_paid'],
|
|
188
|
+
customer: invoice['customer'],
|
|
189
|
+
message: "Invoice #{invoice['number']} paid"
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def handle_invoice_failed(invoice)
|
|
194
|
+
{
|
|
195
|
+
status: 'processed',
|
|
196
|
+
event: 'invoice_failed',
|
|
197
|
+
invoice_id: invoice['id'],
|
|
198
|
+
amount: invoice['amount_due'],
|
|
199
|
+
customer: invoice['customer'],
|
|
200
|
+
message: "Invoice #{invoice['number']} payment failed"
|
|
201
|
+
}
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def handle_refund(charge)
|
|
205
|
+
{
|
|
206
|
+
status: 'processed',
|
|
207
|
+
event: 'refund',
|
|
208
|
+
charge_id: charge['id'],
|
|
209
|
+
amount_refunded: charge['amount_refunded'],
|
|
210
|
+
customer: charge['customer'],
|
|
211
|
+
message: "Charge #{charge['id']} refunded"
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Run the agent if this file is executed directly
|
|
216
|
+
if __FILE__ == $PROGRAM_NAME
|
|
217
|
+
agent = LanguageOperator::Dsl.agent_registry.get('stripe-payment-processor')
|
|
218
|
+
agent.run!
|
|
219
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example webhook agent that demonstrates the reactive/HTTP server capability
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# PORT=8080 ruby examples/webhook_agent.rb
|
|
8
|
+
#
|
|
9
|
+
# Test with curl:
|
|
10
|
+
# curl -X POST http://localhost:8080/webhook -H "Content-Type: application/json" -d '{"event": "test"}'
|
|
11
|
+
# curl http://localhost:8080/health
|
|
12
|
+
|
|
13
|
+
require 'bundler/setup'
|
|
14
|
+
require 'language_operator'
|
|
15
|
+
require 'language_operator/dsl'
|
|
16
|
+
|
|
17
|
+
# Define a webhook agent using the DSL
|
|
18
|
+
# rubocop:disable Metrics/BlockLength
|
|
19
|
+
LanguageOperator::Dsl.define do
|
|
20
|
+
agent 'example-webhook-handler' do
|
|
21
|
+
description 'Example agent that handles HTTP webhooks'
|
|
22
|
+
mode :reactive
|
|
23
|
+
|
|
24
|
+
# Health check endpoint (already provided by default)
|
|
25
|
+
# GET /health
|
|
26
|
+
|
|
27
|
+
# Custom webhook endpoint
|
|
28
|
+
webhook '/webhook' do
|
|
29
|
+
method :post
|
|
30
|
+
on_request do |context|
|
|
31
|
+
puts "\n=== Received Webhook ==="
|
|
32
|
+
puts "Method: #{context[:method]}"
|
|
33
|
+
puts "Path: #{context[:path]}"
|
|
34
|
+
puts "Body: #{context[:body]}"
|
|
35
|
+
puts "Params: #{context[:params].inspect}"
|
|
36
|
+
puts "========================\n"
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
status: 'received',
|
|
40
|
+
message: 'Webhook processed successfully',
|
|
41
|
+
received_data: context[:params]
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# GitHub-style webhook endpoint
|
|
47
|
+
webhook '/github/pr' do
|
|
48
|
+
method :post
|
|
49
|
+
on_request do |_context|
|
|
50
|
+
puts "\n=== GitHub PR Webhook ==="
|
|
51
|
+
puts 'Simulating PR review...'
|
|
52
|
+
puts "============================\n"
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
status: 'pr_reviewed',
|
|
56
|
+
message: 'Pull request review queued'
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
# rubocop:enable Metrics/BlockLength
|
|
63
|
+
|
|
64
|
+
# Get the agent definition and run it
|
|
65
|
+
agent_def = LanguageOperator::Dsl.agent_registry.get('example-webhook-handler')
|
|
66
|
+
|
|
67
|
+
if agent_def
|
|
68
|
+
puts "Starting webhook agent on port #{ENV.fetch('PORT', '8080')}"
|
|
69
|
+
puts 'Available endpoints:'
|
|
70
|
+
puts ' GET /health - Health check'
|
|
71
|
+
puts ' GET /ready - Readiness check'
|
|
72
|
+
puts ' POST /webhook - Generic webhook'
|
|
73
|
+
puts ' POST /github/pr - GitHub PR webhook'
|
|
74
|
+
puts "\nPress Ctrl+C to stop\n\n"
|
|
75
|
+
|
|
76
|
+
agent_def.run!
|
|
77
|
+
else
|
|
78
|
+
puts 'Error: Agent definition not found'
|
|
79
|
+
exit 1
|
|
80
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../client'
|
|
4
|
+
require_relative 'telemetry'
|
|
5
|
+
require_relative 'instrumentation'
|
|
6
|
+
|
|
7
|
+
module LanguageOperator
|
|
8
|
+
module Agent
|
|
9
|
+
# Base Agent Class
|
|
10
|
+
#
|
|
11
|
+
# Extends LanguageOperator::Client::Base with agent-specific functionality including:
|
|
12
|
+
# - Workspace integration
|
|
13
|
+
# - Goal-directed execution
|
|
14
|
+
# - Autonomous operation modes
|
|
15
|
+
#
|
|
16
|
+
# @example Basic agent
|
|
17
|
+
# agent = LanguageOperator::Agent::Base.new(config)
|
|
18
|
+
# agent.connect!
|
|
19
|
+
# agent.execute_goal("Complete the task")
|
|
20
|
+
class Base < LanguageOperator::Client::Base
|
|
21
|
+
include Instrumentation
|
|
22
|
+
|
|
23
|
+
attr_reader :workspace_path, :mode
|
|
24
|
+
|
|
25
|
+
# Initialize the agent
|
|
26
|
+
#
|
|
27
|
+
# @param config [Hash] Configuration hash
|
|
28
|
+
def initialize(config)
|
|
29
|
+
super
|
|
30
|
+
|
|
31
|
+
# Initialize OpenTelemetry
|
|
32
|
+
LanguageOperator::Agent::Telemetry.configure
|
|
33
|
+
otel_enabled = !ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil).nil?
|
|
34
|
+
logger.info "OpenTelemetry #{otel_enabled ? 'enabled' : 'disabled'}"
|
|
35
|
+
|
|
36
|
+
@workspace_path = ENV.fetch('WORKSPACE_PATH', '/workspace')
|
|
37
|
+
@mode = ENV.fetch('AGENT_MODE', 'autonomous')
|
|
38
|
+
@executor = nil
|
|
39
|
+
@scheduler = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Run the agent in its configured mode
|
|
43
|
+
#
|
|
44
|
+
# @return [void]
|
|
45
|
+
def run
|
|
46
|
+
with_span('agent.run', attributes: {
|
|
47
|
+
'agent.name' => ENV.fetch('AGENT_NAME', nil),
|
|
48
|
+
'agent.mode' => @mode,
|
|
49
|
+
'agent.workspace_available' => workspace_available?
|
|
50
|
+
}) do
|
|
51
|
+
connect!
|
|
52
|
+
|
|
53
|
+
case @mode
|
|
54
|
+
when 'autonomous', 'interactive'
|
|
55
|
+
run_autonomous
|
|
56
|
+
when 'scheduled', 'event-driven'
|
|
57
|
+
run_scheduled
|
|
58
|
+
when 'reactive', 'http', 'webhook'
|
|
59
|
+
run_reactive
|
|
60
|
+
else
|
|
61
|
+
raise "Unknown agent mode: #{@mode}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Execute a single goal
|
|
67
|
+
#
|
|
68
|
+
# @param goal [String] The goal to achieve
|
|
69
|
+
# @return [String] The result
|
|
70
|
+
def execute_goal(goal)
|
|
71
|
+
@executor ||= Executor.new(self)
|
|
72
|
+
@executor.execute(goal)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if workspace is available
|
|
76
|
+
#
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def workspace_available?
|
|
79
|
+
File.directory?(@workspace_path) && File.writable?(@workspace_path)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Run in autonomous mode
|
|
85
|
+
#
|
|
86
|
+
# @return [void]
|
|
87
|
+
def run_autonomous
|
|
88
|
+
@executor = Executor.new(self)
|
|
89
|
+
@executor.run_loop
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Run in scheduled mode
|
|
93
|
+
#
|
|
94
|
+
# @return [void]
|
|
95
|
+
def run_scheduled
|
|
96
|
+
@scheduler = Scheduler.new(self)
|
|
97
|
+
@scheduler.start
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Run in reactive mode (HTTP server)
|
|
101
|
+
#
|
|
102
|
+
# @return [void]
|
|
103
|
+
def run_reactive
|
|
104
|
+
require_relative 'web_server'
|
|
105
|
+
@web_server = WebServer.new(self)
|
|
106
|
+
@web_server.start
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|