ai-consultants 4.0.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -188,6 +188,10 @@ QWEN3_CMD=qwen
188
188
  # is installed; the xAI API is selected when the CLI is unavailable and a key
189
189
  # exists. A failed CLI launch also falls back to the API at runtime.
190
190
  # GROK_USE_API=false
191
+ # Grok OAuth concurrency: shared keeps one runner-owned credential generation
192
+ # so parallel consultants can safely reuse refresh rotation. serialized holds
193
+ # the adapter lock for the full CLI call and is intended for diagnostics.
194
+ # GROK_OAUTH_MODE=shared
191
195
 
192
196
  # =============================================================================
193
197
  # API-ONLY AND FALLBACK CONFIGURATION
@@ -205,6 +209,7 @@ GROK_CMD=grok
205
209
  GROK_MODEL=grok-4.6
206
210
  GROK_TIMEOUT=180
207
211
  GROK_MAX_TURNS=4
212
+ GROK_OAUTH_MODE=shared
208
213
  GROK_API_MAX_TOKENS=4096
209
214
  GROK_API_URL=https://api.x.ai/v1/chat/completions
210
215
  # GROK_REASONING_EFFORT= # max_quality sets xhigh (Grok Build maximum)
package/CLAUDE.md CHANGED
@@ -6,7 +6,7 @@ AI Consultants is a multi-model coverage system that queries up to 10 AI consult
6
6
 
7
7
  **Self-Exclusion**: The invoking agent is automatically excluded from both the panel and synthesis. Claude Code won't query or synthesize with Claude, Codex CLI won't query or synthesize with Codex, etc.
8
8
 
9
- **Version**: 4.0.0
9
+ **Version**: 4.0.2
10
10
 
11
11
  ## Distribution
12
12
 
@@ -760,6 +760,26 @@ curl -fsSL https://raw.githubusercontent.com/matteoscurati/ai-consultants/main/s
760
760
 
761
761
  ## Changelog
762
762
 
763
+ ### v4.0.2
764
+
765
+ - **The Grok adapter used to copy a rotating OAuth credential into a disposable HOME and delete any refreshed token at exit.** Parallel calls amplified the race: both could seed from one refresh token, one rotation vanished with its temp directory, and the ambient session could be left stale. `scripts/lib/grok_oauth.sh` now owns a durable generation under the XDG data root and `scripts/query_grok.sh` defaults to `GROK_OAUTH_MODE=shared`.
766
+ - **Shared means credential state only.** Every invocation still has a private HOME, workspace, prompt file, output, capability-probe HOME, permission mode, strict sandbox, empty tool surface, model, and effort. Concurrent processes use one runner-owned `GROK_HOME`, allowing Grok Build's native auth lock to coordinate refresh while inference remains outside the ai-consultants lock.
767
+ - **Publication is optimistic and atomic.** A recoverable directory lock bounds seed, bootstrap, inventory, and publication; valid rotation is staged mode 600 and renamed into the ambient `auth.json`. The marker binds generation and last-synced SHA-256. If a concurrent external `grok login` changes the ambient digest, that login wins, the consultation fails temporarily, and no API fallback occurs.
768
+ - **Malformed and obsolete state fails closed but recovers forward.** Corrupt credentials never replace ambient auth; dead locks are reclaimed; unpersistible refreshes publish no answer. A later run reseeds from valid ambient state. Generations carrying the exact legacy runner-owned `config.toml` signature are superseded without modification or deletion.
769
+ - **Grok 1.0.4 required exact-transport fixes beyond the reference implementation.** Its pristine-home bootstrap lazily creates metadata/cache/DB state; capability probes must use a separate credential-free HOME; authenticated inventory is bounded and coordinated. Provider-owned `config.toml` cannot be replaced before each inventory, and the display sentence “You are logged in with grok.com.” is not stable enough to be an auth gate. Exit 0, a still-valid credential, and the requested inventory model are the attestation.
770
+ - **Secrets remain private.** The prompt and tokens never enter argv. Grok dispatch uses redacted `run_query` errors, so token-shaped provider stderr is absent from stdout, normal logs, error envelopes, and artifacts. Forced API mode remains stateless; missing/auth-unavailable CLI fallback and post-dispatch fallback suppression are unchanged.
771
+ - **Verification closes both concurrency layers.** The adapter suite grows to 88 checks and covers simultaneous fake dispatch, rotation/reuse, external-login CAS, corrupt state, dead locks, atomic policy reconciliation, legacy migration, stateless API mode, serialized mode, and token leakage. A real two-process `grok-4.6`/`xhigh` smoke returned structured `PONG` twice with exit 0, no workspace changes, no API fallback, and fully aligned ambient/generation/marker state. Full gate: 23/23 suites, 452/452 core checks, ShellCheck and PR #14 CI green.
772
+
773
+ Deliberate residuals: superseded generations are retained because deleting one that an active process still references would be unsafe. Real forced refresh rotation and a real concurrent `grok login` were not induced; those conflict paths are verified with controlled fakes. The separate local 4.1.0 model-default WIP is not part of this release.
774
+
775
+ ### v4.0.1
776
+
777
+ - **The installer's first line of user-visible output was six major/minor generations stale.** `scripts/install.sh:53` printed `AI Consultants v2.10 - Installation` even while the public binary, package metadata, npm registry, active checkout, and doctor all reported v4.0.0. That did not change installed bytes or runtime behavior, but it made a successful v4 install look wrong at the exact moment users were deciding whether to trust it.
778
+ - **The banner is now deliberately version-neutral, not dynamically coupled to a tenth version surface.** The curl-piped installer prints before it has cloned or updated the checkout, so sourcing `scripts/config.sh` there would either be impossible on a fresh install or would report the old local version during an update. A static `AI Consultants - Installation` label stays truthful throughout both paths and does not need another release-time rewrite.
779
+ - **The failure is regression-tested through rendered output.** `scripts/test_install.sh` sources the existing define-only path, calls `print_header`, requires the installer label, and rejects `AI Consultants v<major>.<minor>` in the banner. The focused installer suite grows to 47 checks; the full 23-suite gate and ShellCheck pass on the 4.0.1 surfaces.
780
+
781
+ Deliberately unchanged: no runtime, model, preset, credential, host command, or minisite copy changes. The showcase site already reflects the ten-consultant v4 panel; after npm serves 4.0.1 it needs only its two mechanical version surfaces bumped.
782
+
763
783
  ### v4.0.0
764
784
 
765
785
  - **`max_quality` now means the full ten-consultant panel at each exact transport's strongest supported effort and response budget.** GLM and DeepSeek run at `max`; Grok uses its highest supported setting. K3-256k, MiniMax M3, and Qwen3.8-Max remain confined to the maximum tier, with Qwen promoted only when the authenticated Token Plan transport is already configured.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # AI Consultants v4.0.0
1
+ # AI Consultants v4.0.2
2
2
 
3
3
  > **Coverage, not a single guess.** A panel of up to 10 frontier models from different vendors fans out on your question in parallel and hands you the *union* of what they collectively see — the risks, edge cases, and approaches a single model misses.
4
4
 
