diff options
-rw-r--r-- | Gemfile.lock | 7 | ||||
-rw-r--r-- | components/crypto.rb | 47 | ||||
-rw-r--r-- | components/provider.rb | 4 | ||||
-rw-r--r-- | components/providers/openai.rb | 19 | ||||
-rw-r--r-- | components/storage.rb | 33 | ||||
-rw-r--r-- | controllers/cartridges.rb | 2 | ||||
-rw-r--r-- | controllers/instance.rb | 8 | ||||
-rw-r--r-- | controllers/session.rb | 12 | ||||
-rw-r--r-- | nano-bots.gemspec | 1 | ||||
-rw-r--r-- | ports/dsl/nano-bots.rb | 15 |
10 files changed, 122 insertions, 26 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index facc989..f5acf72 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,7 @@ PATH faraday (~> 2.7, >= 2.7.5) pry (~> 0.14.2) rainbow (~> 3.1, >= 3.1.1) + rbnacl (~> 7.1, >= 7.1.1) ruby-openai (~> 4.0) sweet-moon (~> 0.0.7) @@ -35,6 +36,8 @@ GEM coderay (~> 1.1) method_source (~> 1.0) rainbow (3.1.1) + rbnacl (7.1.1) + ffi regexp_parser (2.8.0) rexml (3.2.5) rspec (3.12.0) @@ -50,7 +53,7 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-support (3.12.0) - rubocop (1.51.0) + rubocop (1.52.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.2.0.0) @@ -60,7 +63,7 @@ GEM rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.28.1) + rubocop-ast (1.29.0) parser (>= 3.2.1.0) rubocop-capybara (2.18.0) rubocop (~> 1.41) diff --git a/components/crypto.rb b/components/crypto.rb new file mode 100644 index 0000000..3f97f57 --- /dev/null +++ b/components/crypto.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'singleton' +require 'rbnacl' +require 'base64' + +module NanoBot + module Components + class Crypto + include Singleton + + def initialize + password = ENV.fetch('NANO_BOTS_ENCRYPTION_PASSWORD', nil) + + password = 'UNSAFE' unless password && password != '' + + @box = RbNaCl::SecretBox.new(RbNaCl::Hash.sha256(password)) + @fixed_nonce = RbNaCl::Hash.sha256(password)[0...@box.nonce_bytes] + end + + def encrypt(content, soft: false) + return content unless @box + + nonce = soft ? @fixed_nonce : RbNaCl::Random.random_bytes(@box.nonce_bytes) + Base64.urlsafe_encode64(nonce + @box.encrypt(nonce, content)) + end + + def decrypt(content) + return content unless @box + + decoded_content = Base64.urlsafe_decode64(content) + nonce = decoded_content[0...@box.nonce_bytes] + cipher_text = decoded_content[@box.nonce_bytes..] + + @box.decrypt(nonce, cipher_text) + end + + def self.encrypt(content, soft: false) + instance.encrypt(content, soft:) + end + + def self.decrypt(content) + instance.decrypt(content) + end + end + end +end diff --git a/components/provider.rb b/components/provider.rb index dbfc8bd..3138cc4 100644 --- a/components/provider.rb +++ b/components/provider.rb @@ -7,10 +7,10 @@ require_relative './providers/openai' module NanoBot module Components class Provider - def self.new(provider) + def self.new(provider, environment: {}) case provider[:name] when 'openai' - Providers::OpenAI.new(provider[:settings]) + Providers::OpenAI.new(provider[:settings], environment:) else raise "Unsupported provider #{provider[:name]}" end diff --git a/components/providers/openai.rb b/components/providers/openai.rb index c0a6639..c64a588 100644 --- a/components/providers/openai.rb +++ b/components/providers/openai.rb @@ -3,6 +3,7 @@ require 'openai' require_relative './base' +require_relative '../crypto' module NanoBot module Components @@ -15,8 +16,9 @@ module NanoBot attr_reader :settings - def initialize(settings) + def initialize(settings, environment: {}) @settings = settings + @environment = environment @client = ::OpenAI::Client.new( uri_base: "#{@settings[:credentials][:address].sub(%r{/$}, '')}/", @@ -46,11 +48,16 @@ module NanoBot ) end - payload = { - model: @settings[:model], - user: @settings[:credentials][:'user-identifier'], - messages: - } + user = @settings[:credentials][:'user-identifier'] + + user_suffix = @environment && ( + @environment['NANO_BOTS_USER_IDENTIFIER'] || + @environment[:NANO_BOTS_USER_IDENTIFIER] + ) + + user = "#{user}/#{user_suffix}" if user_suffix && user_suffix != '' + + payload = { model: @settings[:model], user: Crypto.encrypt(user, soft: true), messages: } CHAT_SETTINGS.each do |key| payload[key] = @settings[key] if @settings.key?(key) diff --git a/components/storage.rb b/components/storage.rb index cba3dd0..b6d8910 100644 --- a/components/storage.rb +++ b/components/storage.rb @@ -3,11 +3,12 @@ require 'babosa' require_relative '../logic/helpers/hash' +require_relative './crypto' module NanoBot module Components class Storage - def self.build_path_and_ensure_state_file!(key, cartridge) + def self.build_path_and_ensure_state_file!(key, cartridge, environment: {}) path = [ Logic::Helpers::Hash.fetch(cartridge, %i[state directory]), ENV.fetch('NANO_BOTS_STATE_DIRECTORY', nil) @@ -17,14 +18,38 @@ module NanoBot path = "#{user_home!.sub(%r{/$}, '')}/.local/state/nano-bots" if path.nil? - path = "#{path.sub(%r{/$}, '')}/ruby-nano-bots/#{cartridge[:meta][:author].to_slug.normalize}" + prefix = environment && ( + environment['NANO_BOTS_USER_IDENTIFIER'] || + environment[:NANO_BOTS_USER_IDENTIFIER] + ) + + path = "#{path.sub(%r{/$}, '')}/ruby-nano-bots/vault" + + if prefix + normalized = prefix.split('/').map do |part| + Crypto.encrypt( + part.to_s.gsub('.', '-').force_encoding('UTF-8').to_slug.normalize, + soft: true + ) + end.join('/') + + path = "#{path}/#{normalized}" + end + + path = "#{path}/#{cartridge[:meta][:author].to_slug.normalize}" path = "#{path}/#{cartridge[:meta][:name].to_slug.normalize}" - path = "#{path}/#{cartridge[:meta][:version].to_s.gsub('.', '-').to_slug.normalize}/#{key}" + path = "#{path}/#{cartridge[:meta][:version].to_s.gsub('.', '-').to_slug.normalize}" + path = "#{path}/#{Crypto.encrypt(key, soft: true)}" path = "#{path}/state.json" FileUtils.mkdir_p(File.dirname(path)) - File.write(path, JSON.generate({ key:, history: [] })) unless File.exist?(path) + unless File.exist?(path) + File.write( + path, + Crypto.encrypt(JSON.generate({ key:, history: [] })) + ) + end path end diff --git a/controllers/cartridges.rb b/controllers/cartridges.rb index 151cc46..fe0d56e 100644 --- a/controllers/cartridges.rb +++ b/controllers/cartridges.rb @@ -39,7 +39,7 @@ module NanoBot rescue StandardError => _e end - cartridges.sort_by { |cartridge| cartridge[:meta][:name] } + cartridges = cartridges.sort_by { |cartridge| cartridge[:meta][:name] } cartridges.prepend( { system: { id: '-' }, meta: { name: 'Default', symbol: '🤖' } } diff --git a/controllers/instance.rb b/controllers/instance.rb index a982261..d4e0c1b 100644 --- a/controllers/instance.rb +++ b/controllers/instance.rb @@ -13,14 +13,16 @@ require_relative './session' module NanoBot module Controllers class Instance - def initialize(cartridge_path:, stream:, state: nil) + def initialize(cartridge_path:, stream:, state: nil, environment: {}) @stream = stream load_cartridge!(cartridge_path) - provider = Components::Provider.new(@cartridge[:provider]) + provider = Components::Provider.new(@cartridge[:provider], environment:) - @session = Session.new(provider:, cartridge: @cartridge, state:, stream: @stream) + @session = Session.new( + provider:, cartridge: @cartridge, state:, stream: @stream, environment: + ) end def cartridge diff --git a/controllers/session.rb b/controllers/session.rb index 270b623..4694911 100644 --- a/controllers/session.rb +++ b/controllers/session.rb @@ -9,6 +9,7 @@ require_relative '../logic/cartridge/streaming' require_relative '../logic/cartridge/interaction' require_relative '../components/storage' require_relative '../components/adapter' +require_relative '../components/crypto' module NanoBot module Controllers @@ -17,7 +18,7 @@ module NanoBot class Session attr_accessor :stream - def initialize(provider:, cartridge:, state: nil, stream: $stdout) + def initialize(provider:, cartridge:, state: nil, stream: $stdout, environment: {}) @stream = stream @provider = provider @cartridge = cartridge @@ -28,8 +29,9 @@ module NanoBot @state = { history: [] } else @state_path = Components::Storage.build_path_and_ensure_state_file!( - state.strip, @cartridge + state.strip, @cartridge, environment: ) + @state = load_state end end @@ -39,11 +41,13 @@ module NanoBot end def load_state - @state = Logic::Helpers::Hash.symbolize_keys(JSON.parse(File.read(@state_path))) + @state = Logic::Helpers::Hash.symbolize_keys(JSON.parse( + Components::Crypto.decrypt(File.read(@state_path)) + )) end def store_state! - File.write(@state_path, JSON.generate(@state)) + File.write(@state_path, Components::Crypto.encrypt(JSON.generate(@state))) end def boot(mode:) diff --git a/nano-bots.gemspec b/nano-bots.gemspec index eb6f748..d66ec6c 100644 --- a/nano-bots.gemspec +++ b/nano-bots.gemspec @@ -36,6 +36,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'faraday', '~> 2.7', '>= 2.7.5' spec.add_dependency 'pry', '~> 0.14.2' spec.add_dependency 'rainbow', '~> 3.1', '>= 3.1.1' + spec.add_dependency 'rbnacl', '~> 7.1', '>= 7.1.1' spec.add_dependency 'ruby-openai', '~> 4.0' spec.add_dependency 'sweet-moon', '~> 0.0.7' diff --git a/ports/dsl/nano-bots.rb b/ports/dsl/nano-bots.rb index 89da466..cbfe7f9 100644 --- a/ports/dsl/nano-bots.rb +++ b/ports/dsl/nano-bots.rb @@ -9,8 +9,13 @@ require_relative '../../controllers/interfaces/cli' require_relative '../../components/stream' module NanoBot - def self.new(cartridge: '-', state: '-') - Controllers::Instance.new(cartridge_path: cartridge, state:, stream: Components::Stream.new) + def self.new(cartridge: '-', state: '-', environment: {}) + Controllers::Instance.new( + cartridge_path: cartridge, + state:, + stream: Components::Stream.new, + environment: + ) end def self.cartridges @@ -21,8 +26,10 @@ module NanoBot Controllers::Interfaces::CLI.handle! end - def self.repl(cartridge: '-', state: '-') - Controllers::Instance.new(cartridge_path: cartridge, state:, stream: $stdout).repl + def self.repl(cartridge: '-', state: '-', environment: {}) + Controllers::Instance.new( + cartridge_path: cartridge, state:, stream: $stdout, environment: + ).repl end def self.version |