config: layer grok auth into provider-key resolution (add-grok phase 2)

ober

fd5568286eac8fb3259bb5dc7dff50b457387eb1

diff --git a/src/jcode/core/config.ss b/src/jcode/core/config.ss
index d44b973..355b72b 100644
--- a/src/jcode/core/config.ss
+++ b/src/jcode/core/config.ss
@@ -8,6 +8,10 @@
         config-provider
         config-get-provider-key
         config-default-model
+        provider-key-layers
+        detect-provider-from-list
+        *provider-env-vars*
+        *detect-provider-order*
         *config*
         *version*)
 
@@ -15,7 +19,8 @@
         :std/os/path
         :jcode/core/log
         :jcode/core/models
-        :jcode/core/secrets)
+        :jcode/core/secrets
+        :jcode/core/grok-auth)
 
 (def logger (make-logger "config"))
 
@@ -32,6 +37,9 @@
     ("openrouter"  . "OPENROUTER_API_KEY")
     ("deepseek"    . "DEEPSEEK_API_KEY")
     ("xai"         . "XAI_API_KEY")
+    ;; "grok" reuses XAI_API_KEY as its primary env var; env-key-for-provider
+    ;; falls back to GROK_CODE_XAI_API_KEY when that one is unset.
+    ("grok"        . "XAI_API_KEY")
     ("groq"        . "GROQ_API_KEY")
     ("mistral"     . "MISTRAL_API_KEY")
     ("together"    . "TOGETHER_API_KEY")
@@ -104,11 +112,16 @@
       ((not (hash-table? obj)) #f)
       (else (loop (hash-get obj (car keys)) (cdr keys))))))
 
+(def (env-nonempty var)
+  (let ((v (getenv var))) (and v (not (string=? v "")) v)))
+
 (def (env-key-for-provider provider)
-  (let ((entry (assoc provider *provider-env-vars*)))
-    (and entry
-         (let ((v (getenv (cdr entry))))
-           (and v (not (string=? v "")) v)))))
+  ;; Primary: the provider's *provider-env-vars* entry. For "grok" specifically
+  ;; we also accept GROK_CODE_XAI_API_KEY as a fallback so users can keep a
+  ;; separate key for the Grok CLI session vs the xAI console.
+  (or (let ((entry (assoc provider *provider-env-vars*)))
+        (and entry (env-nonempty (cdr entry))))
+      (and (equal? provider "grok") (env-nonempty "GROK_CODE_XAI_API_KEY"))))
 
 (def (encrypted-key-for-provider provider)
   ;; Only consult the encrypted store for providers that we know take
@@ -122,10 +135,22 @@
        (secret-store-exists?)
        (secret-store-get provider)))
 
+(def (provider-key-layers provider env-fn store-fn config-fn grok-fn)
+  ;; Layered provider-key resolution: env -> encrypted store -> jcode.json
+  ;; "providers" hash -> grok-auth-token (only for "grok"). Lookups are passed
+  ;; in as closures so unit tests can stub them; live config-get-provider-key
+  ;; wires the real env, store, config, and grok-auth functions.
+  (or (env-fn provider)
+      (store-fn provider)
+      (config-fn provider)
+      (and (equal? provider "grok") (grok-fn))))
+
 (def (config-get-provider-key provider)
-  (or (env-key-for-provider provider)
-      (encrypted-key-for-provider provider)
-      (config-ref "providers" provider "api_key")))
+  (provider-key-layers provider
+    env-key-for-provider
+    encrypted-key-for-provider
+    (lambda (p) (config-ref "providers" p "api_key"))
+    grok-auth-token))
 
 (def (config-model)
   (or (config-ref "model")
@@ -139,14 +164,24 @@
       (config-detect-provider)
       "anthropic"))
 
