diff options
Diffstat (limited to '.rules/plan/03-15-keystore.md')
| -rw-r--r-- | .rules/plan/03-15-keystore.md | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/.rules/plan/03-15-keystore.md b/.rules/plan/03-15-keystore.md new file mode 100644 index 0000000..75a9155 --- /dev/null +++ b/.rules/plan/03-15-keystore.md @@ -0,0 +1,168 @@ +# 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`. |
