brainzlab 0.1.0 → 0.1.2
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/CHANGELOG.md +9 -0
- data/README.md +30 -0
- data/lib/brainzlab/beacon/client.rb +209 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +341 -3
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +141 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +227 -0
- data/lib/brainzlab/dendrite/client.rb +232 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
- data/lib/brainzlab/devtools/assets/devtools.js +322 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -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 +180 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +376 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +155 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +94 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +70 -0
- data/lib/brainzlab/flux/provisioner.rb +57 -0
- data/lib/brainzlab/flux.rb +174 -0
- data/lib/brainzlab/instrumentation/active_record.rb +18 -1
- data/lib/brainzlab/instrumentation/aws.rb +179 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/resque.rb +115 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +198 -0
- data/lib/brainzlab/instrumentation/stripe.rb +164 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +104 -0
- data/lib/brainzlab/instrumentation.rb +72 -0
- data/lib/brainzlab/nerve/client.rb +217 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/instrumentation.rb +35 -2
- data/lib/brainzlab/pulse/propagation.rb +1 -1
- data/lib/brainzlab/pulse/tracer.rb +1 -1
- data/lib/brainzlab/pulse.rb +1 -1
- data/lib/brainzlab/rails/log_subscriber.rb +1 -2
- data/lib/brainzlab/rails/railtie.rb +36 -3
- data/lib/brainzlab/recall/provisioner.rb +17 -0
- data/lib/brainzlab/recall.rb +6 -1
- data/lib/brainzlab/reflex.rb +20 -5
- data/lib/brainzlab/sentinel/client.rb +218 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +62 -0
- data/lib/brainzlab/signal/provisioner.rb +55 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +290 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +265 -0
- data/lib/brainzlab/utilities/health_check.rb +296 -0
- data/lib/brainzlab/utilities/log_formatter.rb +256 -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 +198 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +268 -0
- data/lib/brainzlab/version.rb +1 -1
- data/lib/brainzlab/vision/client.rb +128 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +157 -0
- data/lib/brainzlab.rb +101 -0
- metadata +62 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module BrainzLab
|
|
8
|
+
module Vault
|
|
9
|
+
class Client
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
@base_url = config.vault_url || "https://vault.brainzlab.ai"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(key, environment:)
|
|
16
|
+
response = request(
|
|
17
|
+
:get,
|
|
18
|
+
"/api/v1/secrets/#{CGI.escape(key)}",
|
|
19
|
+
headers: { "X-Vault-Environment" => environment }
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
23
|
+
|
|
24
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
25
|
+
data[:value]
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
log_error("get", e)
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def set(key, value, environment:, description: nil, note: nil)
|
|
32
|
+
body = {
|
|
33
|
+
key: key,
|
|
34
|
+
value: value,
|
|
35
|
+
description: description,
|
|
36
|
+
note: note
|
|
37
|
+
}.compact
|
|
38
|
+
|
|
39
|
+
response = request(
|
|
40
|
+
:post,
|
|
41
|
+
"/api/v1/secrets",
|
|
42
|
+
headers: { "X-Vault-Environment" => environment },
|
|
43
|
+
body: body
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
log_error("set", e)
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def list(environment:)
|
|
53
|
+
response = request(
|
|
54
|
+
:get,
|
|
55
|
+
"/api/v1/secrets",
|
|
56
|
+
headers: { "X-Vault-Environment" => environment }
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return [] unless response.is_a?(Net::HTTPSuccess)
|
|
60
|
+
|
|
61
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
62
|
+
data[:secrets] || []
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
log_error("list", e)
|
|
65
|
+
[]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def delete(key)
|
|
69
|
+
response = request(:delete, "/api/v1/secrets/#{CGI.escape(key)}")
|
|
70
|
+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPNoContent)
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
log_error("delete", e)
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def export(environment:, format:)
|
|
77
|
+
params = { format: format }
|
|
78
|
+
response = request(
|
|
79
|
+
:get,
|
|
80
|
+
"/api/v1/sync/export",
|
|
81
|
+
headers: { "X-Vault-Environment" => environment },
|
|
82
|
+
params: params
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return {} unless response.is_a?(Net::HTTPSuccess)
|
|
86
|
+
|
|
87
|
+
case format
|
|
88
|
+
when :json
|
|
89
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
90
|
+
data[:secrets] || {}
|
|
91
|
+
else
|
|
92
|
+
response.body
|
|
93
|
+
end
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
log_error("export", e)
|
|
96
|
+
format == :json ? {} : ""
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def provision(project_id:, app_name:)
|
|
100
|
+
response = request(
|
|
101
|
+
:post,
|
|
102
|
+
"/api/v1/projects/provision",
|
|
103
|
+
body: { project_id: project_id, app_name: app_name },
|
|
104
|
+
use_service_key: true
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
log_error("provision", e)
|
|
110
|
+
false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get all provider keys for the current project
|
|
114
|
+
# Returns a hash of provider => decrypted_key
|
|
115
|
+
def get_provider_keys
|
|
116
|
+
response = request(:get, "/api/v1/provider_keys/bulk")
|
|
117
|
+
|
|
118
|
+
return {} unless response.is_a?(Net::HTTPSuccess)
|
|
119
|
+
|
|
120
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
121
|
+
# Convert to simple hash: { openai: "sk-...", anthropic: "sk-..." }
|
|
122
|
+
keys = {}
|
|
123
|
+
(data[:keys] || []).each do |key_data|
|
|
124
|
+
keys[key_data[:provider].to_sym] = key_data[:key]
|
|
125
|
+
end
|
|
126
|
+
keys
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
log_error("get_provider_keys", e)
|
|
129
|
+
{}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Get a specific provider key
|
|
133
|
+
def get_provider_key(provider:, model_type: "llm")
|
|
134
|
+
response = request(
|
|
135
|
+
:get,
|
|
136
|
+
"/api/v1/provider_keys/resolve",
|
|
137
|
+
params: { provider: provider, model_type: model_type }
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
141
|
+
|
|
142
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
143
|
+
data[:key]
|
|
144
|
+
rescue StandardError => e
|
|
145
|
+
log_error("get_provider_key", e)
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
|
|
152
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
153
|
+
|
|
154
|
+
if params
|
|
155
|
+
uri.query = URI.encode_www_form(params)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
159
|
+
http.use_ssl = uri.scheme == "https"
|
|
160
|
+
http.open_timeout = 10
|
|
161
|
+
http.read_timeout = 30
|
|
162
|
+
|
|
163
|
+
request = case method
|
|
164
|
+
when :get
|
|
165
|
+
Net::HTTP::Get.new(uri)
|
|
166
|
+
when :post
|
|
167
|
+
Net::HTTP::Post.new(uri)
|
|
168
|
+
when :put
|
|
169
|
+
Net::HTTP::Put.new(uri)
|
|
170
|
+
when :delete
|
|
171
|
+
Net::HTTP::Delete.new(uri)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Set headers
|
|
175
|
+
request["Content-Type"] = "application/json"
|
|
176
|
+
request["Accept"] = "application/json"
|
|
177
|
+
|
|
178
|
+
if use_service_key
|
|
179
|
+
request["X-Service-Key"] = @config.vault_master_key || @config.secret_key
|
|
180
|
+
else
|
|
181
|
+
auth_key = @config.vault_api_key || @config.secret_key
|
|
182
|
+
request["Authorization"] = "Bearer #{auth_key}" if auth_key
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
headers.each { |k, v| request[k] = v }
|
|
186
|
+
|
|
187
|
+
# Set body
|
|
188
|
+
request.body = body.to_json if body
|
|
189
|
+
|
|
190
|
+
http.request(request)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def log_error(operation, error)
|
|
194
|
+
BrainzLab.debug_log("[Vault::Client] #{operation} failed: #{error.message}")
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Vault
|
|
5
|
+
class Provisioner
|
|
6
|
+
def initialize(config)
|
|
7
|
+
@config = config
|
|
8
|
+
@provisioned = false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def ensure_project!
|
|
12
|
+
return if @provisioned
|
|
13
|
+
return unless @config.vault_auto_provision
|
|
14
|
+
return unless valid_auth?
|
|
15
|
+
|
|
16
|
+
@provisioned = true
|
|
17
|
+
|
|
18
|
+
# Try to provision with Platform project ID
|
|
19
|
+
project_id = detect_project_id
|
|
20
|
+
return unless project_id
|
|
21
|
+
|
|
22
|
+
client = Client.new(@config)
|
|
23
|
+
client.provision(
|
|
24
|
+
project_id: project_id,
|
|
25
|
+
app_name: @config.app_name || @config.service
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
BrainzLab.debug_log("[Vault::Provisioner] Project provisioned: #{project_id}")
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
BrainzLab.debug_log("[Vault::Provisioner] Provisioning failed: #{e.message}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def valid_auth?
|
|
36
|
+
key = @config.vault_api_key || @config.vault_master_key || @config.secret_key
|
|
37
|
+
!key.nil? && !key.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def detect_project_id
|
|
41
|
+
# Try environment variable first
|
|
42
|
+
return ENV["BRAINZLAB_PROJECT_ID"] if ENV["BRAINZLAB_PROJECT_ID"]
|
|
43
|
+
|
|
44
|
+
# Could also detect from Platform API if we have a secret key
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,268 @@
|
|
|
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
|
+
if overwrite || !ENV.key?(key_str)
|
|
39
|
+
ENV[key_str] = value.to_s
|
|
40
|
+
loaded[key_str] = value
|
|
41
|
+
BrainzLab.debug_log("[Vault] Loaded #{key_str}")
|
|
42
|
+
end
|
|
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
|
+
if overwrite || !ENV.key?(env_var)
|
|
71
|
+
ENV[env_var] = key
|
|
72
|
+
loaded[env_var] = key
|
|
73
|
+
BrainzLab.debug_log("[Vault] Loaded provider key: #{env_var}")
|
|
74
|
+
end
|
|
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
|
+
if BrainzLab.configuration.vault_cache_enabled && cache.has?(cache_key)
|
|
117
|
+
return cache.get(cache_key)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
value = client.get(key, environment: env)
|
|
121
|
+
|
|
122
|
+
if value.nil?
|
|
123
|
+
default
|
|
124
|
+
else
|
|
125
|
+
cache.set(cache_key, value) if BrainzLab.configuration.vault_cache_enabled
|
|
126
|
+
value
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Set a secret value
|
|
131
|
+
# @param key [String] The secret key
|
|
132
|
+
# @param value [String] The secret value
|
|
133
|
+
# @param environment [String, Symbol] Optional environment (defaults to current environment)
|
|
134
|
+
# @param description [String] Optional description
|
|
135
|
+
# @param note [String] Optional version note
|
|
136
|
+
# @return [Boolean] True if successful
|
|
137
|
+
def set(key, value, environment: nil, description: nil, note: nil)
|
|
138
|
+
return false unless enabled?
|
|
139
|
+
|
|
140
|
+
ensure_provisioned!
|
|
141
|
+
return false unless BrainzLab.configuration.vault_valid?
|
|
142
|
+
|
|
143
|
+
env = environment&.to_s || BrainzLab.configuration.environment
|
|
144
|
+
result = client.set(key, value, environment: env, description: description, note: note)
|
|
145
|
+
|
|
146
|
+
# Invalidate cache
|
|
147
|
+
if result && BrainzLab.configuration.vault_cache_enabled
|
|
148
|
+
cache.delete("#{env}:#{key}")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
result
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# List all secret keys
|
|
155
|
+
# @param environment [String, Symbol] Optional environment
|
|
156
|
+
# @return [Array<Hash>] List of secret metadata
|
|
157
|
+
def list(environment: nil)
|
|
158
|
+
return [] unless enabled?
|
|
159
|
+
|
|
160
|
+
ensure_provisioned!
|
|
161
|
+
return [] unless BrainzLab.configuration.vault_valid?
|
|
162
|
+
|
|
163
|
+
env = environment&.to_s || BrainzLab.configuration.environment
|
|
164
|
+
client.list(environment: env)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Delete (archive) a secret
|
|
168
|
+
# @param key [String] The secret key
|
|
169
|
+
# @return [Boolean] True if successful
|
|
170
|
+
def delete(key)
|
|
171
|
+
return false unless enabled?
|
|
172
|
+
|
|
173
|
+
ensure_provisioned!
|
|
174
|
+
return false unless BrainzLab.configuration.vault_valid?
|
|
175
|
+
|
|
176
|
+
result = client.delete(key)
|
|
177
|
+
|
|
178
|
+
# Invalidate all environment caches for this key
|
|
179
|
+
if result && BrainzLab.configuration.vault_cache_enabled
|
|
180
|
+
cache.delete_pattern("*:#{key}")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
result
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Export all secrets for an environment
|
|
187
|
+
# @param environment [String, Symbol] Environment to export
|
|
188
|
+
# @param format [Symbol] Output format (:json, :dotenv, :shell)
|
|
189
|
+
# @return [Hash, String] Exported secrets
|
|
190
|
+
def export(environment: nil, format: :json)
|
|
191
|
+
return {} unless enabled?
|
|
192
|
+
|
|
193
|
+
ensure_provisioned!
|
|
194
|
+
return {} unless BrainzLab.configuration.vault_valid?
|
|
195
|
+
|
|
196
|
+
env = environment&.to_s || BrainzLab.configuration.environment
|
|
197
|
+
client.export(environment: env, format: format)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Fetch a secret with automatic fallback
|
|
201
|
+
# @param key [String] The secret key
|
|
202
|
+
# @param env_var [String] Environment variable to fall back to
|
|
203
|
+
# @return [String, nil] The secret value
|
|
204
|
+
def fetch(key, env_var: nil)
|
|
205
|
+
value = get(key)
|
|
206
|
+
return value if value && !value.to_s.empty?
|
|
207
|
+
|
|
208
|
+
# Fall back to environment variable
|
|
209
|
+
if env_var
|
|
210
|
+
ENV[env_var]
|
|
211
|
+
else
|
|
212
|
+
ENV[key]
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Clear the secret cache
|
|
217
|
+
def clear_cache!
|
|
218
|
+
cache.clear!
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Warm the cache with all secrets
|
|
222
|
+
def warm_cache!(environment: nil)
|
|
223
|
+
return unless enabled? && BrainzLab.configuration.vault_cache_enabled
|
|
224
|
+
|
|
225
|
+
env = environment&.to_s || BrainzLab.configuration.environment
|
|
226
|
+
secrets = export(environment: env, format: :json)
|
|
227
|
+
|
|
228
|
+
secrets.each do |key, value|
|
|
229
|
+
cache.set("#{env}:#{key}", value)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# === INTERNAL ===
|
|
234
|
+
|
|
235
|
+
def ensure_provisioned!
|
|
236
|
+
return if @provisioned
|
|
237
|
+
|
|
238
|
+
@provisioned = true
|
|
239
|
+
provisioner.ensure_project!
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def provisioner
|
|
243
|
+
@provisioner ||= Provisioner.new(BrainzLab.configuration)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def client
|
|
247
|
+
@client ||= Client.new(BrainzLab.configuration)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def cache
|
|
251
|
+
@cache ||= Cache.new(BrainzLab.configuration.vault_cache_ttl)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def reset!
|
|
255
|
+
@client = nil
|
|
256
|
+
@provisioner = nil
|
|
257
|
+
@cache = nil
|
|
258
|
+
@provisioned = false
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
def enabled?
|
|
264
|
+
BrainzLab.configuration.vault_enabled
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
data/lib/brainzlab/version.rb
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
{ error: "Unauthorized: Invalid API key" }
|
|
102
|
+
when Net::HTTPForbidden
|
|
103
|
+
{ error: "Forbidden: Vision is not enabled for this project" }
|
|
104
|
+
when Net::HTTPNotFound
|
|
105
|
+
{ error: "Not found: #{path}" }
|
|
106
|
+
else
|
|
107
|
+
{ error: "HTTP #{response.code}: #{response.message}" }
|
|
108
|
+
end
|
|
109
|
+
rescue JSON::ParserError => e
|
|
110
|
+
{ error: "Invalid JSON response: #{e.message}" }
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
{ error: "Request failed: #{e.message}" }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def auth_key
|
|
116
|
+
@config.vision_ingest_key || @config.vision_api_key || @config.secret_key
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def execute(uri, request)
|
|
120
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
121
|
+
http.use_ssl = uri.scheme == "https"
|
|
122
|
+
http.open_timeout = 10
|
|
123
|
+
http.read_timeout = 300 # Long timeout for AI tasks
|
|
124
|
+
http.request(request)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|