# Phase 03 — KeyStore **Estimated time:** ~15 minutes **Touches:** `lib/dispatch/adapter/minimax/key_store.rb` (NEW), `lib/dispatch/adapter/minimax/token_store.rb` (DELETE), `lib/dispatch/adapter/minimax.rb`, `spec/dispatch/adapter/minimax/key_store_spec.rb` (NEW), `spec/dispatch/adapter/minimax/token_store_spec.rb` (DELETE). ## Goal Replace the OAuth `TokenStore` (which managed a JSON blob with access/refresh tokens) with a tiny `KeyStore` that handles a single static API key. API key resolution order, top to bottom: 1. Explicit `api_key:` constructor argument. 2. `ENV["MINIMAX_API_KEY"]`. 3. File `~/.config/dispatch/minimax_api_key` — plain UTF-8 text, key on the first line, file mode `0600`. Path is overridable via constructor keyword `key_path:` (used by tests). 4. Raise `ArgumentError, "MiniMax API key not found"`. ## Steps ### 1. Delete the existing `token_store.rb` and its spec ```text DELETE lib/dispatch/adapter/minimax/token_store.rb DELETE spec/dispatch/adapter/minimax/token_store_spec.rb ``` ### 2. Create `lib/dispatch/adapter/minimax/key_store.rb` ```ruby # frozen_string_literal: true require "fileutils" module Dispatch module Adapter class MiniMax < Base # Resolves the MiniMax API key from (in order): # 1. an explicit `api_key:` argument # 2. ENV["MINIMAX_API_KEY"] # 3. the file at `key_path` (default ~/.config/dispatch/minimax_api_key) # # Raises ArgumentError if no key can be found. class KeyStore DEFAULT_PATH = File.join(Dir.home, ".config", "dispatch", "minimax_api_key") def initialize(key_path: DEFAULT_PATH) @key_path = key_path end attr_reader :key_path # @param explicit [String, nil] caller-supplied key (highest priority) # @return [String] the resolved key # @raise [ArgumentError] if no key is found anywhere def load(explicit: nil) return explicit.to_s if explicit.is_a?(String) && !explicit.empty? env = ENV["MINIMAX_API_KEY"].to_s return env unless env.empty? if File.file?(@key_path) text = File.read(@key_path, encoding: "UTF-8").to_s line = text.lines.first.to_s.strip return line unless line.empty? end raise ArgumentError, "MiniMax API key not found. Set MINIMAX_API_KEY, " \ "write the key to #{@key_path}, or pass api_key: to the constructor." end # Persist a key to disk with mode 0600. Parent dir is created if missing. # @param value [String] # @return [String] the stored key def save(value) FileUtils.mkdir_p(File.dirname(@key_path)) File.write(@key_path, "#{value.to_s.strip}\n", encoding: "UTF-8") File.chmod(0o600, @key_path) value end # Remove the on-disk key (no-op if absent). def delete File.delete(@key_path) if File.file?(@key_path) end end end end end ``` ### 3. Wire `KeyStore` into the main adapter file In `lib/dispatch/adapter/minimax.rb`: - Add `require_relative "minimax/key_store"` near the other requires (in alphabetical order with the rest). - Update the constructor signature to: ```ruby def initialize( model: DEFAULT_MODEL, api_key: nil, key_path: nil, base_url: DEFAULT_BASE_URL, max_tokens: nil, thinking: "high", cache_retention: nil, min_request_interval: DEFAULT_MIN_REQUEST_INTERVAL, rate_limit: nil, extra_betas: [] ) ``` - Inside the body, replace the `@api_key.empty?` raise with: ```ruby store = KeyStore.new(key_path: key_path || KeyStore::DEFAULT_PATH) @api_key = store.load(explicit: api_key) ``` - Keep `@api_key` as an instance variable; do NOT store the `KeyStore` instance — the gem only ever reads the key once at construction. ### 4. Create `spec/dispatch/adapter/minimax/key_store_spec.rb` Cover at minimum: - `#load` returns the explicit argument when given. - `#load` returns `ENV["MINIMAX_API_KEY"]` when set and explicit is nil. - `#load` reads the first line of the file when present and env is unset. - `#load` strips trailing whitespace / newlines. - `#load` raises `ArgumentError` when nothing is available. - `#save` writes the key, sets mode `0600`, and creates the parent dir. - `#delete` removes the file if present and is a no-op otherwise. Use `Dir.mktmpdir` for `key_path:` and stub `ENV` with `ClimateControl` … EXCEPT: do NOT add a new gem. Use `ENV.fetch` / `ENV.delete` and reset in an `after` block, or stub directly with `allow(ENV).to receive(:[]).with("MINIMAX_API_KEY").and_return(...)`. ## Acceptance criteria - `lib/dispatch/adapter/minimax/token_store.rb` does NOT exist. - `spec/dispatch/adapter/minimax/token_store_spec.rb` does NOT exist. - `lib/dispatch/adapter/minimax/key_store.rb` exists. - `spec/dispatch/adapter/minimax/key_store_spec.rb` exists. - `grep -rn 'TokenStore' lib/ spec/` returns ZERO matches. - The constructor accepts `api_key:` and `key_path:` kwargs and resolves in the documented order. - `bundle exec rubocop --autocorrect-all` exits 0 (run via `run_tests`). - `bundle exec rspec spec/dispatch/adapter/minimax/key_store_spec.rb` passes. Other specs may still fail; that is expected and will be addressed in later phases. ## Verification Run `run_tests` with `project_path=reference/dispatch-adapter-minimax`. Resolve every rubocop offense before calling `ask_for_next_plan`. The `key_store_spec.rb` examples must all pass. Other rspec failures from not-yet-completed phases are acceptable as long as they are NOT `LoadError` / `NameError` referencing `TokenStore` or `KeyStore`.