-(def (config-detect-provider)
-  ;; Auto-detect first provider with a configured key
-  (let loop ((names '("anthropic" "openrouter" "openai" "deepseek" "google"
-                       "xai" "groq" "mistral" "together" "cerebras" "perplexity")))
+;; Auto-detect order. Existing API-key providers come first so a configured
+;; xai/openai/etc. is preferred over an ambient ~/.grok session token; "grok"
+;; is last so it's the fallback when nothing else has a key.
+(def *detect-provider-order*
+  '("anthropic" "openrouter" "openai" "deepseek" "google"
+    "xai" "groq" "mistral" "together" "cerebras" "perplexity"
+    "grok"))
+
+(def (detect-provider-from-list names key-fn)
+  ;; Pure: return the first NAME in NAMES for which (key-fn name) is truthy.
+  (let loop ((names names))
     (cond
       ((null? names) #f)
-      ((config-get-provider-key (car names)) (car names))
+      ((key-fn (car names)) (car names))
       (#t (loop (cdr names))))))
 
+(def (config-detect-provider)
+  (detect-provider-from-list *detect-provider-order* config-get-provider-key))
+
 (def (config-api-key)
   (config-get-provider-key (config-provider)))
diff --git a/test/run.ss b/test/run.ss
index de0fa0c..eb696a5 100644
--- a/test/run.ss
+++ b/test/run.ss
@@ -5,6 +5,7 @@
         (jcode core log)
         (jcode core message)
         (jcode core grok-auth)
+        (jcode core config)
         (jcode provider provider)
         (jcode tool registry)
         (jcode tool file)
@@ -1613,6 +1614,60 @@
         '(("grok-build" . "Grok Build")))
 (check! "grok models-from #f → ()" (grok-models-from #f) '())
 
+;; ── config: grok provider-key layers + detect order ──────────────
+;; The live config-get-provider-key reads real env / store / config / ~/.grok,
+;; so we test the *pure* helpers it dispatches through: provider-key-layers
+;; (precedence) and detect-provider-from-list (auto-detect order).
+
+(section "=== config: grok provider key ===")
+
+;; *provider-env-vars* maps both grok and xai to XAI_API_KEY (env-key-for-provider
+;; layers GROK_CODE_XAI_API_KEY on top for grok only).
+(check! "env-vars: grok → XAI_API_KEY"
+        (cdr (assoc "grok" *provider-env-vars*)) "XAI_API_KEY")
+(check! "env-vars: xai still → XAI_API_KEY"
+        (cdr (assoc "xai" *provider-env-vars*)) "XAI_API_KEY")
+
+;; provider-key-layers precedence: env > store > config > grok-fn (grok only).
+(check! "layers: env wins"
+        (provider-key-layers "grok"
+          (lambda (p) "env-k") (lambda (p) "store-k") (lambda (p) "cfg-k") (lambda () "tok"))
+        "env-k")
+(check! "layers: store wins when env #f"
+        (provider-key-layers "grok"
+          (lambda (p) #f) (lambda (p) "store-k") (lambda (p) "cfg-k") (lambda () "tok"))
+        "store-k")
+(check! "layers: config wins when env+store #f"
+        (provider-key-layers "grok"
+          (lambda (p) #f) (lambda (p) #f) (lambda (p) "cfg-k") (lambda () "tok"))
+        "cfg-k")
+(check! "layers: grok-fn last (only for grok)"
+        (provider-key-layers "grok"
+          (lambda (p) #f) (lambda (p) #f) (lambda (p) #f) (lambda () "tok"))
+        "tok")
+(check! "layers: grok-fn NOT used for xai"
+        (provider-key-layers "xai"
+          (lambda (p) #f) (lambda (p) #f) (lambda (p) #f) (lambda () "tok"))
+        #f)
+(check! "layers: nothing keyed → #f"
+        (provider-key-layers "grok"
+          (lambda (p) #f) (lambda (p) #f) (lambda (p) #f) (lambda () #f))
+        #f)
+
+;; detect-provider-from-list: grok is last so existing keyed providers win.
+(check! "detect order: grok at the tail"
+        (car (reverse *detect-provider-order*)) "grok")
+(check! "detect: skip until anthropic keyed"
+        (detect-provider-from-list *detect-provider-order*
+          (lambda (p) (equal? p "anthropic")))
+        "anthropic")
+(check! "detect: grok picked when nothing else keyed"
+        (detect-provider-from-list *detect-provider-order*
+          (lambda (p) (equal? p "grok")))
+        "grok")
+(check! "detect: nothing keyed → #f"
+        (detect-provider-from-list *detect-provider-order* (lambda (p) #f)) #f)
+
 ;; ── Results ───────────────────────────────────────────────────────
 
 (printf "~n~a passed, ~a failed~n" pass-count fail-count)