5
- [![Version](https://img.shields.io/badge/version-4.0.0-blue.svg)](https://github.com/matteoscurati/ai-consultants)
5
+ [![Version](https://img.shields.io/badge/version-4.0.2-blue.svg)](https://github.com/matteoscurati/ai-consultants)
6
6
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
7
  [![Claude Code Skill](https://img.shields.io/badge/Claude%20Code-Skill-orange.svg)](https://docs.anthropic.com/en/docs/claude-code/skills)
8
8
  [![GitHub stars](https://img.shields.io/github/stars/matteoscurati/ai-consultants?style=social)](https://github.com/matteoscurati/ai-consultants)
@@ -389,7 +389,16 @@ Grok uses Grok Build with `grok-4.6` in an isolated, tool-free sandbox. Prompts
389
389
  are passed through a private file rather than process arguments. It falls back
390
390
  to the xAI API only when the CLI is missing, cannot launch, or has no usable
391
391
  authentication and `GROK_API_KEY` is configured; post-launch request failures
392
- are surfaced without a silent API charge. Qwen3 and MiniMax can also switch
392
+ are surfaced without a silent API charge. Grok CLI OAuth defaults to concurrent
393
+ `shared` mode: HOME, workspace, prompt, output, permissions, and agent state
394
+ remain isolated per invocation, while processes share one runner-owned
395
+ persistent `GROK_HOME` so the CLI's own auth lock can coordinate refresh.
396
+ Credential adoption/publication uses a short lock and digest CAS, so an
397
+ external `grok login` wins and the affected run fails without overwriting it or
398
+ falling back to the API. Set `GROK_OAUTH_MODE=serialized` only for diagnostic
399
+ full-run serialization. A new generation performs one locked inventory
400
+ bootstrap because Grok 1.0.4 lazily initializes home metadata; inference is
401
+ never serialized by that bootstrap. Qwen3 and MiniMax can also switch
393
402
  from their CLI to API transport. Gemini, Codex, Claude, and Mistral are
394
403
  CLI/API switchable. Gemini, Grok, and Kimi verify the requested model
395
404
  against the CLI inventory before dispatch when that inventory exists. Grok and
package/SKILL.md CHANGED
@@ -5,10 +5,10 @@ license: MIT
5
5
  compatibility: Requires bash, jq, and at least 2 AI CLI tools (agy, codex, vibe, etc.). macOS and Linux.
6
6
  metadata:
7
7
  author: matteoscurati
8
- version: 4.0.0
8
+ version: 4.0.2
9
9
  ---
10
10
 
11
- # AI Consultants v4.0.0 - AI Expert Panel
11
+ # AI Consultants v4.0.2 - AI Expert Panel
12
12
 
13
13
  **Coverage, not a single guess.** Convene a panel of AI "consultants" from different vendors for coding questions: they fan out in parallel and you get the *union* of what they collectively see — the risks, edge cases, and approaches a single model misses. Each consultant has a **configurable persona** that decorrelates its analysis.
14
14
 
@@ -88,6 +88,14 @@ Configuration (presets, strategies, features, personas, API keys) can be managed
88
88
 
89
89
  **CLI/API Mode**: Gemini, Codex, Claude, Mistral, Qwen, Grok, and MiniMax can switch between CLI and API mode via `*_USE_API` environment variables. Grok is CLI-first on Grok Build with `grok-4.6` and falls back to the xAI API when the CLI cannot run. Gemini auto-selects API mode when `GEMINI_API_KEY` is set (no `agy` install needed) and the CLI otherwise. Mistral keeps separate API (`MISTRAL_MODEL`) and Vibe (`MISTRAL_CLI_MODEL`) identifiers.
90
90
 
91
+ Grok CLI OAuth defaults to `GROK_OAUTH_MODE=shared`: concurrent invocations
92
+ share one runner-owned persistent `GROK_HOME` so the vendor auth lock can
93
+ coordinate refresh, while HOME, workspace, prompt, output, permissions, and
94
+ agent state remain isolated per invocation. A short adapter lock publishes a
95
+ valid rotated credential atomically; an external `grok login` wins the CAS and
96
+ makes the affected run fail without API fallback. `serialized` remains an
97
+ explicit diagnostic mode. Forced API mode stays stateless.
98
+
91
99
  Grok and Kimi are compatible by capability rather than by CLI version. Before
92
100
  dispatch, their adapters verify the required headless arguments, requested
93
101
  model, and structured-output surface. Compatible version changes are accepted;
@@ -1,4 +1,4 @@
1
- # Cost Rates - AI Consultants v4.0.0
1
+ # Cost Rates - AI Consultants v4.0.2
2
2
 
3
3
  This page documents the per-token rates used by the cost tracking system.
4
4
 
package/docs/SETUP.md CHANGED
@@ -396,6 +396,17 @@ INVOKING_AGENT=codex ./scripts/consult_all.sh "Question"
396
396
 
397
397
  Seven consultants can switch between CLI mode (using local CLI tools) and API mode (direct API calls): **Gemini, Codex, Claude, Mistral, Qwen3, Grok, and MiniMax**. Grok is CLI-first and runs in an isolated, tool-free strict sandbox. It falls back automatically only when Grok Build is missing, cannot launch, or lacks usable authentication and a key is available; request failures after launch are surfaced without an API fallback.
398
398
 
399
+ Grok's normal OAuth mode is `GROK_OAUTH_MODE=shared`, which is safe for the
400
+ parallel panel: each call gets a private HOME/workspace/prompt/output, while a
401
+ runner-owned persistent `GROK_HOME` lets Grok Build coordinate token refresh
402
+ with its own lock. A short ai-consultants lock atomically seeds and publishes
403
+ valid generations; a concurrent external `grok login` wins the CAS and makes
404
+ the affected run fail without an API fallback. Use
405
+ `GROK_OAUTH_MODE=serialized` only as a diagnostic fallback. Forced API mode is
406
+ stateless and does not read or create the shared OAuth generation. A newly
407
+ created shared generation runs one locked inventory bootstrap to let Grok 1.0.4
408
+ initialize its home metadata; the lock is released before inference.
409
+
399
410
  ### Why Use API Mode?
400
411
 
401
412
  - **No CLI installation required**: Use API keys without installing CLI tools
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.0.0",
2
+ "version": "4.0.2",
3
3
  "updated": "2026-08-23",
4
4
  "models": {
5
5
  "_comment_units": "All rates are USD per 1,000 tokens (per-1K). lib/costs.sh computes cost as (token_count / 1000) * rate, so every value here MUST be a per-1K rate = (provider $/1M price) / 1000. Do NOT paste $/MTok figures directly.",
@@ -0,0 +1,57 @@
1
+ # Release v4.0.1
2
+
3
+ **Date:** 2026-08-24
4
+
5
+ **Type:** Patch — truthful installer presentation
6
+
7
+ **Previous:** v4.0.0
8
+
9
+ ## Highlights
10
+
11
+ - The installer no longer displays the obsolete label `AI Consultants v2.10` while installing a 4.x package.
12
+ - The banner is intentionally version-neutral, preventing the same drift in future releases.
13
+ - A hermetic regression test renders the banner and rejects hardcoded package versions.
14
+
15
+ ## What's New
16
+
17
+ ### A truthful installer banner
18
+
19
+ The curl and git-clone installation paths previously began with a hardcoded `AI Consultants v2.10 - Installation` banner. The installed package, public binary, and runtime were correctly at v4.0.0, but the stale presentation made a successful installation look inconsistent.
20
+
21
+ The banner now reads simply `AI Consultants - Installation`. This is more reliable than loading the package version dynamically: on a fresh curl-piped install there is no checkout to read yet, while on an update the checkout still contains the old version when the banner is printed.
22
+
23
+ ### Regression coverage
24
+
25
+ The installer suite now checks the rendered header itself. It requires the version-neutral installer label and fails if a package-style SemVer is hardcoded into that banner again.
26
+
27
+ ## Breaking Changes
28
+
29
+ None. Installation behavior, files, commands, configuration, credentials, model selection, and runtime output are unchanged.
30
+
31
+ ## Upgrade Guide
32
+
33
+ ```bash
34
+ # npx, after npm publishes 4.0.1
35
+ npx ai-consultants version
36
+
37
+ # active git-clone installation
38
+ cd ~/.claude/skills/ai-consultants && git pull
39
+ ```
40
+
41
+ No configuration or authentication migration is required.
42
+
43
+ ## Verification
44
+
45
+ - Installer-focused suite: 47/47 checks
46
+ - Full local gate: 23/23 suites
47
+ - ShellCheck: passed
48
+ - Installer banner rendered directly from the shipped helper: passed
49
+ - Showcase site audit: ten consultant cards, no Cursor consultant card, correct v4 preset counts, ready for the mechanical 4.0.1 sync after npm publication
50
+
51
+ ## Commits
52
+
53
+ - Release commit added at publication time
54
+
55
+ ## Contributors
56
+
57
+ - @matteoscurati
@@ -0,0 +1,98 @@
1
+ # Release v4.0.2
2
+
3
+ **Date:** 2026-08-28
4
+
5
+ **Type:** Patch — concurrency-safe Grok OAuth
6
+
7
+ **Previous:** v4.0.1
8
+
9
+ ## Highlights
10
+
11
+ - Parallel Grok Build consultations can now share rotating OAuth safely without sharing agent state.
12
+ - Refreshed credentials survive temporary HOME cleanup and are published atomically to the ambient session.
13
+ - A concurrent external `grok login` always wins; the affected consultation fails without overwriting it or creating an API charge.
14
+ - A real two-process `grok-4.6` smoke returned two structured `PONG` responses with exit 0.
15
+
16
+ ## What's New
17
+
18
+ ### Shared OAuth by default
19
+
20
+ `GROK_OAUTH_MODE=shared` is now the normal Grok CLI path. The runner maintains one persistent credential generation under:
21
+
22
+ ```text
23
+ $XDG_DATA_HOME/ai-consultants/grok-shared-oauth
24
+ ```
25
+
26
+ Only `GROK_HOME` is shared. Each consultation still receives an independent HOME, workspace, private prompt, output, capability-probe environment, strict sandbox, empty tool surface, model, and effort.
27
+
28
+ Grok Build's own auth lock coordinates token refresh between concurrent processes. The ai-consultants lock is used only for bounded seed, first-use bootstrap, authenticated inventory, and publication — never for inference.
29
+
30
+ ### Atomic refresh publication and CAS
31
+
32
+ The shared marker binds the active generation to the SHA-256 of the credential last synchronized with ambient `~/.grok/auth.json`.
33
+
34
+ After a run:
35
+
36
+ - unchanged valid credentials require no write;
37
+ - a valid rotation is copied into a mode-600 candidate and atomically renamed;
38
+ - malformed rotation fails closed;
39
+ - a changed ambient digest means an external login won, so the run fails temporarily and publishes no successful answer;
40
+ - a publication failure never triggers the separately billed xAI API fallback.
41
+
42
+ `GROK_OAUTH_MODE=serialized` remains available for diagnostic comparison and holds the runner lock across the complete CLI call.
43
+
44
+ ### Provider-owned configuration
45
+
46
+ Grok 1.0.4 lazily creates provider-required metadata, caches, DBs, and `config.toml`. ai-consultants therefore preserves that file and stores its own atomic contract in `.ai-consultants-policy.json`. The effective agent restrictions remain pinned through the invocation arguments.
47
+
48
+ Generations produced by the earlier development runner are recognized from their exact legacy config signature and superseded without deletion or mutation.
49
+
50
+ ### Sanitized failures
51
+
52
+ Grok requests use redacted retry logging. Token-shaped provider stderr remains only in ephemeral private storage and never reaches argv, stdout, logs, error envelopes, or consultation artifacts.
53
+
54
+ ## Breaking Changes
55
+
56
+ None. `shared` replaces the unsafe disposable-copy behavior for CLI OAuth, but model, effort, permissions, sandbox, response schema, API transport, and fallback classification remain unchanged.
57
+
58
+ Users who need full-run serialization for diagnosis can set:
59
+
60
+ ```bash
61
+ GROK_OAUTH_MODE=serialized
62
+ ```
63
+
64
+ ## Upgrade Guide
65
+
66
+ ```bash
67
+ # npx, after npm publishes 4.0.2
68
+ npx ai-consultants@4.0.2 version
69
+
70
+ # active git-clone installation
71
+ cd ~/.claude/skills/ai-consultants && git pull
72
+ ```
73
+
74
+ No login or configuration migration is required. The first shared run seeds from the existing ambient Grok credential. No API key is copied or created.
75
+
76
+ ## Verification
77
+
78
+ - Grok adapter: 88/88 checks
79
+ - Configurator: 91/91 checks
80
+ - Core gate: 452/452 checks
81
+ - Full local gate: 23/23 suites
82
+ - ShellCheck, JSON validation, npm tarball, and `git diff --check`: passed
83
+ - PR #14 CI on exact head `883fad7`: passed
84
+ - Real concurrent smoke: two exit-0 structured `PONG` responses, CLI transport, `grok-4.6`, `xhigh`, no workspace/runtime/lock residue, no API fallback, valid and hash-aligned ambient/generation/marker state
85
+
86
+ ## Provider-Specific Residuals
87
+
88
+ - Real token rotation during concurrent inference was not forced; rotation/reuse is verified synthetically.
89
+ - A real concurrent external `grok login` was not induced; external-login precedence is verified synthetically.
90
+ - Superseded generations are retained to avoid deleting state still referenced by an active process.
91
+
92
+ ## Commits
93
+
94
+ - `6c9e4b2` Merge pull request #14
95
+
96
+ ## Contributors
97
+
98
+ - @matteoscurati
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-consultants",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "A cross-vendor panel of up to 10 AI models that returns the coverage union of their distinct perspectives",
5
5
  "bin": {
6
6
  "ai-consultants": "bin/ai-consultants"
@@ -259,6 +259,7 @@ GROK_MODEL=grok-4.6
259
259
  GROK_MAX_TURNS=4
260
260
  GROK_API_MAX_TOKENS=4096
261
261
  GROK_REASONING_EFFORT= # max_quality sets xhigh (Grok Build maximum)
262
+ GROK_OAUTH_MODE=shared # shared (parallel default) | serialized (diagnostic)
262
263
  DEEPSEEK_MODEL=deepseek-v4-pro
263
264
  DEEPSEEK_API_MAX_TOKENS=16384
264
265
  DEEPSEEK_MAX_QUALITY_TIMEOUT=600
@@ -285,6 +286,28 @@ Set `GROK_USE_API=true` only to force the API path. The adapter accepts any CLI
285
286
  version that exposes the complete required headless interface and requested
286
287
  model; `metadata.cli_version` is provenance, not a version pin.
287
288
 
289
+ `GROK_OAUTH_MODE=shared` is the normal CLI path because ai-consultants launches
290
+ consultants in parallel. Each Grok invocation keeps a private HOME, workspace,
291
+ prompt, output, permissions, and tool-free agent policy. Concurrent Grok
292
+ processes share only a runner-owned persistent `GROK_HOME` generation under
293
+ `$XDG_DATA_HOME/ai-consultants/grok-shared-oauth` (or the standard XDG data
294
+ fallback). Grok Build's own auth lock coordinates refreshes inside that shared
295
+ generation; a short ai-consultants lock protects seeding, adoption, atomic
296
+ runner-policy reconciliation, and atomic publication back to the ambient
297
+ `~/.grok/auth.json`. An external `grok login` always wins the digest-based CAS:
298
+ the affected consultation fails temporarily and never overwrites the new login
299
+ or falls back to a separately billed API request. Malformed credentials and
300
+ unpersisted refreshes fail closed. Use `GROK_OAUTH_MODE=serialized` only to hold
301
+ the adapter lock across the complete CLI run for diagnostic comparison. Forced
302
+ API mode is stateless and never creates or reads the shared OAuth generation.
303
+ Each new generation also performs exactly one model-inventory bootstrap under
304
+ the short lock because Grok 1.0.4 lazily initializes home metadata; all later
305
+ inventory probes and every inference remain concurrent.
306
+ Grok 1.0.4 retains ownership of its generated `config.toml`; ai-consultants does
307
+ not replace provider-required sections. Instead it atomically reconciles a
308
+ separate `.ai-consultants-policy.json`, while the actual enforced surface stays
309
+ pinned by the per-invocation CLI arguments.
310
+
288
311
  `max_quality` sets `GROK_REASONING_EFFORT=xhigh` (the highest value accepted by
289
312
  Grok Build),
290
313
  `GLM_REASONING_EFFORT=max`, and `DEEPSEEK_REASONING_EFFORT=max`. Grok passes
package/scripts/config.sh CHANGED
@@ -202,6 +202,7 @@ GROK_MODEL="${GROK_MODEL:-grok-4.6}"
202
202
  GROK_TIMEOUT_SECONDS="${GROK_TIMEOUT:-180}"
203
203
  GROK_API_MAX_TOKENS="${GROK_API_MAX_TOKENS:-4096}"
204
204
  GROK_MAX_TURNS="${GROK_MAX_TURNS:-4}"
205
+ GROK_OAUTH_MODE="${GROK_OAUTH_MODE:-shared}"
205
206
  GROK_API_URL="${GROK_API_URL:-https://api.x.ai/v1/chat/completions}"
206
207
  GROK_FORMAT="${GROK_FORMAT:-openai}"
207
208
  # Grok Build's official credential name is XAI_API_KEY. Keep the historical
@@ -221,7 +222,7 @@ if [[ -z "${GROK_USE_API+x}" ]]; then
221
222
  GROK_USE_API=false
222
223
  fi
223
224
  fi
224
- export GROK_USE_API
225
+ export GROK_USE_API GROK_OAUTH_MODE
225
226
 
226
227
  # =============================================================================
227
228
  # DEEPSEEK CONFIGURATION - The Methodologist (API-based)
@@ -1010,4 +1011,4 @@ EOF
1010
1011
  # VERSION
1011
1012
  # =============================================================================
1012
1013
 
1013
- AI_CONSULTANTS_VERSION="4.0.0"
1014
+ AI_CONSULTANTS_VERSION="4.0.2"
@@ -50,7 +50,7 @@ fi
50
50
  print_header() {
51
51
  echo ""
52
52
  echo -e "${BLUE}+---------------------------------------------------------------+${NC}"
53
- echo -e "${BLUE}| AI Consultants v2.10 - Installation |${NC}"
53
+ echo -e "${BLUE}| AI Consultants - Installation |${NC}"
54
54
  echo -e "${BLUE}+---------------------------------------------------------------+${NC}"
55
55
  echo ""
56
56
  }
@@ -221,7 +221,9 @@ run_query() {
221
221
 
222
222
  # Error handling
223
223
  local error_msg=""
224
- [[ -f "$error_file" ]] && error_msg=$(head -5 "$error_file" 2>/dev/null)
224
+ if [[ "${RUN_QUERY_REDACT_ERRORS:-false}" != "true" && -f "$error_file" ]]; then
225
+ error_msg=$(head -5 "$error_file" 2>/dev/null)
226
+ fi
225
227
 
226
228
  # First line of the CLI's own stderr — the actual reason (auth error,
227
229
  # command not found, etc.). Kept for the final failure message so it
@@ -0,0 +1,455 @@
1
+ #!/bin/bash
2
+ # grok_oauth.sh - Persistent, concurrency-safe Grok Build OAuth handoff.
3
+ #
4
+ # Shared mode keeps one runner-owned credential generation across invocations.
5
+ # Each invocation still receives its own HOME, workspace, prompt, and output;
6
+ # only GROK_HOME is shared so Grok Build's own auth.json.lock can coordinate a
7
+ # refresh. A short adapter lock protects generation adoption and publication.
8
+ # Serialized mode retains a per-run GROK_HOME and holds that adapter lock for
9
+ # the full CLI sequence as a diagnostic fallback.
10
+
11
+ GROK_OAUTH_SOURCE_HOME=""
12
+ GROK_OAUTH_SHARED_ROOT=""
13
+ GROK_OAUTH_ACTIVE_HOME=""
14
+ GROK_OAUTH_GENERATION=""
15
+ GROK_OAUTH_BASELINE_HASH=""
16
+ GROK_OAUTH_LOCK_PATH=""
17
+ GROK_OAUTH_LOCK_HELD=0
18
+ GROK_OAUTH_SYNC_STATUS="not_attempted"
19
+ GROK_OAUTH_ERROR=""
20
+ GROK_OAUTH_LOCK_WAIT_SECONDS=25
21
+ GROK_OAUTH_BOOTSTRAP_TIMEOUT_SECONDS=20
22
+
23
+ grok_oauth_digest() {
24
+ if command -v shasum >/dev/null 2>&1; then
25
+ shasum -a 256 "$1" 2>/dev/null | awk '{print $1}'
26
+ elif command -v sha256sum >/dev/null 2>&1; then
27
+ sha256sum "$1" 2>/dev/null | awk '{print $1}'
28
+ else
29
+ return 1
30
+ fi
31
+ }
32
+
33
+ grok_oauth_credential_valid() {
34
+ local credential="$1"
35
+ [[ -f "$credential" && ! -L "$credential" ]] || return 1
36
+ jq -e '
37
+ type == "object" and
38
+ ([.[]? | objects |
39
+ select(((.key // "") | type == "string" and length > 0) and
40
+ ((.refresh_token // "") | type == "string" and length > 0))] |
41
+ length > 0)
42
+ ' "$credential" >/dev/null 2>&1
43
+ }
44
+
45
+ grok_oauth_release_lock() {
46
+ local owner_pid=""
47
+ [[ "$GROK_OAUTH_LOCK_HELD" -eq 1 ]] || return 0
48
+ if [[ -d "$GROK_OAUTH_LOCK_PATH" && ! -L "$GROK_OAUTH_LOCK_PATH" &&
49
+ -f "$GROK_OAUTH_LOCK_PATH/owner" && ! -L "$GROK_OAUTH_LOCK_PATH/owner" ]]; then
50
+ owner_pid=$(<"$GROK_OAUTH_LOCK_PATH/owner")
51
+ if [[ "$owner_pid" == "$$" ]]; then
52
+ rm -f -- "$GROK_OAUTH_LOCK_PATH/owner"
53
+ rmdir "$GROK_OAUTH_LOCK_PATH" >/dev/null 2>&1 || true
54
+ fi
55
+ fi
56
+ GROK_OAUTH_LOCK_HELD=0
57
+ }
58
+
59
+ grok_oauth_acquire_lock() {
60
+ local owner_pid=""
61
+ [[ -d "$GROK_OAUTH_SOURCE_HOME" && ! -L "$GROK_OAUTH_SOURCE_HOME" ]] || return 1
62
+ GROK_OAUTH_LOCK_PATH="$GROK_OAUTH_SOURCE_HOME/.ai-consultants-oauth.lock.d"
63
+ [[ ! -L "$GROK_OAUTH_LOCK_PATH" ]] || return 1
64
+
65
+ if ! mkdir "$GROK_OAUTH_LOCK_PATH" 2>/dev/null; then
66
+ if [[ -f "$GROK_OAUTH_LOCK_PATH/owner" && ! -L "$GROK_OAUTH_LOCK_PATH/owner" ]]; then
67
+ owner_pid=$(<"$GROK_OAUTH_LOCK_PATH/owner")
68
+ fi
69
+ if [[ "$owner_pid" =~ ^[1-9][0-9]*$ ]] && ! kill -0 "$owner_pid" 2>/dev/null; then
70
+ rm -f -- "$GROK_OAUTH_LOCK_PATH/owner"
71
+ rmdir "$GROK_OAUTH_LOCK_PATH" >/dev/null 2>&1 || return 2
72
+ mkdir "$GROK_OAUTH_LOCK_PATH" 2>/dev/null || return 2
73
+ else
74
+ return 2
75
+ fi
76
+ fi
77
+
78
+ if ! printf '%s\n' "$$" > "$GROK_OAUTH_LOCK_PATH/owner" ||
79
+ ! chmod 700 "$GROK_OAUTH_LOCK_PATH" ||
80
+ ! chmod 600 "$GROK_OAUTH_LOCK_PATH/owner"; then
81
+ rm -f -- "$GROK_OAUTH_LOCK_PATH/owner"
82
+ rmdir "$GROK_OAUTH_LOCK_PATH" >/dev/null 2>&1 || true
83
+ return 1
84
+ fi
85
+ GROK_OAUTH_LOCK_HELD=1
86
+ }
87
+
88
+ grok_oauth_acquire_lock_wait() {
89
+ local deadline rc
90
+ deadline=$(( $(date +%s) + GROK_OAUTH_LOCK_WAIT_SECONDS ))
91
+ while :; do
92
+ rc=0
93
+ grok_oauth_acquire_lock || rc=$?
94
+ [[ "$rc" -eq 2 ]] || return "$rc"
95
+ [[ "$(date +%s)" -lt "$deadline" ]] || return 2
96
+ sleep 1
97
+ done
98
+ }
99
+
100
+ grok_oauth_write_controlled_config() {
101
+ local grok_home="$1" config_path candidate
102
+ [[ -d "$grok_home" && ! -L "$grok_home" ]] || return 1
103
+ # Grok 1.0.4 owns config.toml and lazily populates provider-required
104
+ # sections. Replacing it before every inventory makes the first probe look
105
+ # logged out. Keep the runner contract in a separate atomic manifest; the
106
+ # actual agent surface remains pinned by per-invocation CLI arguments.
107
+ config_path="$grok_home/.ai-consultants-policy.json"
108
+ candidate=$(mktemp "$grok_home/.ai-consultants-policy.json.XXXXXX") || return 1
109
+ if ! jq -n '{
110
+ schema:"ai_consultants_grok_policy_v1",
111
+ compatibility_imports:false,
112
+ workflows:false,
113
+ memory:false,
114
+ subagents:false,
115
+ web:false,
116
+ tools:[],
117
+ permission_mode:"dontAsk",
118
+ sandbox:"strict"
119
+ }' > "$candidate"; then
120
+ rm -f -- "$candidate"
121
+ return 1
122
+ fi
123
+ if ! chmod 600 "$candidate" || ! mv -f "$candidate" "$config_path"; then
124
+ rm -f -- "$candidate"
125
+ return 1
126
+ fi
127
+ }
128
+
129
+ grok_oauth_read_marker() {
130
+ local marker="$GROK_OAUTH_SHARED_ROOT/sync-marker.json"
131
+ [[ -f "$marker" && ! -L "$marker" ]] || return 1
132
+ jq -re '
133
+ select(.schema == "ai_consultants_grok_shared_oauth_marker_v1") |
134
+ select((.generation | type == "string" and test("^gen-[0-9]+-[0-9]+$")) and
135
+ (.synced_sha256 | type == "string" and test("^[0-9a-f]{64}$"))) |
136
+ "\(.generation) \(.synced_sha256)"
137
+ ' "$marker" 2>/dev/null
138
+ }
139
+
140
+ grok_oauth_write_marker() {
141
+ local generation="$1" synced_sha256="$2" candidate
142
+ candidate=$(mktemp "$GROK_OAUTH_SHARED_ROOT/.sync-marker.json.XXXXXX") || return 1
143
+ if ! jq -n --arg generation "$generation" --arg synced_sha256 "$synced_sha256" '
144
+ {schema:"ai_consultants_grok_shared_oauth_marker_v1",
145
+ generation:$generation,synced_sha256:$synced_sha256}
146
+ ' > "$candidate" || ! chmod 600 "$candidate" ||
147
+ ! mv -f "$candidate" "$GROK_OAUTH_SHARED_ROOT/sync-marker.json"; then
148
+ rm -f -- "$candidate"
149
+ return 1
150
+ fi
151
+ }
152
+
153
+ grok_oauth_generation_has_legacy_config() {
154
+ local config="$1/config.toml"
155
+ [[ -f "$config" && ! -L "$config" ]] || return 1
156
+ grep -q '^\[compat\.claude\]$' "$config" &&
157
+ grep -q '^\[compat\.cursor\]$' "$config" &&
158
+ grep -q '^\[workflows\]$' "$config" &&
159
+ grep -q '^enabled = false$' "$config"
160
+ }
161
+
162
+ grok_oauth_init() {
163
+ local source_home="$1"
164
+ GROK_OAUTH_ERROR=""
165
+ GROK_OAUTH_ACTIVE_HOME=""
166
+ GROK_OAUTH_GENERATION=""
167
+ GROK_OAUTH_BASELINE_HASH=""
168
+ GROK_OAUTH_SYNC_STATUS="not_attempted"
169
+ case "$GROK_OAUTH_MODE" in
170
+ shared|serialized) ;;
171
+ *) GROK_OAUTH_ERROR="Invalid GROK_OAUTH_MODE (expected shared or serialized)"; return 1 ;;
172
+ esac
173
+ if [[ ! -d "$source_home" ]]; then
174
+ GROK_OAUTH_ERROR="Grok OAuth home is missing"
175
+ return 2
176
+ elif [[ -L "$source_home" ]]; then
177
+ GROK_OAUTH_ERROR="Grok OAuth home is unsafe"
178
+ return 1
179
+ fi
180
+ GROK_OAUTH_SOURCE_HOME=$(cd "$source_home" && pwd -P) || {
181
+ GROK_OAUTH_ERROR="Could not resolve the Grok OAuth home"
182
+ return 1
183
+ }
184
+ GROK_OAUTH_SHARED_ROOT="${_AI_CONSULTANTS_XDG_DATA}/grok-shared-oauth"
185
+ if [[ ! -f "$GROK_OAUTH_SOURCE_HOME/auth.json" ]]; then
186
+ GROK_OAUTH_ERROR="Grok OAuth credential is missing"
187
+ return 2
188
+ elif ! grok_oauth_credential_valid "$GROK_OAUTH_SOURCE_HOME/auth.json"; then
189
+ GROK_OAUTH_ERROR="Grok OAuth credential is malformed"
190
+ return 1
191
+ fi
192
+ grok_oauth_digest "$GROK_OAUTH_SOURCE_HOME/auth.json" >/dev/null || {
193
+ GROK_OAUTH_ERROR="SHA-256 support is required for Grok OAuth reconciliation"
194
+ return 1
195
+ }
196
+ }
197
+
198
+ grok_oauth_seed_shared() {
199
+ local marker="" marker_generation="" marker_sha="" ambient_hash generation generation_dir
200
+ local lock_rc=0
201
+ if [[ -e "$GROK_OAUTH_SHARED_ROOT" &&
202
+ ( ! -d "$GROK_OAUTH_SHARED_ROOT" || -L "$GROK_OAUTH_SHARED_ROOT" ) ]]; then
203
+ GROK_OAUTH_ERROR="Shared Grok OAuth root is unsafe"
204
+ return 1
205
+ fi
206
+ if ! mkdir -p "$GROK_OAUTH_SHARED_ROOT" || ! chmod 700 "$GROK_OAUTH_SHARED_ROOT"; then
207
+ GROK_OAUTH_ERROR="Could not create the shared Grok OAuth root"
208
+ return 1
209
+ fi
210
+ grok_oauth_acquire_lock_wait || lock_rc=$?
211
+ if [[ "$lock_rc" -eq 2 ]]; then
212
+ GROK_OAUTH_ERROR="Another Grok invocation is reconciling OAuth state"
213
+ return 2
214
+ elif [[ "$lock_rc" -ne 0 ]]; then
215
+ GROK_OAUTH_ERROR="Could not secure Grok OAuth state"
216
+ return 1
217
+ fi
218
+
219
+ if ! grok_oauth_credential_valid "$GROK_OAUTH_SOURCE_HOME/auth.json"; then
220
+ GROK_OAUTH_ERROR="Ambient Grok OAuth credential changed or became malformed during adoption"
221
+ grok_oauth_release_lock
222
+ return 1
223
+ fi
224
+ ambient_hash=$(grok_oauth_digest "$GROK_OAUTH_SOURCE_HOME/auth.json") || {
225
+ GROK_OAUTH_ERROR="Could not fingerprint the ambient Grok OAuth credential"
226
+ grok_oauth_release_lock
227
+ return 1
228
+ }
229
+ if marker=$(grok_oauth_read_marker); then
230
+ marker_generation=${marker%% *}
231
+ marker_sha=${marker##* }
232
+ fi
233
+
234
+ generation_dir="$GROK_OAUTH_SHARED_ROOT/$marker_generation"
235
+ if [[ -n "$marker_generation" && "$marker_sha" == "$ambient_hash" &&
236
+ -d "$generation_dir" && ! -L "$generation_dir" ]] &&
237
+ ! grok_oauth_generation_has_legacy_config "$generation_dir" &&
238
+ grok_oauth_credential_valid "$generation_dir/auth.json"; then
239
+ GROK_OAUTH_GENERATION="$marker_generation"
240
+ if ! grok_oauth_write_controlled_config "$generation_dir"; then
241
+ GROK_OAUTH_ERROR="Could not reconcile shared Grok configuration"
242
+ grok_oauth_release_lock
243
+ return 1
244
+ fi
245
+ else
246
+ generation="gen-$(date +%s)-$$"
247
+ generation_dir="$GROK_OAUTH_SHARED_ROOT/$generation"
248
+ if [[ -e "$generation_dir" || -L "$generation_dir" ]] ||
249
+ ! mkdir "$generation_dir" || ! chmod 700 "$generation_dir"; then
250
+ GROK_OAUTH_ERROR="Could not create a shared Grok OAuth generation"
251
+ grok_oauth_release_lock
252
+ return 1
253
+ fi
254
+ if ! cp "$GROK_OAUTH_SOURCE_HOME/auth.json" "$generation_dir/auth.json" ||
255
+ ! chmod 600 "$generation_dir/auth.json" ||
256
+ ! grok_oauth_credential_valid "$generation_dir/auth.json" ||
257
+ ! grok_oauth_write_controlled_config "$generation_dir" ||
258
+ ! grok_oauth_write_marker "$generation" "$ambient_hash"; then
259
+ GROK_OAUTH_ERROR="Could not seed a shared Grok OAuth generation"
260
+ grok_oauth_release_lock
261
+ return 1
262
+ fi
263
+ GROK_OAUTH_GENERATION="$generation"
264
+ fi
265
+ grok_oauth_release_lock
266
+ GROK_OAUTH_ACTIVE_HOME=$(cd "$GROK_OAUTH_SHARED_ROOT/$GROK_OAUTH_GENERATION" && pwd -P) || {
267
+ GROK_OAUTH_ERROR="Could not resolve the shared Grok OAuth generation"
268
+ return 1
269
+ }
270
+ }
271
+
272
+ grok_oauth_seed_serialized() {
273
+ local isolated_grok_home="$1" lock_rc=0
274
+ grok_oauth_acquire_lock_wait || lock_rc=$?
275
+ if [[ "$lock_rc" -eq 2 ]]; then
276
+ GROK_OAUTH_ERROR="Another Grok invocation is using serialized OAuth mode"
277
+ return 2
278
+ elif [[ "$lock_rc" -ne 0 ]]; then
279
+ GROK_OAUTH_ERROR="Could not secure Grok OAuth state"
280
+ return 1
281
+ fi
282
+ if ! grok_oauth_credential_valid "$GROK_OAUTH_SOURCE_HOME/auth.json" ||
283
+ ! mkdir -p "$isolated_grok_home" || ! chmod 700 "$isolated_grok_home" ||
284
+ ! cp "$GROK_OAUTH_SOURCE_HOME/auth.json" "$isolated_grok_home/auth.json" ||
285
+ ! chmod 600 "$isolated_grok_home/auth.json" ||
286
+ ! grok_oauth_credential_valid "$isolated_grok_home/auth.json" ||
287
+ ! grok_oauth_write_controlled_config "$isolated_grok_home"; then
288
+ GROK_OAUTH_ERROR="Could not seed serialized Grok OAuth state"
289
+ grok_oauth_release_lock
290
+ return 1
291
+ fi
292
+ GROK_OAUTH_BASELINE_HASH=$(grok_oauth_digest "$GROK_OAUTH_SOURCE_HOME/auth.json") || {
293
+ GROK_OAUTH_ERROR="Could not fingerprint serialized Grok OAuth state"
294
+ grok_oauth_release_lock
295
+ return 1
296
+ }
297
+ GROK_OAUTH_ACTIVE_HOME="$isolated_grok_home"
298
+ }
299
+
300
+ grok_oauth_prepare() {
301
+ local isolated_grok_home="$1"
302
+ if [[ "$GROK_OAUTH_MODE" == "shared" ]]; then
303
+ grok_oauth_seed_shared
304
+ else
305
+ grok_oauth_seed_serialized "$isolated_grok_home"
306
+ fi
307
+ }
308
+
309
+ # Grok 1.0.4 lazily creates metadata, caches, DBs, and managed config the first
310
+ # time a new GROK_HOME reaches the authenticated inventory. Its own locks cover
311
+ # individual files but two pristine homes entering that bootstrap concurrently
312
+ # can both report authentication unavailable. Serialize this one-time seed
313
+ # only; the sentinel keeps every later capability probe and inference parallel.
314
+ grok_oauth_bootstrap_shared() {
315
+ local probe_home="$1" grok_cmd="$2"
316
+ local sentinel="$GROK_OAUTH_ACTIVE_HOME/.ai-consultants-ready.json"
317
+ local candidate models lock_rc=0
318
+ [[ "$GROK_OAUTH_MODE" == "shared" ]] || return 0
319
+ if [[ -f "$sentinel" && ! -L "$sentinel" ]] &&
320
+ jq -e --arg generation "$GROK_OAUTH_GENERATION" '
321
+ .schema == "ai_consultants_grok_home_ready_v1" and
322
+ .generation == $generation
323
+ ' "$sentinel" >/dev/null 2>&1; then
324
+ return 0
325
+ fi
326
+
327
+ grok_oauth_acquire_lock_wait || lock_rc=$?
328
+ if [[ "$lock_rc" -eq 2 ]]; then
329
+ GROK_OAUTH_ERROR="Another Grok invocation is bootstrapping shared OAuth state"
330
+ return 2
331
+ elif [[ "$lock_rc" -ne 0 ]]; then
332
+ GROK_OAUTH_ERROR="Could not secure shared Grok OAuth bootstrap"
333
+ return 1
334
+ fi
335
+ if [[ -f "$sentinel" && ! -L "$sentinel" ]] &&
336
+ jq -e --arg generation "$GROK_OAUTH_GENERATION" '
337
+ .schema == "ai_consultants_grok_home_ready_v1" and
338
+ .generation == $generation
339
+ ' "$sentinel" >/dev/null 2>&1; then
340
+ grok_oauth_release_lock
341
+ return 0
342
+ fi
343
+
344
+ if ! models=$(run_with_timeout "$GROK_OAUTH_BOOTSTRAP_TIMEOUT_SECONDS" \
345
+ env HOME="$probe_home" GROK_HOME="$GROK_OAUTH_ACTIVE_HOME" \
346
+ "$grok_cmd" models 2>&1); then
347
+ if grep -Eiq 'not logged in|logged out|login required|authentication required|unauthorized|run .?grok login|status.?401' <<< "$models"; then
348
+ GROK_OAUTH_ERROR="Grok Build authentication unavailable"
349
+ grok_oauth_release_lock
350
+ return 4
351
+ fi
352
+ GROK_OAUTH_ERROR="Shared Grok OAuth bootstrap could not authenticate"
353
+ grok_oauth_release_lock
354
+ return 1
355
+ fi
356
+ # This call exists only to let a pristine provider home finish its lazy
357
+ # initialization. The adapter's normal model probe immediately follows and
358
+ # remains the single authority for authenticated inventory/model identity;
359
+ # first-run provider output is not stable enough to validate twice here.
360
+ if ! grok_oauth_credential_valid "$GROK_OAUTH_ACTIVE_HOME/auth.json"; then
361
+ GROK_OAUTH_ERROR="Shared Grok OAuth bootstrap returned invalid state"
362
+ grok_oauth_release_lock
363
+ return 1
364
+ fi
365
+ candidate=$(mktemp "$GROK_OAUTH_ACTIVE_HOME/.ai-consultants-ready.json.XXXXXX") || {
366
+ GROK_OAUTH_ERROR="Could not stage shared Grok OAuth readiness"
367
+ grok_oauth_release_lock
368
+ return 1
369
+ }
370
+ if ! jq -n --arg generation "$GROK_OAUTH_GENERATION" '
371
+ {schema:"ai_consultants_grok_home_ready_v1",generation:$generation}
372
+ ' > "$candidate" || ! chmod 600 "$candidate" || ! mv -f "$candidate" "$sentinel"; then
373
+ rm -f -- "$candidate"
374
+ GROK_OAUTH_ERROR="Could not record shared Grok OAuth readiness"
375
+ grok_oauth_release_lock
376
+ return 1
377
+ fi
378
+ grok_oauth_release_lock
379
+ }
380
+
381
+ grok_oauth_publish_shared() {
382
+ local marker marker_generation marker_sha shared_hash ambient_hash candidate lock_rc=0 rc=0
383
+ grok_oauth_acquire_lock_wait || lock_rc=$?
384
+ [[ "$lock_rc" -ne 2 ]] || return 3
385
+ [[ "$lock_rc" -eq 0 ]] || return 1
386
+
387
+ if ! marker=$(grok_oauth_read_marker); then
388
+ rc=1
389
+ else
390
+ marker_generation=${marker%% *}
391
+ marker_sha=${marker##* }
392
+ if [[ "$marker_generation" != "$GROK_OAUTH_GENERATION" ]]; then
393
+ rc=2
394
+ elif ! grok_oauth_credential_valid "$GROK_OAUTH_ACTIVE_HOME/auth.json" ||
395
+ ! shared_hash=$(grok_oauth_digest "$GROK_OAUTH_ACTIVE_HOME/auth.json"); then
396
+ rc=1
397
+ elif ! grok_oauth_credential_valid "$GROK_OAUTH_SOURCE_HOME/auth.json" ||
398
+ ! ambient_hash=$(grok_oauth_digest "$GROK_OAUTH_SOURCE_HOME/auth.json"); then
399
+ rc=2
400
+ elif [[ "$ambient_hash" != "$marker_sha" ]]; then
401
+ rc=2
402
+ elif [[ "$shared_hash" == "$marker_sha" ]]; then
403
+ rc=0
404
+ else
405
+ candidate=$(mktemp "$GROK_OAUTH_SOURCE_HOME/.auth.json.ai-consultants.XXXXXX") || rc=1
406
+ if [[ "$rc" -eq 0 ]]; then
407
+ if cp "$GROK_OAUTH_ACTIVE_HOME/auth.json" "$candidate" &&
408
+ chmod 600 "$candidate" &&
409
+ mv -f "$candidate" "$GROK_OAUTH_SOURCE_HOME/auth.json" &&
410
+ grok_oauth_write_marker "$GROK_OAUTH_GENERATION" "$shared_hash"; then
411
+ rc=0
412
+ else
413
+ rm -f -- "$candidate"
414
+ rc=1
415
+ fi
416
+ fi
417
+ fi
418
+ fi
419
+ grok_oauth_release_lock
420
+ return "$rc"
421
+ }
422
+
423
+ grok_oauth_publish_serialized() {
424
+ local scratch_hash ambient_hash candidate
425
+ grok_oauth_credential_valid "$GROK_OAUTH_ACTIVE_HOME/auth.json" || return 1
426
+ scratch_hash=$(grok_oauth_digest "$GROK_OAUTH_ACTIVE_HOME/auth.json") || return 1
427
+ grok_oauth_credential_valid "$GROK_OAUTH_SOURCE_HOME/auth.json" || return 2
428
+ ambient_hash=$(grok_oauth_digest "$GROK_OAUTH_SOURCE_HOME/auth.json") || return 1
429
+ [[ "$ambient_hash" == "$GROK_OAUTH_BASELINE_HASH" ]] || return 2
430
+ [[ "$scratch_hash" != "$GROK_OAUTH_BASELINE_HASH" ]] || return 0
431
+ candidate=$(mktemp "$GROK_OAUTH_SOURCE_HOME/.auth.json.ai-consultants.XXXXXX") || return 1
432
+ if ! cp "$GROK_OAUTH_ACTIVE_HOME/auth.json" "$candidate" ||
433
+ ! chmod 600 "$candidate" ||
434
+ ! mv -f "$candidate" "$GROK_OAUTH_SOURCE_HOME/auth.json"; then
435
+ rm -f -- "$candidate"
436
+ return 1
437
+ fi
438
+ }
439
+
440
+ grok_oauth_sync() {
441
+ local rc=0
442
+ if [[ "$GROK_OAUTH_MODE" == "shared" ]]; then
443
+ grok_oauth_publish_shared || rc=$?
444
+ else
445
+ grok_oauth_publish_serialized || rc=$?
446
+ fi
447
+ grok_oauth_release_lock
448
+ case "$rc" in
449
+ 0) GROK_OAUTH_SYNC_STATUS="ok" ;;
450
+ 2) GROK_OAUTH_SYNC_STATUS="conflict" ;;
451
+ 3) GROK_OAUTH_SYNC_STATUS="lock_busy" ;;
452
+ *) GROK_OAUTH_SYNC_STATUS="failed" ;;
453
+ esac
454
+ return "$rc"
455
+ }
@@ -12,6 +12,7 @@
12
12
  # GROK_API_KEY - xAI API key used by the fallback
13
13
  # XAI_API_KEY - Official xAI key name; accepted as a GROK_API_KEY alias
14
14
  # GROK_REASONING_EFFORT - Optional CLI/API effort; max_quality sets xhigh
15
+ # GROK_OAUTH_MODE - shared (default, concurrent) or serialized (diagnostic)
15
16
  # ENABLE_PERSONA - Enable "The Provocateur" persona (default: true)
16
17
 
17
18
  set -euo pipefail
@@ -19,6 +20,7 @@ set -euo pipefail
19
20
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
21
  source "$SCRIPT_DIR/lib/common.sh"
21
22
  source "$SCRIPT_DIR/lib/personas.sh"
23
+ source "$SCRIPT_DIR/lib/grok_oauth.sh"
22
24
 
23
25
  QUERY="${1:-}"
24
26
  CONTEXT_FILE="${2:-}"
@@ -46,6 +48,8 @@ GROK_CLI_EFFORT=""
46
48
  _GROK_CAPABILITY_ERROR=""
47
49
  _GROK_REQUEST_LAUNCHED=false
48
50
  _GROK_MODEL_PROBE_ERROR=""
51
+ _GROK_OAUTH_PREPARED=false
52
+ _GROK_OAUTH_FAILURE=false
49
53
  exit_code=1
50
54
 
51
55
  if ! [[ "$GROK_MAX_TURNS" =~ ^[1-9][0-9]*$ ]]; then
@@ -57,6 +61,7 @@ if ! [[ "$GROK_MAX_TURNS" =~ ^[1-9][0-9]*$ ]]; then
57
61
  fi
58
62
 
59
63
  cleanup() {
64
+ grok_oauth_release_lock
60
65
  rm -f "$TEMP_OUTPUT" "${TEMP_OUTPUT}.err"
61
66
 
62
67
  # GROK_RUNTIME_DIR is always created by this script under the system temp
@@ -103,7 +108,8 @@ grok_cli_supports_required_interface() {
103
108
  local probe_workspace="$3"
104
109
  local help flag
105
110
 
106
- help=$(env HOME="$probe_home" GROK_HOME="$probe_grok_home" \
111
+ help=$(run_with_timeout "$GROK_OAUTH_BOOTSTRAP_TIMEOUT_SECONDS" \
112
+ env HOME="$probe_home" GROK_HOME="$probe_grok_home" \
107
113
  "$GROK_CMD" --help 2>&1 || true)
108
114
  for flag in \
109
115
  --prompt-file --model --cwd --output-format --no-plan --no-subagents \
@@ -156,7 +162,8 @@ grok_cli_supports_required_interface() {
156
162
 
157
163
  # Exercise the complete headless argument surface under --help. This checks
158
164
  # parser compatibility without starting a session or sending a prompt.
159
- "${probe_args[@]}" --help >/dev/null 2>&1 || return 1
165
+ run_with_timeout "$GROK_OAUTH_BOOTSTRAP_TIMEOUT_SECONDS" \
166
+ "${probe_args[@]}" --help >/dev/null 2>&1 || return 1
160
167
 
161
168
  if grep -q -- '--no-auto-update' <<< "$help"; then
162
169
  GROK_SUPPORTS_NO_AUTO_UPDATE=true
@@ -166,22 +173,29 @@ grok_cli_supports_required_interface() {
166
173
  GROK_CLI_COMPATIBILITY="capability-probed"
167
174
  }
168
175
 
176
+ grok_capability_probe_with_oauth_coordination() {
177
+ # Capability parsing is provider-free and must not initialize either the
178
+ # shared OAuth home or the HOME later used for authenticated inventory.
179
+ grok_cli_supports_required_interface "$1" "$2" "$3"
180
+ }
181
+
169
182
  grok_cli_exposes_requested_model() {
170
183
  local isolated_home="$1"
171
184
  local isolated_grok_home="$2"
172
185
  local models
173
186
 
174
- if ! models=$(env HOME="$isolated_home" GROK_HOME="$isolated_grok_home" \
187
+ if ! models=$(run_with_timeout "$GROK_OAUTH_BOOTSTRAP_TIMEOUT_SECONDS" \
188
+ env HOME="$isolated_home" GROK_HOME="$isolated_grok_home" \
175
189
  "$GROK_CMD" models 2>&1); then
176
190
  if grep -Eiq 'auth|log ?in|credential|token|401|unauthor' <<<"$models"; then
177
- _GROK_MODEL_PROBE_ERROR="Grok Build CLI authentication unavailable"
191
+ _GROK_MODEL_PROBE_ERROR="Grok Build CLI authentication unavailable (inventory_command_failed_auth)"
178
192
  else
179
- _GROK_MODEL_PROBE_ERROR="Grok Build CLI model inventory failed"
193
+ _GROK_MODEL_PROBE_ERROR="Grok Build CLI model inventory failed (inventory_command_failed)"
180
194
  fi
181
195
  return 1
182
196
  fi
183
- grep -Fq "You are logged in with grok.com." <<< "$models" || {
184
- _GROK_MODEL_PROBE_ERROR="Grok Build CLI authentication unavailable"
197
+ grok_oauth_credential_valid "$isolated_grok_home/auth.json" || {
198
+ _GROK_MODEL_PROBE_ERROR="Grok Build CLI authentication unavailable (credential_invalid_after_inventory)"
185
199
  return 1
186
200
  }
187
201
  if ! awk -v requested="$GROK_MODEL" '
@@ -193,12 +207,39 @@ grok_cli_exposes_requested_model() {
193
207
  }
194
208
  END { exit(found ? 0 : 1) }
195
209
  ' <<< "$models"; then
196
- _GROK_MODEL_PROBE_ERROR="Grok Build CLI does not expose the requested model $GROK_MODEL"
210
+ _GROK_MODEL_PROBE_ERROR="Grok Build CLI does not expose the requested model $GROK_MODEL (requested_model_missing)"
197
211
  return 1
198
212
  fi
199
213
  return 0
200
214
  }
201
215
 
216
+ grok_model_probe_with_oauth_coordination() {
217
+ local isolated_home="$1" isolated_grok_home="$2" lock_rc=0 probe_rc=0
218
+ if [[ "$GROK_OAUTH_MODE" != "shared" ]]; then
219
+ grok_cli_exposes_requested_model "$isolated_home" "$isolated_grok_home"
220
+ return $?
221
+ fi
222
+ grok_oauth_acquire_lock_wait || lock_rc=$?
223
+ if [[ "$lock_rc" -ne 0 ]]; then
224
+ _GROK_OAUTH_FAILURE=true
225
+ if [[ "$lock_rc" -eq 2 ]]; then
226
+ _GROK_MODEL_PROBE_ERROR="Grok OAuth inventory coordination is temporarily busy"
227
+ else
228
+ _GROK_MODEL_PROBE_ERROR="Grok OAuth inventory coordination failed"
229
+ fi
230
+ return 1
231
+ fi
232
+ grok_cli_exposes_requested_model "$isolated_home" "$isolated_grok_home" || probe_rc=$?
233
+ grok_oauth_release_lock
234
+ return "$probe_rc"
235
+ }
236
+
237
+ grok_shared_oauth_bootstrap_ready() {
238
+ oauth_rc=0
239
+ grok_oauth_bootstrap_shared "$1" "$GROK_CMD" || oauth_rc=$?
240
+ [[ "$oauth_rc" -eq 0 ]]
241
+ }
242
+
202
243
  grok_cli_is_unavailable() {
203
244
  local cli_exit_code="$1"
204
245
  local error_file="$2"
@@ -241,34 +282,81 @@ else
241
282
  isolated_home="$GROK_RUNTIME_DIR/home"
242
283
  isolated_grok_home="$isolated_home/.grok"
243
284
  isolated_workspace="$GROK_RUNTIME_DIR/workspace"
285
+ capability_home="$GROK_RUNTIME_DIR/capability-home"
286
+ capability_grok_home="$capability_home/.grok"
287
+ capability_workspace="$GROK_RUNTIME_DIR/capability-workspace"
244
288
  prompt_file="$GROK_RUNTIME_DIR/prompt.txt"
245
- mkdir -p "$isolated_grok_home" "$isolated_workspace"
246
- chmod 700 "$isolated_home" "$isolated_grok_home" "$isolated_workspace"
289
+ mkdir -p "$isolated_home" "$isolated_workspace" \
290
+ "$capability_grok_home" "$capability_workspace"
291
+ chmod 700 "$isolated_home" "$isolated_workspace" \
292
+ "$capability_home" "$capability_grok_home" "$capability_workspace"
293
+ _GROK_CAPABILITY_HOME_READY=true
294
+ grok_oauth_write_controlled_config "$capability_grok_home" || {
295
+ printf '%s\n' "Grok capability home could not be initialized" > "${TEMP_OUTPUT}.err"
296
+ _GROK_OAUTH_FAILURE=true
297
+ _GROK_CAPABILITY_HOME_READY=false
298
+ exit_code=70
299
+ }
247
300
  printf '%s' "$FULL_QUERY" > "$prompt_file"
248
301
  chmod 600 "$prompt_file"
249
302
 
250
- # Preserve only the credential needed to use the installed CLI. All
251
- # config, plugins, hooks, MCP servers, skills, memories, and project
252
- # instructions stay outside the isolated HOME/CWD.
303
+ # Preserve a rotating subscription credential without sharing agent
304
+ # state. HOME/CWD/prompt/output remain per invocation. In shared mode,
305
+ # concurrent processes use one runner-owned GROK_HOME so Grok Build's
306
+ # native auth lock can coordinate refresh. Serialized mode keeps the
307
+ # previous per-run GROK_HOME shape under a full diagnostic lock.
253
308
  source_grok_home=$(printenv GROK_HOME 2>/dev/null || true)
254
309
  [[ -n "$source_grok_home" ]] || source_grok_home="${HOME}/.grok"
255
- if [[ -f "$source_grok_home/auth.json" && ! -L "$source_grok_home/auth.json" ]]; then
256
- if cp "$source_grok_home/auth.json" "$isolated_grok_home/auth.json"; then
257
- chmod 600 "$isolated_grok_home/auth.json"
310
+ if [[ "$_GROK_CAPABILITY_HOME_READY" == "true" ]]; then
311
+ oauth_rc=0
312
+ grok_oauth_init "$source_grok_home" || oauth_rc=$?
313
+ if [[ "$oauth_rc" -eq 2 ]]; then
314
+ printf '%s\n' "Grok Build authentication unavailable; run grok login" > "${TEMP_OUTPUT}.err"
315
+ log_warn "[$CONSULTANT_NAME] Grok Build authentication unavailable"
316
+ exit_code=69
317
+ elif [[ "$oauth_rc" -ne 0 ]]; then
318
+ _GROK_OAUTH_FAILURE=true
319
+ printf '%s\n' "Grok OAuth state failed validation" > "${TEMP_OUTPUT}.err"
320
+ log_warn "[$CONSULTANT_NAME] $GROK_OAUTH_ERROR"
321
+ exit_code=70
258
322
  else
259
- log_warn "[$CONSULTANT_NAME] Could not copy the Grok CLI credential into the isolated runtime"
323
+ oauth_rc=0
324
+ grok_oauth_prepare "$isolated_grok_home" || oauth_rc=$?
325
+ if [[ "$oauth_rc" -ne 0 ]]; then
326
+ _GROK_OAUTH_FAILURE=true
327
+ printf '%s\n' "Grok OAuth state could not be prepared" > "${TEMP_OUTPUT}.err"
328
+ log_warn "[$CONSULTANT_NAME] $GROK_OAUTH_ERROR"
329
+ [[ "$oauth_rc" -eq 2 ]] && exit_code=75 || exit_code=70
330
+ else
331
+ _GROK_OAUTH_PREPARED=true
332
+ fi
260
333
  fi
261
334
  fi
262
335
 
263
336
  GROK_CLI_VERSION=$(observe_grok_cli_version)
264
- if ! grok_cli_supports_required_interface \
265
- "$isolated_home" "$isolated_grok_home" "$isolated_workspace"; then
337
+ if [[ "$_GROK_OAUTH_PREPARED" != "true" ]]; then
338
+ :
339
+ elif ! grok_capability_probe_with_oauth_coordination \
340
+ "$capability_home" "$capability_grok_home" "$capability_workspace"; then
266
341
  GROK_CLI_COMPATIBILITY="incompatible"
267
342
  printf '%s\n' "${_GROK_CAPABILITY_ERROR:-Grok Build CLI lacks the required capabilities}" > "${TEMP_OUTPUT}.err"
268
343
  log_warn "[$CONSULTANT_NAME] ${_GROK_CAPABILITY_ERROR:-Grok Build CLI lacks the required capabilities} (reported version: $GROK_CLI_VERSION)"
269
344
  exit_code=69
270
- elif ! grok_cli_exposes_requested_model \
271
- "$isolated_home" "$isolated_grok_home" > "${TEMP_OUTPUT}.err"; then
345
+ elif [[ "$GROK_OAUTH_MODE" == "shared" ]] &&
346
+ ! grok_shared_oauth_bootstrap_ready "$isolated_home"; then
347
+ _GROK_OAUTH_PREPARED=false
348
+ if [[ "$oauth_rc" -eq 4 ]]; then
349
+ printf '%s\n' "Grok Build authentication unavailable; run grok login" > "${TEMP_OUTPUT}.err"
350
+ log_warn "[$CONSULTANT_NAME] Grok Build authentication unavailable"
351
+ exit_code=69
352
+ else
353
+ _GROK_OAUTH_FAILURE=true
354
+ printf '%s\n' "Grok shared OAuth bootstrap failed" > "${TEMP_OUTPUT}.err"
355
+ log_warn "[$CONSULTANT_NAME] $GROK_OAUTH_ERROR"
356
+ [[ "$oauth_rc" -eq 2 ]] && exit_code=75 || exit_code=70
357
+ fi
358
+ elif ! grok_model_probe_with_oauth_coordination \
359
+ "$isolated_home" "$GROK_OAUTH_ACTIVE_HOME" > "${TEMP_OUTPUT}.err"; then
272
360
  printf '%s\n' "${_GROK_MODEL_PROBE_ERROR:-Grok Build CLI model inventory failed}" > "${TEMP_OUTPUT}.err"
273
361
  log_warn "[$CONSULTANT_NAME] ${_GROK_MODEL_PROBE_ERROR:-Grok Build CLI model inventory failed}"
274
362
  exit_code=69
@@ -280,7 +368,7 @@ else
280
368
  GROK_ARGS=(
281
369
  env
282
370
  HOME="$isolated_home"
283
- GROK_HOME="$isolated_grok_home"
371
+ GROK_HOME="$GROK_OAUTH_ACTIVE_HOME"
284
372
  "$GROK_CMD"
285
373
  --prompt-file "$prompt_file"
286
374
  -m "$GROK_MODEL"
@@ -314,7 +402,7 @@ else
314
402
  fi
315
403
 
316
404
  _GROK_REQUEST_LAUNCHED=true
317
- if run_query \
405
+ if RUN_QUERY_REDACT_ERRORS=true run_query \
318
406
  "$CONSULTANT_NAME" \
319
407
  "$TEMP_OUTPUT" \
320
408
  "$GROK_TIMEOUT_SECONDS" \
@@ -324,6 +412,35 @@ else
324
412
  exit_code=$?
325
413
  fi
326
414
  fi
415
+
416
+ # Publish refreshes before considering fallback or publishing output.
417
+ # A concurrent external `grok login` changes the ambient digest, wins
418
+ # the CAS, and makes this run fail temporarily without overwriting it.
419
+ if [[ "$_GROK_OAUTH_PREPARED" == "true" ]]; then
420
+ oauth_rc=0
421
+ grok_oauth_sync || oauth_rc=$?
422
+ if [[ "$oauth_rc" -ne 0 ]]; then
423
+ _GROK_OAUTH_FAILURE=true
424
+ : > "$TEMP_OUTPUT"
425
+ case "$oauth_rc" in
426
+ 2)
427
+ printf '%s\n' "Grok OAuth changed concurrently; retry" > "${TEMP_OUTPUT}.err"
428
+ log_warn "[$CONSULTANT_NAME] External Grok login superseded this run"
429
+ exit_code=75
430
+ ;;
431
+ 3)
432
+ printf '%s\n' "Grok OAuth reconciliation lock is busy; retry" > "${TEMP_OUTPUT}.err"
433
+ log_warn "[$CONSULTANT_NAME] Grok OAuth reconciliation is temporarily busy"
434
+ exit_code=75
435
+ ;;
436
+ *)
437
+ printf '%s\n' "Grok OAuth refresh could not be persisted" > "${TEMP_OUTPUT}.err"
438
+ log_warn "[$CONSULTANT_NAME] Grok OAuth refresh could not be persisted"
439
+ exit_code=70
440
+ ;;
441
+ esac
442
+ fi
443
+ fi
327
444
  else
328
445
  log_warn "[$CONSULTANT_NAME] Grok Build CLI not found: $GROK_CMD"
329
446
  exit_code=127
@@ -334,6 +451,7 @@ else
334
451
  # it cannot silently turn into a separately billed API request.
335
452
  if [[ $exit_code -ne 0 ]] \
336
453
  && [[ "$_GROK_REQUEST_LAUNCHED" != "true" ]] \
454
+ && [[ "$_GROK_OAUTH_FAILURE" != "true" ]] \
337
455
  && grok_cli_is_unavailable "$exit_code" "${TEMP_OUTPUT}.err" \
338
456
  && [[ -n "${GROK_API_KEY:-}" ]]; then
339
457
  log_warn "[$CONSULTANT_NAME] Grok Build CLI unavailable (exit $exit_code); falling back to the xAI API"