summaryrefslogtreecommitdiffhomepage
path: root/.rules/plan/03-15-keystore.md
blob: 75a9155d21893bcd635b9bd504f123502613914d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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`.