ai-consultants 4.0.1 → 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 +5 -0
- package/CLAUDE.md +13 -1
- package/README.md +12 -3
- package/SKILL.md +10 -2
- package/docs/COST_RATES.md +1 -1
- package/docs/SETUP.md +11 -0
- package/docs/cost_rates.json +1 -1
- package/docs/releases/v4.0.2.md +98 -0
- package/package.json +1 -1
- package/references/configuration.md +23 -0
- package/scripts/config.sh +3 -2
- package/scripts/lib/common.sh +3 -1
- package/scripts/lib/grok_oauth.sh +455 -0
- package/scripts/query_grok.sh +141 -23
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.
|
|
9
|
+
**Version**: 4.0.2
|
|
10
10
|
|
|
11
11
|
## Distribution
|
|
12
12
|
|
|
@@ -760,6 +760,18 @@ 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
|
+
|
|
763
775
|
### v4.0.1
|
|
764
776
|
|
|
765
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.
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# AI Consultants v4.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
|
-
[](https://github.com/matteoscurati/ai-consultants)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://docs.anthropic.com/en/docs/claude-code/skills)
|
|
8
8
|
[](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.
|
|
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.
|
|
8
|
+
version: 4.0.2
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
# AI Consultants v4.0.
|
|
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;
|
package/docs/COST_RATES.md
CHANGED
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
|
package/docs/cost_rates.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.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,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
|
@@ -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.
|
|
1014
|
+
AI_CONSULTANTS_VERSION="4.0.2"
|
package/scripts/lib/common.sh
CHANGED
|
@@ -221,7 +221,9 @@ run_query() {
|
|
|
221
221
|
|
|
222
222
|
# Error handling
|
|
223
223
|
local error_msg=""
|
|
224
|
-
[[
|
|
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
|
+
}
|
package/scripts/query_grok.sh
CHANGED
|
@@ -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=$(
|
|
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
|
-
"$
|
|
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=$(
|
|
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
|
-
|
|
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 "$
|
|
246
|
-
|
|
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
|
|
251
|
-
#
|
|
252
|
-
#
|
|
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 [[
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
|
265
|
-
|
|
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
|
|
271
|
-
|
|
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="$
|
|
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"
|