summaryrefslogtreecommitdiffhomepage
path: root/.rules/plan/03-15-keystore.md
diff options
context:
space:
mode:
Diffstat (limited to '.rules/plan/03-15-keystore.md')
-rw-r--r--.rules/plan/03-15-keystore.md168
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`.