fluyenta-ruby 0.1.14
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 +7 -0
- data/CHANGELOG.md +68 -0
- data/LICENSE +11 -0
- data/README.md +571 -0
- data/lib/brainzlab/beacon/client.rb +227 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +676 -0
- data/lib/brainzlab/context.rb +90 -0
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +159 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +223 -0
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +250 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/development/logger.rb +150 -0
- data/lib/brainzlab/development/store.rb +121 -0
- data/lib/brainzlab/development.rb +72 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
- data/lib/brainzlab/devtools/assets/devtools.js +396 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +68 -0
- data/lib/brainzlab/flux/provisioner.rb +124 -0
- data/lib/brainzlab/flux.rb +184 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +559 -0
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
- data/lib/brainzlab/instrumentation/aws.rb +183 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/faraday.rb +181 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +252 -0
- data/lib/brainzlab/instrumentation/httparty.rb +193 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +114 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation/redis.rb +324 -0
- data/lib/brainzlab/instrumentation/resque.rb +114 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
- data/lib/brainzlab/instrumentation/stripe.rb +163 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
- data/lib/brainzlab/instrumentation.rb +360 -0
- data/lib/brainzlab/nerve/client.rb +235 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/client.rb +203 -0
- data/lib/brainzlab/pulse/instrumentation.rb +401 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +294 -0
- data/lib/brainzlab/rails/log_formatter.rb +807 -0
- data/lib/brainzlab/rails/log_subscriber.rb +334 -0
- data/lib/brainzlab/rails/railtie.rb +606 -0
- data/lib/brainzlab/recall/buffer.rb +66 -0
- data/lib/brainzlab/recall/client.rb +158 -0
- data/lib/brainzlab/recall/logger.rb +116 -0
- data/lib/brainzlab/recall/provisioner.rb +130 -0
- data/lib/brainzlab/recall.rb +175 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +150 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +421 -0
- data/lib/brainzlab/sentinel/client.rb +236 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +60 -0
- data/lib/brainzlab/signal/provisioner.rb +115 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +308 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/testing/event_store.rb +377 -0
- data/lib/brainzlab/testing/helpers.rb +650 -0
- data/lib/brainzlab/testing/matchers.rb +391 -0
- data/lib/brainzlab/testing.rb +327 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
- data/lib/brainzlab/utilities/health_check.rb +294 -0
- data/lib/brainzlab/utilities/log_formatter.rb +254 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +216 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +262 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab/vision/client.rb +175 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +155 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +306 -0
- data/lib/generators/brainzlab/install/install_generator.rb +63 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- metadata +251 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'vault/client'
|
|
4
|
+
require_relative 'vault/cache'
|
|
5
|
+
require_relative 'vault/provisioner'
|
|
6
|
+
|
|
7
|
+
module BrainzLab
|
|
8
|
+
module Vault
|
|
9
|
+
class << self
|
|
10
|
+
# Load all secrets into ENV like dotenv
|
|
11
|
+
# This is the main method to use at app startup
|
|
12
|
+
#
|
|
13
|
+
# @param environment [String, Symbol] Environment to load (defaults to current)
|
|
14
|
+
# @param overwrite [Boolean] Whether to overwrite existing ENV vars (default: false)
|
|
15
|
+
# @param provider_keys [Boolean] Also load provider keys like OPENAI_API_KEY (default: true)
|
|
16
|
+
# @return [Hash] The secrets that were loaded
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# # In config/application.rb or an initializer
|
|
20
|
+
# BrainzLab::Vault.load!
|
|
21
|
+
#
|
|
22
|
+
# # Load with options
|
|
23
|
+
# BrainzLab::Vault.load!(environment: :production, overwrite: true)
|
|
24
|
+
#
|
|
25
|
+
def load!(environment: nil, overwrite: false, provider_keys: true)
|
|
26
|
+
return {} unless enabled?
|
|
27
|
+
|
|
28
|
+
ensure_provisioned!
|
|
29
|
+
return {} unless BrainzLab.configuration.vault_valid?
|
|
30
|
+
|
|
31
|
+
env = environment&.to_s || BrainzLab.configuration.environment
|
|
32
|
+
loaded = {}
|
|
33
|
+
|
|
34
|
+
# Load regular secrets
|
|
35
|
+
secrets = export(environment: env, format: :json)
|
|
36
|
+
secrets.each do |key, value|
|
|
37
|
+
key_str = key.to_s
|
|
38
|
+
next unless overwrite || !ENV.key?(key_str)
|
|
39
|
+
|
|
40
|
+
ENV[key_str] = value.to_s
|
|
41
|
+
loaded[key_str] = value
|
|
42
|
+
BrainzLab.debug_log("[Vault] Loaded #{key_str}")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Load provider keys (OpenAI, Anthropic, etc.)
|
|
46
|
+
if provider_keys
|
|
47
|
+
provider_secrets = load_provider_keys!(overwrite: overwrite)
|
|
48
|
+
loaded.merge!(provider_secrets)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
BrainzLab.debug_log("[Vault] Loaded #{loaded.size} secrets into ENV")
|
|
52
|
+
loaded
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
BrainzLab.debug_log("[Vault] Failed to load secrets: #{e.message}")
|
|
55
|
+
{}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Load provider keys (API keys for LLMs, etc.) into ENV
|
|
59
|
+
#
|
|
60
|
+
# @param overwrite [Boolean] Whether to overwrite existing ENV vars
|
|
61
|
+
# @return [Hash] Provider keys that were loaded
|
|
62
|
+
def load_provider_keys!(overwrite: false)
|
|
63
|
+
return {} unless enabled? && BrainzLab.configuration.vault_valid?
|
|
64
|
+
|
|
65
|
+
loaded = {}
|
|
66
|
+
provider_keys = client.get_provider_keys
|
|
67
|
+
|
|
68
|
+
provider_keys.each do |provider, key|
|
|
69
|
+
env_var = "#{provider.to_s.upcase}_API_KEY"
|
|
70
|
+
next unless overwrite || !ENV.key?(env_var)
|
|
71
|
+
|
|
72
|
+
ENV[env_var] = key
|
|
73
|
+
loaded[env_var] = key
|
|
74
|
+
BrainzLab.debug_log("[Vault] Loaded provider key: #{env_var}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
loaded
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
BrainzLab.debug_log("[Vault] Failed to load provider keys: #{e.message}")
|
|
80
|
+
{}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get a specific provider key
|
|
84
|
+
# @param provider [String, Symbol] Provider name (openai, anthropic, etc.)
|
|
85
|
+
# @param model_type [String] Model type (llm, embedding, etc.)
|
|
86
|
+
# @return [String, nil] The API key
|
|
87
|
+
def provider_key(provider, model_type: 'llm')
|
|
88
|
+
return nil unless enabled?
|
|
89
|
+
|
|
90
|
+
ensure_provisioned!
|
|
91
|
+
return nil unless BrainzLab.configuration.vault_valid?
|
|
92
|
+
|
|
93
|
+
# Check ENV first
|
|
94
|
+
env_var = "#{provider.to_s.upcase}_API_KEY"
|
|
95
|
+
return ENV[env_var] if ENV[env_var] && !ENV[env_var].empty?
|
|
96
|
+
|
|
97
|
+
# Fetch from Vault
|
|
98
|
+
client.get_provider_key(provider: provider.to_s, model_type: model_type)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get a secret value
|
|
102
|
+
# @param key [String] The secret key
|
|
103
|
+
# @param environment [String, Symbol] Optional environment (defaults to current environment)
|
|
104
|
+
# @param default [Object] Default value if secret not found
|
|
105
|
+
# @return [String, nil] The secret value
|
|
106
|
+
def get(key, environment: nil, default: nil)
|
|
107
|
+
return default unless enabled?
|
|
108
|
+
|
|
109
|
+
ensure_provisioned!
|
|
110
|
+
return default unless BrainzLab.configuration.vault_valid?
|
|
111
|
+
|
|
112
|
+
env = environment&.to_s || BrainzLab.configuration.environment
|
|
113
|
+
cache_key = "#{env}:#{key}"
|
|
114
|
+
|
|
115
|
+
# Check cache first
|
|
116
|
+
return cache.get(cache_key) if BrainzLab.configuration.vault_cache_enabled && cache.has?(cache_key)
|
|
117
|
+
|
|
118
|
+
value = client.get(key, environment: env)
|
|
119
|
+
|
|
120
|
+
if value.nil?
|
|
121
|
+
default
|
|
122
|
+
else
|
|
123
|
+
cache.set(cache_key, value) if BrainzLab.configuration.vault_cache_enabled
|
|
124
|
+
value
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Set a secret value
|
|
129
|
+
# @param key [String] The secret key
|
|
130
|
+
# @param value [String] The secret value
|
|
131
|
+
# @param environment [String, Symbol] Optional environment (defaults to current environment)
|
|
132
|
+
# @param description [String] Optional description
|
|
133
|
+
# @param note [String] Optional version note
|
|
134
|
+
# @return [Boolean] True if successful
|
|
135
|
+
def set(key, value, environment: nil, description: nil, note: nil)
|
|
136
|
+
return false unless enabled?
|
|
137
|
+
|
|
138
|
+
ensure_provisioned!
|
|
139
|
+
return false unless BrainzLab.configuration.vault_valid?
|
|
140
|
+
|
|
141
|
+
env = environment&.to_s || BrainzLab.configuration.environment
|
|
142
|
+
result = client.set(key, value, environment: env, description: description, note: note)
|
|
143
|
+
|
|
144
|
+
# Invalidate cache
|
|
145
|
+
cache.delete("#{env}:#{key}") if result && BrainzLab.configuration.vault_cache_enabled
|
|
146
|
+
|
|
147
|
+
result
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# List all secret keys
|
|
151
|
+
# @param environment [String, Symbol] Optional environment
|
|
152
|
+
# @return [Array<Hash>] List of secret metadata
|
|
153
|
+
def list(environment: nil)
|
|
154
|
+
return [] unless enabled?
|
|
155
|
+
|
|
156
|
+
ensure_provisioned!
|
|
157
|
+
return [] unless BrainzLab.configuration.vault_valid?
|
|
158
|
+
|
|
159
|
+
env = environment&.to_s || BrainzLab.configuration.environment
|
|
160
|
+
client.list(environment: env)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Delete (archive) a secret
|
|
164
|
+
# @param key [String] The secret key
|
|
165
|
+
# @return [Boolean] True if successful
|
|
166
|
+
def delete(key)
|
|
167
|
+
return false unless enabled?
|
|
168
|
+
|
|
169
|
+
ensure_provisioned!
|
|
170
|
+
return false unless BrainzLab.configuration.vault_valid?
|
|
171
|
+
|
|
172
|
+
result = client.delete(key)
|
|
173
|
+
|
|
174
|
+
# Invalidate all environment caches for this key
|
|
175
|
+
cache.delete_pattern("*:#{key}") if result && BrainzLab.configuration.vault_cache_enabled
|
|
176
|
+
|
|
177
|
+
result
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Export all secrets for an environment
|
|
181
|
+
# @param environment [String, Symbol] Environment to export
|
|
182
|
+
# @param format [Symbol] Output format (:json, :dotenv, :shell)
|
|
183
|
+
# @return [Hash, String] Exported secrets
|
|
184
|
+
def export(environment: nil, format: :json)
|
|
185
|
+
return {} unless enabled?
|
|
186
|
+
|
|
187
|
+
ensure_provisioned!
|
|
188
|
+
return {} unless BrainzLab.configuration.vault_valid?
|
|
189
|
+
|
|
190
|
+
env = environment&.to_s || BrainzLab.configuration.environment
|
|
191
|
+
client.export(environment: env, format: format)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Fetch a secret with automatic fallback
|
|
195
|
+
# @param key [String] The secret key
|
|
196
|
+
# @param env_var [String] Environment variable to fall back to
|
|
197
|
+
# @return [String, nil] The secret value
|
|
198
|
+
def fetch(key, env_var: nil)
|
|
199
|
+
value = get(key)
|
|
200
|
+
return value if value && !value.to_s.empty?
|
|
201
|
+
|
|
202
|
+
# Fall back to environment variable
|
|
203
|
+
if env_var
|
|
204
|
+
ENV.fetch(env_var, nil)
|
|
205
|
+
else
|
|
206
|
+
ENV.fetch(key, nil)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Clear the secret cache
|
|
211
|
+
def clear_cache!
|
|
212
|
+
cache.clear!
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Warm the cache with all secrets
|
|
216
|
+
def warm_cache!(environment: nil)
|
|
217
|
+
return unless enabled? && BrainzLab.configuration.vault_cache_enabled
|
|
218
|
+
|
|
219
|
+
env = environment&.to_s || BrainzLab.configuration.environment
|
|
220
|
+
secrets = export(environment: env, format: :json)
|
|
221
|
+
|
|
222
|
+
secrets.each do |key, value|
|
|
223
|
+
cache.set("#{env}:#{key}", value)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# === INTERNAL ===
|
|
228
|
+
|
|
229
|
+
def ensure_provisioned!
|
|
230
|
+
return if @provisioned
|
|
231
|
+
|
|
232
|
+
@provisioned = true
|
|
233
|
+
provisioner.ensure_project!
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def provisioner
|
|
237
|
+
@provisioner ||= Provisioner.new(BrainzLab.configuration)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def client
|
|
241
|
+
@client ||= Client.new(BrainzLab.configuration)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def cache
|
|
245
|
+
@cache ||= Cache.new(BrainzLab.configuration.vault_cache_ttl)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def reset!
|
|
249
|
+
@client = nil
|
|
250
|
+
@provisioner = nil
|
|
251
|
+
@cache = nil
|
|
252
|
+
@provisioned = false
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
private
|
|
256
|
+
|
|
257
|
+
def enabled?
|
|
258
|
+
BrainzLab.configuration.vault_enabled
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module BrainzLab
|
|
8
|
+
module Vision
|
|
9
|
+
class Client
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Execute an autonomous AI task
|
|
15
|
+
def execute_task(instruction:, start_url:, model: nil, browser_provider: nil, max_steps: 50, timeout: 300)
|
|
16
|
+
payload = {
|
|
17
|
+
instruction: instruction,
|
|
18
|
+
start_url: start_url,
|
|
19
|
+
max_steps: max_steps,
|
|
20
|
+
timeout: timeout
|
|
21
|
+
}
|
|
22
|
+
payload[:model] = model if model
|
|
23
|
+
payload[:browser_provider] = browser_provider if browser_provider
|
|
24
|
+
|
|
25
|
+
post('/mcp/tools/vision_task', payload)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create a browser session
|
|
29
|
+
def create_session(url: nil, viewport: nil, browser_provider: nil)
|
|
30
|
+
payload = {}
|
|
31
|
+
payload[:url] = url if url
|
|
32
|
+
payload[:viewport] = viewport if viewport
|
|
33
|
+
payload[:browser_provider] = browser_provider if browser_provider
|
|
34
|
+
|
|
35
|
+
post('/mcp/tools/vision_session_create', payload)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Perform an AI-powered action
|
|
39
|
+
def ai_action(session_id:, instruction:, model: nil)
|
|
40
|
+
payload = {
|
|
41
|
+
session_id: session_id,
|
|
42
|
+
instruction: instruction
|
|
43
|
+
}
|
|
44
|
+
payload[:model] = model if model
|
|
45
|
+
|
|
46
|
+
post('/mcp/tools/vision_ai_action', payload)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Perform a direct browser action
|
|
50
|
+
def perform(session_id:, action:, selector: nil, value: nil)
|
|
51
|
+
payload = {
|
|
52
|
+
session_id: session_id,
|
|
53
|
+
action: action.to_s
|
|
54
|
+
}
|
|
55
|
+
payload[:selector] = selector if selector
|
|
56
|
+
payload[:value] = value if value
|
|
57
|
+
|
|
58
|
+
post('/mcp/tools/vision_perform', payload)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Extract structured data
|
|
62
|
+
def extract(session_id:, schema:, instruction: nil)
|
|
63
|
+
payload = {
|
|
64
|
+
session_id: session_id,
|
|
65
|
+
schema: schema
|
|
66
|
+
}
|
|
67
|
+
payload[:instruction] = instruction if instruction
|
|
68
|
+
|
|
69
|
+
post('/mcp/tools/vision_extract', payload)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Close a session
|
|
73
|
+
def close_session(session_id:)
|
|
74
|
+
post('/mcp/tools/vision_session_close', { session_id: session_id })
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Take a screenshot
|
|
78
|
+
def screenshot(session_id:, full_page: true)
|
|
79
|
+
post('/mcp/tools/vision_screenshot', {
|
|
80
|
+
session_id: session_id,
|
|
81
|
+
full_page: full_page
|
|
82
|
+
})
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def post(path, payload)
|
|
88
|
+
uri = URI.parse("#{@config.vision_url}#{path}")
|
|
89
|
+
request = Net::HTTP::Post.new(uri)
|
|
90
|
+
request['Content-Type'] = 'application/json'
|
|
91
|
+
request['Authorization'] = "Bearer #{auth_key}"
|
|
92
|
+
request['User-Agent'] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
|
|
93
|
+
request.body = JSON.generate(payload)
|
|
94
|
+
|
|
95
|
+
response = execute(uri, request)
|
|
96
|
+
|
|
97
|
+
case response
|
|
98
|
+
when Net::HTTPSuccess
|
|
99
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
100
|
+
when Net::HTTPUnauthorized
|
|
101
|
+
structured_error = AuthenticationError.new(
|
|
102
|
+
'Invalid API key',
|
|
103
|
+
hint: 'Verify your Vision API key is correct and has not expired.',
|
|
104
|
+
code: 'vision_unauthorized'
|
|
105
|
+
)
|
|
106
|
+
log_error(path, structured_error)
|
|
107
|
+
{ error: structured_error.message, brainzlab_error: structured_error }
|
|
108
|
+
when Net::HTTPForbidden
|
|
109
|
+
structured_error = AuthenticationError.new(
|
|
110
|
+
'Vision is not enabled for this project',
|
|
111
|
+
hint: 'Enable Vision in your project settings or check your permissions.',
|
|
112
|
+
code: 'vision_forbidden'
|
|
113
|
+
)
|
|
114
|
+
log_error(path, structured_error)
|
|
115
|
+
{ error: structured_error.message, brainzlab_error: structured_error }
|
|
116
|
+
when Net::HTTPNotFound
|
|
117
|
+
structured_error = NotFoundError.new(
|
|
118
|
+
"Vision endpoint not found: #{path}",
|
|
119
|
+
hint: 'Verify the Vision service is properly configured.',
|
|
120
|
+
code: 'vision_not_found',
|
|
121
|
+
resource_type: 'endpoint',
|
|
122
|
+
resource_id: path
|
|
123
|
+
)
|
|
124
|
+
log_error(path, structured_error)
|
|
125
|
+
{ error: structured_error.message, brainzlab_error: structured_error }
|
|
126
|
+
when Net::HTTPTooManyRequests
|
|
127
|
+
structured_error = RateLimitError.new(
|
|
128
|
+
'Vision rate limit exceeded',
|
|
129
|
+
retry_after: response['Retry-After']&.to_i,
|
|
130
|
+
code: 'vision_rate_limit'
|
|
131
|
+
)
|
|
132
|
+
log_error(path, structured_error)
|
|
133
|
+
{ error: structured_error.message, brainzlab_error: structured_error }
|
|
134
|
+
else
|
|
135
|
+
structured_error = ErrorHandler.from_response(response, service: 'Vision', operation: path)
|
|
136
|
+
log_error(path, structured_error)
|
|
137
|
+
{ error: structured_error.message, brainzlab_error: structured_error }
|
|
138
|
+
end
|
|
139
|
+
rescue JSON::ParserError => e
|
|
140
|
+
structured_error = ServerError.new(
|
|
141
|
+
"Invalid JSON response from Vision: #{e.message}",
|
|
142
|
+
hint: 'The Vision service returned an unexpected response format.',
|
|
143
|
+
code: 'vision_invalid_response'
|
|
144
|
+
)
|
|
145
|
+
log_error(path, structured_error)
|
|
146
|
+
{ error: structured_error.message, brainzlab_error: structured_error }
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
structured_error = ErrorHandler.wrap(e, service: 'Vision', operation: path)
|
|
149
|
+
log_error(path, structured_error)
|
|
150
|
+
{ error: structured_error.message, brainzlab_error: structured_error }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def log_error(operation, error)
|
|
154
|
+
BrainzLab.debug_log("[Vision::Client] #{operation} failed: #{error.message}")
|
|
155
|
+
|
|
156
|
+
# Call on_error callback if configured
|
|
157
|
+
if @config.on_error
|
|
158
|
+
@config.on_error.call(error, { service: 'Vision', operation: operation })
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def auth_key
|
|
163
|
+
@config.vision_ingest_key || @config.vision_api_key || @config.secret_key
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def execute(uri, request)
|
|
167
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
168
|
+
http.use_ssl = uri.scheme == 'https'
|
|
169
|
+
http.open_timeout = 10
|
|
170
|
+
http.read_timeout = 300 # Long timeout for AI tasks
|
|
171
|
+
http.request(request)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
|
|
8
|
+
module BrainzLab
|
|
9
|
+
module Vision
|
|
10
|
+
class Provisioner
|
|
11
|
+
CACHE_DIR = ENV.fetch('BRAINZLAB_CACHE_DIR') { File.join(Dir.home, '.brainzlab') }
|
|
12
|
+
|
|
13
|
+
def initialize(config)
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ensure_project!
|
|
18
|
+
return unless should_provision?
|
|
19
|
+
|
|
20
|
+
# Try cached credentials first
|
|
21
|
+
if (cached = load_cached_credentials)
|
|
22
|
+
apply_credentials(cached)
|
|
23
|
+
return cached
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Provision new project
|
|
27
|
+
project = provision_project
|
|
28
|
+
return unless project
|
|
29
|
+
|
|
30
|
+
# Cache and apply credentials
|
|
31
|
+
cache_credentials(project)
|
|
32
|
+
apply_credentials(project)
|
|
33
|
+
|
|
34
|
+
project
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def should_provision?
|
|
40
|
+
if @config.debug
|
|
41
|
+
log_debug('Checking Vision provision conditions:')
|
|
42
|
+
log_debug(" vision_auto_provision: #{@config.vision_auto_provision}")
|
|
43
|
+
log_debug(" app_name: '#{@config.app_name}'")
|
|
44
|
+
log_debug(" vision_api_key set: #{@config.vision_api_key.to_s.strip.length.positive?}")
|
|
45
|
+
log_debug(" vision_ingest_key set: #{@config.vision_ingest_key.to_s.strip.length.positive?}")
|
|
46
|
+
log_debug(" vision_master_key set: #{@config.vision_master_key.to_s.strip.length.positive?}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
return false unless @config.vision_auto_provision
|
|
50
|
+
return false unless @config.app_name.to_s.strip.length.positive?
|
|
51
|
+
return false if @config.vision_api_key.to_s.strip.length.positive?
|
|
52
|
+
return false if @config.vision_ingest_key.to_s.strip.length.positive?
|
|
53
|
+
return false unless @config.vision_master_key.to_s.strip.length.positive?
|
|
54
|
+
|
|
55
|
+
log_debug('Will provision Vision project') if @config.debug
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def log_debug(message)
|
|
60
|
+
if @config.logger
|
|
61
|
+
@config.logger.info("[BrainzLab::Debug] #{message}")
|
|
62
|
+
else
|
|
63
|
+
puts "[BrainzLab::Debug] #{message}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def provision_project
|
|
68
|
+
uri = URI.parse("#{@config.vision_url}/api/v1/projects/provision")
|
|
69
|
+
request = Net::HTTP::Post.new(uri)
|
|
70
|
+
request['Content-Type'] = 'application/json'
|
|
71
|
+
request['X-Master-Key'] = @config.vision_master_key
|
|
72
|
+
request['User-Agent'] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
|
|
73
|
+
request.body = JSON.generate({
|
|
74
|
+
name: @config.app_name,
|
|
75
|
+
environment: @config.environment
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
response = execute(uri, request)
|
|
79
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
80
|
+
|
|
81
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
log_error("Failed to provision Vision project: #{e.message}")
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def load_cached_credentials
|
|
88
|
+
path = cache_file_path
|
|
89
|
+
return nil unless File.exist?(path)
|
|
90
|
+
|
|
91
|
+
data = JSON.parse(File.read(path), symbolize_names: true)
|
|
92
|
+
|
|
93
|
+
# Validate cached data has required keys
|
|
94
|
+
return nil unless data[:ingest_key] || data[:api_key]
|
|
95
|
+
|
|
96
|
+
data
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
log_error("Failed to load cached Vision credentials: #{e.message}")
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def cache_credentials(project)
|
|
103
|
+
FileUtils.mkdir_p(CACHE_DIR)
|
|
104
|
+
File.write(cache_file_path, JSON.generate(project))
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
log_error("Failed to cache Vision credentials: #{e.message}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def cache_file_path
|
|
110
|
+
File.join(CACHE_DIR, "#{@config.app_name}.vision.json")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def apply_credentials(project)
|
|
114
|
+
@config.vision_ingest_key = project[:ingest_key]
|
|
115
|
+
@config.vision_api_key = project[:api_key]
|
|
116
|
+
|
|
117
|
+
# Also set service name from app_name if not already set
|
|
118
|
+
@config.service ||= @config.app_name
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def execute(uri, request)
|
|
122
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
123
|
+
http.use_ssl = uri.scheme == 'https'
|
|
124
|
+
http.open_timeout = 5
|
|
125
|
+
http.read_timeout = 10
|
|
126
|
+
http.request(request)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def log_error(message)
|
|
130
|
+
return unless @config.logger
|
|
131
|
+
|
|
132
|
+
@config.logger.error("[BrainzLab] #{message}")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|