# 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`.
