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.
Files changed (121) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +11 -0
  4. data/README.md +571 -0
  5. data/lib/brainzlab/beacon/client.rb +227 -0
  6. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  7. data/lib/brainzlab/beacon.rb +215 -0
  8. data/lib/brainzlab/configuration.rb +676 -0
  9. data/lib/brainzlab/context.rb +90 -0
  10. data/lib/brainzlab/cortex/cache.rb +59 -0
  11. data/lib/brainzlab/cortex/client.rb +159 -0
  12. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  13. data/lib/brainzlab/cortex.rb +223 -0
  14. data/lib/brainzlab/debug.rb +305 -0
  15. data/lib/brainzlab/dendrite/client.rb +250 -0
  16. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  17. data/lib/brainzlab/dendrite.rb +195 -0
  18. data/lib/brainzlab/development/logger.rb +150 -0
  19. data/lib/brainzlab/development/store.rb +121 -0
  20. data/lib/brainzlab/development.rb +72 -0
  21. data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
  22. data/lib/brainzlab/devtools/assets/devtools.js +396 -0
  23. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  24. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
  25. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  26. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  27. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  28. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  29. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  30. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  31. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  32. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  33. data/lib/brainzlab/devtools.rb +75 -0
  34. data/lib/brainzlab/errors.rb +490 -0
  35. data/lib/brainzlab/flux/buffer.rb +96 -0
  36. data/lib/brainzlab/flux/client.rb +68 -0
  37. data/lib/brainzlab/flux/provisioner.rb +124 -0
  38. data/lib/brainzlab/flux.rb +184 -0
  39. data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
  40. data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
  41. data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
  42. data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
  43. data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
  44. data/lib/brainzlab/instrumentation/action_view.rb +380 -0
  45. data/lib/brainzlab/instrumentation/active_job.rb +569 -0
  46. data/lib/brainzlab/instrumentation/active_record.rb +559 -0
  47. data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
  48. data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
  49. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  50. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  51. data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
  52. data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
  53. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  54. data/lib/brainzlab/instrumentation/faraday.rb +181 -0
  55. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  56. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  57. data/lib/brainzlab/instrumentation/graphql.rb +252 -0
  58. data/lib/brainzlab/instrumentation/httparty.rb +193 -0
  59. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  60. data/lib/brainzlab/instrumentation/net_http.rb +114 -0
  61. data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
  62. data/lib/brainzlab/instrumentation/railties.rb +134 -0
  63. data/lib/brainzlab/instrumentation/redis.rb +324 -0
  64. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  65. data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
  66. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  67. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  68. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  69. data/lib/brainzlab/instrumentation.rb +360 -0
  70. data/lib/brainzlab/nerve/client.rb +235 -0
  71. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  72. data/lib/brainzlab/nerve.rb +219 -0
  73. data/lib/brainzlab/pulse/client.rb +203 -0
  74. data/lib/brainzlab/pulse/instrumentation.rb +401 -0
  75. data/lib/brainzlab/pulse/propagation.rb +241 -0
  76. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  77. data/lib/brainzlab/pulse/tracer.rb +111 -0
  78. data/lib/brainzlab/pulse.rb +294 -0
  79. data/lib/brainzlab/rails/log_formatter.rb +807 -0
  80. data/lib/brainzlab/rails/log_subscriber.rb +334 -0
  81. data/lib/brainzlab/rails/railtie.rb +606 -0
  82. data/lib/brainzlab/recall/buffer.rb +66 -0
  83. data/lib/brainzlab/recall/client.rb +158 -0
  84. data/lib/brainzlab/recall/logger.rb +116 -0
  85. data/lib/brainzlab/recall/provisioner.rb +130 -0
  86. data/lib/brainzlab/recall.rb +175 -0
  87. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  88. data/lib/brainzlab/reflex/client.rb +150 -0
  89. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  90. data/lib/brainzlab/reflex.rb +421 -0
  91. data/lib/brainzlab/sentinel/client.rb +236 -0
  92. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  93. data/lib/brainzlab/sentinel.rb +165 -0
  94. data/lib/brainzlab/signal/client.rb +60 -0
  95. data/lib/brainzlab/signal/provisioner.rb +115 -0
  96. data/lib/brainzlab/signal.rb +136 -0
  97. data/lib/brainzlab/synapse/client.rb +308 -0
  98. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  99. data/lib/brainzlab/synapse.rb +270 -0
  100. data/lib/brainzlab/testing/event_store.rb +377 -0
  101. data/lib/brainzlab/testing/helpers.rb +650 -0
  102. data/lib/brainzlab/testing/matchers.rb +391 -0
  103. data/lib/brainzlab/testing.rb +327 -0
  104. data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
  105. data/lib/brainzlab/utilities/health_check.rb +294 -0
  106. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  107. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  108. data/lib/brainzlab/utilities.rb +17 -0
  109. data/lib/brainzlab/vault/cache.rb +80 -0
  110. data/lib/brainzlab/vault/client.rb +216 -0
  111. data/lib/brainzlab/vault/provisioner.rb +49 -0
  112. data/lib/brainzlab/vault.rb +262 -0
  113. data/lib/brainzlab/version.rb +5 -0
  114. data/lib/brainzlab/vision/client.rb +175 -0
  115. data/lib/brainzlab/vision/provisioner.rb +136 -0
  116. data/lib/brainzlab/vision.rb +155 -0
  117. data/lib/brainzlab-sdk.rb +3 -0
  118. data/lib/brainzlab.rb +306 -0
  119. data/lib/generators/brainzlab/install/install_generator.rb +63 -0
  120. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  121. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ VERSION = '0.1.14'
5
+ 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