From 97abc432c7b9ec0ca32669134e22596b8bbfc216 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 08:57:13 +0000 Subject: [PATCH 1/7] Fix Claude Code credential setup: PKCE OAuth flow, no key in-context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add src/bridge/oauth_setup.py: stdlib-only PKCE OAuth helper that flows browser → OpenRouter → localhost:3000 → config.json; API key never passes through Claude's context window - Fix commands/setup.sh: remove infinite self-exec bug (exec "$SCRIPT_DIR/setup.sh" called itself when no key found); replace with guidance message + exit 1 - Rewrite commands/setup.md: instruct Claude to invoke oauth_setup.py via Bash (200s timeout), present auth URL to user, report success/failure; includes xAI direct fallback path - Fix get_api_key() in both grok_bridge.py copies: add ~/.claude/grok-swarm.local.md reading (legacy path written by old setup.sh, was dead code before) - Update README.md and INSTALL.md: replace ./scripts/setup.sh steps for Claude Code users with /grok-swarm:setup OAuth instructions Security: key travels browser → OpenRouter → localhost:3000 → disk, never seen by Claude. Port 3000 conflict detection included. https://claude.ai/code/session_01No9S6TZbfTgocHwHYWwxRN --- INSTALL.md | 17 +- README.md | 6 +- platforms/claude/commands/setup.md | 73 +++-- platforms/claude/commands/setup.sh | 16 +- skills/grok-refactor/bridge/grok_bridge.py | 12 + src/bridge/grok_bridge.py | 12 + src/bridge/oauth_setup.py | 298 +++++++++++++++++++++ 7 files changed, 399 insertions(+), 35 deletions(-) create mode 100644 src/bridge/oauth_setup.py diff --git a/INSTALL.md b/INSTALL.md index d11a839..136c5ae 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -103,9 +103,8 @@ Install the plugin from the Claude Code marketplace. ### Step 3: Configure API Key -```bash -./scripts/setup.sh -``` +Run `/grok-swarm:setup` inside Claude Code — an OAuth browser flow will +authorize your OpenRouter account without exposing your API key in-context. ### Usage in Claude Code @@ -147,9 +146,9 @@ cp -r src/plugin ~/.openclaw/extensions/grok-swarm/ ### Configure API Key -```bash -./scripts/setup.sh -``` +**For Claude Code:** Run `/grok-swarm:setup` — OAuth browser flow, no key in-context. + +**For OpenClaw/CLI:** Run `./scripts/setup.sh` or set `OPENROUTER_API_KEY` manually. --- @@ -175,9 +174,9 @@ grok-swarm --help ### For Claude Code -```bash -# Set up API key -./scripts/setup.sh +``` +# Authorize via OAuth (no API key in-context) +/grok-swarm:setup # Try it out /grok-swarm:analyze Review my auth module for security issues diff --git a/README.md b/README.md index 5266c16..a278af8 100644 --- a/README.md +++ b/README.md @@ -109,11 +109,11 @@ npm install -g @openclaw/grok-swarm # Install the plugin /plugin install grok-swarm@khaentertainment - -# Set up API key -./scripts/setup.sh ``` +Then run `/grok-swarm:setup` inside Claude Code — an OAuth browser flow will +authorize your OpenRouter account without exposing your API key in-context. + ### Option 3: ClawHub (OpenClaw) ```bash diff --git a/platforms/claude/commands/setup.md b/platforms/claude/commands/setup.md index 68cfc68..2cffa0e 100644 --- a/platforms/claude/commands/setup.md +++ b/platforms/claude/commands/setup.md @@ -1,6 +1,6 @@ --- name: setup -description: Set up your OpenRouter API key for Grok Swarm. Run this before first use if prompted. +description: Authorize Grok Swarm with your OpenRouter account via OAuth. No API key handling in-context. argument-hint: None allowed-tools: - Bash @@ -8,35 +8,68 @@ allowed-tools: # Setup Grok Swarm -This command guides you through configuring your OpenRouter API key for Grok Swarm. +Follow these steps exactly. Do not ask the user for their API key — the OAuth +flow ensures the key never passes through this conversation. -## Usage +## Step 1 — Check for existing key +Run: +```bash +python3 "$(dirname $(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1))/oauth_setup.py" --check 2>/dev/null || python3 "$(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1)" --check 2>/dev/null ``` -/grok-swarm:setup + +Alternative (locate bridge relative to this command file): +```bash +BRIDGE_DIR="$(cd "$(dirname "$0")/../../../src/bridge" 2>/dev/null && pwd)" +if [ -f "$BRIDGE_DIR/oauth_setup.py" ]; then + python3 "$BRIDGE_DIR/oauth_setup.py" --check +fi ``` -## What it does +If the check exits 0, tell the user their key is already configured and offer +to run a test query. **Stop here.** + +## Step 2 — Run the OAuth flow + +Locate `oauth_setup.py` in the plugin's `src/bridge/` directory and run it +with a 200-second timeout: -1. Checks for existing API key in: - - `~/.claude/grok-swarm.local.md` (plugin settings) - - `~/.config/grok-swarm/config.json` (CLI config) - - `$OPENROUTER_API_KEY` environment variable +```bash +PLUGIN_ROOT="$(cd "$(dirname "$0")/../../.." 2>/dev/null && pwd)" +python3 "$PLUGIN_ROOT/src/bridge/oauth_setup.py" +``` + +The script will: +1. Print an authorization URL +2. Start a local server on `localhost:3000` +3. Wait up to 180 seconds for the browser callback +4. Exchange the auth code for an API key +5. Save the key to `~/.config/grok-swarm/config.json` (chmod 600) -2. If no key found, prompts you to enter one +## Step 3 — Present the auth URL -3. Saves configuration to plugin settings file +Show the user the URL printed by the script and tell them: -## First-time setup +> **Click this link to authorize Grok Swarm with your OpenRouter account.** +> After approving in your browser, return here — setup completes automatically. -If you're seeing this for the first time: +## Step 4 — Report result -1. Get an OpenRouter API key at https://openrouter.ai/keys -2. Run `/grok-swarm:setup` -3. Paste your API key when prompted +- If the script exits 0: confirm success and suggest running `/grok-swarm:analyze Hello world` +- If it exits 1 (timeout or port conflict): show the error message from the script + and suggest the manual fallback: + ``` + mkdir -p ~/.config/grok-swarm + echo '{"api_key": "sk-or-v1-..."}' > ~/.config/grok-swarm/config.json + chmod 600 ~/.config/grok-swarm/config.json + ``` + Direct them to https://openrouter.ai/keys for a key. -## Troubleshooting +## xAI Direct Users -- **Invalid key error**: Make sure you copied the key correctly from OpenRouter -- **Key starts with `sk-`**: That's correct! OpenRouter keys begin with `sk-or-v1-` -- **Permission denied**: The setup script needs to write to `~/.claude/` and `~/.config/` +If the user says they want to use xAI directly (not OpenRouter), run: +```bash +PLUGIN_ROOT="$(cd "$(dirname "$0")/../../.." 2>/dev/null && pwd)" +python3 "$PLUGIN_ROOT/src/bridge/oauth_setup.py" --provider xai +``` +This prints manual credential instructions without attempting OAuth. diff --git a/platforms/claude/commands/setup.sh b/platforms/claude/commands/setup.sh index f46fbe3..2f21e68 100755 --- a/platforms/claude/commands/setup.sh +++ b/platforms/claude/commands/setup.sh @@ -28,6 +28,16 @@ if api_key=$(get_api_key) && [ -n "$api_key" ]; then exit 0 fi -# No API key - prompt for setup -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -exec "$SCRIPT_DIR/setup.sh" +# No API key found — guide user to the OAuth-based setup command +echo "" +echo "No API key configured." +echo "" +echo "Run '/grok-swarm:setup' inside Claude Code to authorize via OpenRouter's" +echo "OAuth flow. Your key will never pass through Claude's context window." +echo "" +echo "Or set manually:" +echo " export OPENROUTER_API_KEY=sk-or-v1-..." +echo " # or: mkdir -p ~/.config/grok-swarm && echo '{\"api_key\": \"sk-or-v1-...\"}' > ~/.config/grok-swarm/config.json" +echo "" +echo "Get a key at: https://openrouter.ai/keys" +exit 1 diff --git a/skills/grok-refactor/bridge/grok_bridge.py b/skills/grok-refactor/bridge/grok_bridge.py index be2dcbf..808974d 100644 --- a/skills/grok-refactor/bridge/grok_bridge.py +++ b/skills/grok-refactor/bridge/grok_bridge.py @@ -134,6 +134,18 @@ def get_api_key(): except (json.JSONDecodeError, KeyError): pass + # 2b. Claude Code plugin settings file (legacy: written by setup.sh) + claude_settings = Path.home() / ".claude" / "grok-swarm.local.md" + if claude_settings.exists(): + try: + for line in claude_settings.read_text(encoding="utf-8").splitlines(): + if line.startswith("api_key:"): + key = line.split(":", 1)[1].strip() + if key: + return key + except OSError: + pass + # 3. OpenClaw auth profiles (for OpenClaw integration) auth_paths = [ Path.home() / ".openclaw" / "agents" / "coder" / "agent" / "auth-profiles.json", diff --git a/src/bridge/grok_bridge.py b/src/bridge/grok_bridge.py index 517a167..5f28b0c 100644 --- a/src/bridge/grok_bridge.py +++ b/src/bridge/grok_bridge.py @@ -96,6 +96,18 @@ def get_api_key(): except (json.JSONDecodeError, KeyError): pass + # 2b. Claude Code plugin settings file (legacy: written by setup.sh) + claude_settings = Path.home() / ".claude" / "grok-swarm.local.md" + if claude_settings.exists(): + try: + for line in claude_settings.read_text(encoding="utf-8").splitlines(): + if line.startswith("api_key:"): + key = line.split(":", 1)[1].strip() + if key: + return key + except OSError: + pass + # 3. OpenClaw auth profiles (for OpenClaw integration) auth_paths = [ Path.home() / ".openclaw" / "agents" / "coder" / "agent" / "auth-profiles.json", diff --git a/src/bridge/oauth_setup.py b/src/bridge/oauth_setup.py new file mode 100644 index 0000000..ad72eb8 --- /dev/null +++ b/src/bridge/oauth_setup.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 +""" +oauth_setup.py — PKCE OAuth helper for Grok Swarm / OpenRouter. + +The API key NEVER passes through Claude's context window. +Flow: browser → OpenRouter → localhost:3000 → ~/.config/grok-swarm/config.json + +Usage: + python3 oauth_setup.py # interactive PKCE OAuth flow + python3 oauth_setup.py --check # exit 0 if key exists, 1 if not (no output) + python3 oauth_setup.py --provider xai # print manual setup instructions + +Requirements: Python 3.8+ stdlib only (no third-party packages). +""" + +import argparse +import base64 +import hashlib +import json +import os +import secrets +import socket +import sys +import urllib.parse +import urllib.request +from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path + +# OpenRouter OAuth constants +OPENROUTER_AUTH_URL = "https://openrouter.ai/auth" +OPENROUTER_TOKEN_URL = "https://openrouter.ai/api/v1/auth/keys" +CALLBACK_PORT = 3000 # OpenRouter only allows localhost:3000 +CALLBACK_URL = f"http://localhost:{CALLBACK_PORT}" +APP_NAME = "Grok+Swarm" +OAUTH_TIMEOUT_SECS = 180 + +CONFIG_DIR = Path.home() / ".config" / "grok-swarm" +CONFIG_FILE = CONFIG_DIR / "config.json" + + +# --------------------------------------------------------------------------- +# PKCE helpers +# --------------------------------------------------------------------------- + +def _generate_pkce_pair() -> tuple[str, str]: + """Return (code_verifier, code_challenge).""" + verifier_bytes = secrets.token_bytes(64) + code_verifier = base64.urlsafe_b64encode(verifier_bytes).rstrip(b"=").decode() + digest = hashlib.sha256(code_verifier.encode()).digest() + code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() + return code_verifier, code_challenge + + +# --------------------------------------------------------------------------- +# Key persistence +# --------------------------------------------------------------------------- + +def _key_exists() -> bool: + """Return True if an API key is already configured.""" + # Check env vars first + if os.environ.get("OPENROUTER_API_KEY") or os.environ.get("XAI_API_KEY"): + return True + if CONFIG_FILE.exists(): + try: + data = json.loads(CONFIG_FILE.read_text(encoding="utf-8")) + return bool(data.get("api_key")) + except (json.JSONDecodeError, OSError): + pass + # Also check ~/.claude/grok-swarm.local.md + claude_settings = Path.home() / ".claude" / "grok-swarm.local.md" + if claude_settings.exists(): + try: + for line in claude_settings.read_text(encoding="utf-8").splitlines(): + if line.startswith("api_key:") and line.split(":", 1)[1].strip(): + return True + except OSError: + pass + return False + + +def _save_key(api_key: str) -> None: + """Write API key to ~/.config/grok-swarm/config.json with mode 600.""" + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + existing: dict = {} + if CONFIG_FILE.exists(): + try: + existing = json.loads(CONFIG_FILE.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + pass + existing["api_key"] = api_key + CONFIG_FILE.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8") + CONFIG_FILE.chmod(0o600) + + +# --------------------------------------------------------------------------- +# OAuth callback server +# --------------------------------------------------------------------------- + +_received_code: list[str] = [] + + +class _CallbackHandler(BaseHTTPRequestHandler): + """Minimal HTTP handler that captures the ?code= query parameter.""" + + def do_GET(self) -> None: + parsed = urllib.parse.urlparse(self.path) + params = urllib.parse.parse_qs(parsed.query) + code = params.get("code", [None])[0] + if code: + _received_code.append(code) + body = b"

Grok Swarm authorized!

You can close this tab.

" + self.send_response(200) + else: + body = b"

Authorization failed.

No code received. Please try again.

" + self.send_response(400) + self.send_header("Content-Type", "text/html") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, *args) -> None: # type: ignore[override] + pass # suppress request logs + + +def _exchange_code(code: str, code_verifier: str) -> str: + """Exchange auth code for API key via OpenRouter token endpoint.""" + payload = json.dumps({"code": code}).encode() + req = urllib.request.Request( + OPENROUTER_TOKEN_URL, + data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=30) as resp: + data = json.loads(resp.read().decode()) + except Exception as exc: + raise RuntimeError(f"Token exchange failed: {exc}") from exc + + key = data.get("key") or data.get("api_key") + if not key: + raise RuntimeError(f"No key in response: {data}") + return key + + +def _check_port_available() -> bool: + """Return True if localhost:3000 is available.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(1) + return s.connect_ex(("localhost", CALLBACK_PORT)) != 0 + + +# --------------------------------------------------------------------------- +# Public entry points +# --------------------------------------------------------------------------- + +def run_oauth_flow() -> int: + """ + Run the PKCE OAuth flow. + + Returns 0 on success, 1 on failure. + """ + if not _check_port_available(): + print( + f"ERROR: Port {CALLBACK_PORT} is already in use.\n" + f"Stop whatever is using port {CALLBACK_PORT} and retry, or set your key manually:\n" + f" mkdir -p ~/.config/grok-swarm\n" + f" echo '{{\"api_key\": \"sk-or-v1-...\"}}' > ~/.config/grok-swarm/config.json\n" + f" chmod 600 ~/.config/grok-swarm/config.json", + file=sys.stderr, + ) + return 1 + + code_verifier, code_challenge = _generate_pkce_pair() + + auth_params = urllib.parse.urlencode( + { + "callback_url": CALLBACK_URL, + "code_challenge": code_challenge, + "code_challenge_method": "S256", + "app_name": APP_NAME, + } + ) + auth_url = f"{OPENROUTER_AUTH_URL}?{auth_params}" + + print("=" * 60) + print("Grok Swarm — OpenRouter Authorization") + print("=" * 60) + print() + print("Click the link below to authorize Grok Swarm with your") + print("OpenRouter account (or paste it into your browser):") + print() + print(f" {auth_url}") + print() + print(f"Waiting up to {OAUTH_TIMEOUT_SECS}s for authorization callback...") + + server = HTTPServer(("localhost", CALLBACK_PORT), _CallbackHandler) + server.timeout = OAUTH_TIMEOUT_SECS + + deadline = __import__("time").time() + OAUTH_TIMEOUT_SECS + while not _received_code and __import__("time").time() < deadline: + server.handle_request() + + server.server_close() + + if not _received_code: + print("\nERROR: Timed out waiting for authorization.", file=sys.stderr) + print("Please try again or set your key manually:", file=sys.stderr) + print(" export OPENROUTER_API_KEY=sk-or-v1-...", file=sys.stderr) + return 1 + + code = _received_code[0] + print("\nCallback received. Exchanging code for API key...", end=" ", flush=True) + + try: + api_key = _exchange_code(code, code_verifier) + except RuntimeError as exc: + print(f"FAILED\nERROR: {exc}", file=sys.stderr) + return 1 + + _save_key(api_key) + masked = api_key[:12] + "..." if len(api_key) > 12 else api_key + print(f"OK\n\nSuccess! API key saved to {CONFIG_FILE}\nKey: {masked}") + return 0 + + +def run_check() -> int: + """Exit 0 if key exists, 1 if not. No output.""" + return 0 if _key_exists() else 1 + + +def print_manual_instructions() -> None: + """Print instructions for manually placing xAI / OpenRouter credentials.""" + print("""Grok Swarm — Manual Credential Setup +===================================== + +Option A — OpenRouter (recommended): + 1. Create an account at https://openrouter.ai + 2. Generate an API key at https://openrouter.ai/keys + 3. Run one of: + + export OPENROUTER_API_KEY=sk-or-v1-... + + # or save permanently: + mkdir -p ~/.config/grok-swarm + echo '{"api_key": "sk-or-v1-..."}' > ~/.config/grok-swarm/config.json + chmod 600 ~/.config/grok-swarm/config.json + +Option B — xAI direct (no OAuth available): + 1. Create an account at https://console.x.ai + 2. Generate an API key in your dashboard + 3. Run one of: + + export XAI_API_KEY=xai-... + + # or save permanently (grok_bridge will read XAI_API_KEY): + mkdir -p ~/.config/grok-swarm + echo '{"api_key": "xai-..."}' > ~/.config/grok-swarm/config.json + chmod 600 ~/.config/grok-swarm/config.json + + Note: when using xAI direct you must also update grok_bridge.py's + OPENROUTER_BASE to point to https://api.x.ai/v1 +""") + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser( + description="Set up OpenRouter API key for Grok Swarm via PKCE OAuth." + ) + parser.add_argument( + "--check", + action="store_true", + help="Exit 0 if key already configured, 1 otherwise (no output).", + ) + parser.add_argument( + "--provider", + choices=["openrouter", "xai"], + default="openrouter", + help="Credential provider. 'xai' prints manual instructions (no OAuth).", + ) + args = parser.parse_args() + + if args.check: + sys.exit(run_check()) + + if args.provider == "xai": + print_manual_instructions() + sys.exit(0) + + sys.exit(run_oauth_flow()) + + +if __name__ == "__main__": + main() From e8f12cd1482f128d3cfa6532b938744d3a770aea Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Mar 2026 08:58:12 +0000 Subject: [PATCH 2/7] Restore broad lang_path_pattern and absolute path rebasing after rebase Upstream commits narrowed lang_path_pattern to absolute-only paths and reverted the Issue #14 absolute-path rebasing in _safe_dest(). Re-apply both fixes: - lang_path_pattern: r'^(\w+):([^\s\n]+)\n' (relative AND absolute) - _safe_dest(): rebase absolute paths to filename-only with a warning instead of raising ValueError (which silently dropped Grok output) https://claude.ai/code/session_01No9S6TZbfTgocHwHYWwxRN --- src/agent/grok_agent.py | 4 ++-- src/bridge/grok_bridge.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/agent/grok_agent.py b/src/agent/grok_agent.py index e7b87a1..7e4cf31 100644 --- a/src/agent/grok_agent.py +++ b/src/agent/grok_agent.py @@ -187,8 +187,8 @@ def parse_code_blocks(response_text: str) -> list[dict]: """ blocks = [] - # Pattern 1: lang:/path/to/file (language tag contains path) - lang_path_pattern = re.compile(r'^(\w+):(/[^/\s\n]+(?:/[^/\s\n]+)*)\n', re.MULTILINE) + # Pattern 1: lang:path/to/file (language tag contains path; relative OR absolute) + lang_path_pattern = re.compile(r'^(\w+):([^\s\n]+)\n', re.MULTILINE) # Pattern 2: // FILE: /path or # FILE: /path file_marker_pattern = re.compile( diff --git a/src/bridge/grok_bridge.py b/src/bridge/grok_bridge.py index 5f28b0c..0638886 100644 --- a/src/bridge/grok_bridge.py +++ b/src/bridge/grok_bridge.py @@ -187,7 +187,9 @@ def _safe_dest(output_path, file_path): """ raw = Path(file_path) if raw.is_absolute(): - raise ValueError(f"Absolute paths are not allowed: {file_path!r}") + rebased = Path(raw.name) + print(f"WARNING: Absolute path rebased to output dir: {file_path!r} → {rebased!r}", file=sys.stderr) + raw = rebased if ".." in raw.parts: raise ValueError(f"Path traversal not allowed: {file_path!r}") dest = (output_path / raw).resolve() @@ -224,8 +226,8 @@ def parse_and_write_files(response_text, output_dir): written = [] output_path = Path(output_dir) - # Pattern 1: lang:/path/to/file (language tag contains path) - lang_path_pattern = re.compile(r'^(\w+):(/[^\s\n]+(?:/[^\s\n]+)*)\n', re.MULTILINE) + # Pattern 1: lang:path/to/file (language tag contains path; relative OR absolute — broad match) + lang_path_pattern = re.compile(r'^(\w+):([^\s\n]+)\n', re.MULTILINE) # Pattern 2: // FILE: /path or # FILE: /path file_marker_pattern = re.compile( From ee5234de3b5ac975cbaf82acfe781b08aa1052ee Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:32:17 +0000 Subject: [PATCH 3/7] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 3 unresolved review comments. Co-authored-by: CodeRabbit --- platforms/claude/commands/setup.md | 11 ++++++++--- src/bridge/oauth_setup.py | 27 +++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/platforms/claude/commands/setup.md b/platforms/claude/commands/setup.md index 2cffa0e..837f844 100644 --- a/platforms/claude/commands/setup.md +++ b/platforms/claude/commands/setup.md @@ -32,13 +32,18 @@ to run a test query. **Stop here.** ## Step 2 — Run the OAuth flow Locate `oauth_setup.py` in the plugin's `src/bridge/` directory and run it -with a 200-second timeout: +with a 200-second timeout. The timeout is enforced by the Bash tool invocation +(via the `timeout` parameter in the tool call or CI runner timeout settings): ```bash PLUGIN_ROOT="$(cd "$(dirname "$0")/../../.." 2>/dev/null && pwd)" -python3 "$PLUGIN_ROOT/src/bridge/oauth_setup.py" +timeout 200s python3 "$PLUGIN_ROOT/src/bridge/oauth_setup.py" ``` +**Note**: The `timeout 200s` wrapper ensures the command terminates if the OAuth +flow exceeds 200 seconds. The script itself has an internal 180-second callback +timeout (see `OAUTH_TIMEOUT_SECS`), so the 200s outer limit provides a safety margin. + The script will: 1. Print an authorization URL 2. Start a local server on `localhost:3000` @@ -72,4 +77,4 @@ If the user says they want to use xAI directly (not OpenRouter), run: PLUGIN_ROOT="$(cd "$(dirname "$0")/../../.." 2>/dev/null && pwd)" python3 "$PLUGIN_ROOT/src/bridge/oauth_setup.py" --provider xai ``` -This prints manual credential instructions without attempting OAuth. +This prints manual credential instructions without attempting OAuth. \ No newline at end of file diff --git a/src/bridge/oauth_setup.py b/src/bridge/oauth_setup.py index ad72eb8..35a414b 100644 --- a/src/bridge/oauth_setup.py +++ b/src/bridge/oauth_setup.py @@ -124,7 +124,7 @@ def log_message(self, *args) -> None: # type: ignore[override] def _exchange_code(code: str, code_verifier: str) -> str: """Exchange auth code for API key via OpenRouter token endpoint.""" - payload = json.dumps({"code": code}).encode() + payload = json.dumps({"code": code, "code_verifier": code_verifier}).encode() req = urllib.request.Request( OPENROUTER_TOKEN_URL, data=payload, @@ -194,8 +194,27 @@ def run_oauth_flow() -> int: print() print(f"Waiting up to {OAUTH_TIMEOUT_SECS}s for authorization callback...") - server = HTTPServer(("localhost", CALLBACK_PORT), _CallbackHandler) - server.timeout = OAUTH_TIMEOUT_SECS + # Handle bind-time races: another process may have bound between the + # _check_port_available call and this HTTPServer creation. + max_retries = 3 + server = None + for attempt in range(max_retries): + try: + server = HTTPServer(("localhost", CALLBACK_PORT), _CallbackHandler) + server.timeout = OAUTH_TIMEOUT_SECS + break + except OSError as exc: + if attempt < max_retries - 1: + __import__("time").sleep(0.5) + continue + # Final attempt failed + print( + f"\nERROR: Failed to bind to port {CALLBACK_PORT} after {max_retries} attempts: {exc}", + file=sys.stderr, + ) + print("Please try again or set your key manually:", file=sys.stderr) + print(" export OPENROUTER_API_KEY=sk-or-v1-...", file=sys.stderr) + return 1 deadline = __import__("time").time() + OAUTH_TIMEOUT_SECS while not _received_code and __import__("time").time() < deadline: @@ -295,4 +314,4 @@ def main() -> None: if __name__ == "__main__": - main() + main() \ No newline at end of file From b2c0cbac400b40f0a9ca89f810c071afd4140d0b Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:42:17 +0000 Subject: [PATCH 4/7] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 5 unresolved review comments. Co-authored-by: CodeRabbit --- platforms/claude/commands/setup.md | 24 +++++++++++++++++------- src/bridge/oauth_setup.py | 10 +++++----- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/platforms/claude/commands/setup.md b/platforms/claude/commands/setup.md index 837f844..0d8181d 100644 --- a/platforms/claude/commands/setup.md +++ b/platforms/claude/commands/setup.md @@ -20,7 +20,10 @@ python3 "$(dirname $(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | Alternative (locate bridge relative to this command file): ```bash -BRIDGE_DIR="$(cd "$(dirname "$0")/../../../src/bridge" 2>/dev/null && pwd)" +BRIDGE_DIR="$(dirname $(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1) 2>/dev/null)" +if [ -z "$BRIDGE_DIR" ]; then + BRIDGE_DIR="$(dirname $(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1) 2>/dev/null)" +fi if [ -f "$BRIDGE_DIR/oauth_setup.py" ]; then python3 "$BRIDGE_DIR/oauth_setup.py" --check fi @@ -36,13 +39,17 @@ with a 200-second timeout. The timeout is enforced by the Bash tool invocation (via the `timeout` parameter in the tool call or CI runner timeout settings): ```bash -PLUGIN_ROOT="$(cd "$(dirname "$0")/../../.." 2>/dev/null && pwd)" -timeout 200s python3 "$PLUGIN_ROOT/src/bridge/oauth_setup.py" +PLUGIN_ROOT="$(cd "$(dirname $(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1))/../.." 2>/dev/null && pwd)" +if [ -z "$PLUGIN_ROOT" ]; then + PLUGIN_ROOT="$(cd "$(dirname $(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1))/../.." 2>/dev/null && pwd)" +fi +timeout 240s python3 "$PLUGIN_ROOT/src/bridge/oauth_setup.py" ``` -**Note**: The `timeout 200s` wrapper ensures the command terminates if the OAuth -flow exceeds 200 seconds. The script itself has an internal 180-second callback -timeout (see `OAUTH_TIMEOUT_SECS`), so the 200s outer limit provides a safety margin. +**Note**: The `timeout 240s` wrapper ensures the command terminates if the OAuth +flow exceeds 240 seconds. The script itself has an internal OAUTH_TIMEOUT_SECS +(180s) for the callback phase plus roughly 30s for token exchange, so the 240s +outer limit provides a safe margin. The script will: 1. Print an authorization URL @@ -74,7 +81,10 @@ Show the user the URL printed by the script and tell them: If the user says they want to use xAI directly (not OpenRouter), run: ```bash -PLUGIN_ROOT="$(cd "$(dirname "$0")/../../.." 2>/dev/null && pwd)" +PLUGIN_ROOT="$(cd "$(dirname $(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1))/../.." 2>/dev/null && pwd)" +if [ -z "$PLUGIN_ROOT" ]; then + PLUGIN_ROOT="$(cd "$(dirname $(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1))/../.." 2>/dev/null && pwd)" +fi python3 "$PLUGIN_ROOT/src/bridge/oauth_setup.py" --provider xai ``` This prints manual credential instructions without attempting OAuth. \ No newline at end of file diff --git a/src/bridge/oauth_setup.py b/src/bridge/oauth_setup.py index 35a414b..d37e95b 100644 --- a/src/bridge/oauth_setup.py +++ b/src/bridge/oauth_setup.py @@ -25,6 +25,7 @@ import urllib.request from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path +from typing import List, Tuple # OpenRouter OAuth constants OPENROUTER_AUTH_URL = "https://openrouter.ai/auth" @@ -42,7 +43,7 @@ # PKCE helpers # --------------------------------------------------------------------------- -def _generate_pkce_pair() -> tuple[str, str]: +def _generate_pkce_pair() -> Tuple[str, str]: """Return (code_verifier, code_challenge).""" verifier_bytes = secrets.token_bytes(64) code_verifier = base64.urlsafe_b64encode(verifier_bytes).rstrip(b"=").decode() @@ -96,7 +97,7 @@ def _save_key(api_key: str) -> None: # OAuth callback server # --------------------------------------------------------------------------- -_received_code: list[str] = [] +_received_code: List[str] = [] class _CallbackHandler(BaseHTTPRequestHandler): @@ -238,8 +239,7 @@ def run_oauth_flow() -> int: return 1 _save_key(api_key) - masked = api_key[:12] + "..." if len(api_key) > 12 else api_key - print(f"OK\n\nSuccess! API key saved to {CONFIG_FILE}\nKey: {masked}") + print(f"OK\n\nSuccess! API key saved to {CONFIG_FILE}") return 0 @@ -272,7 +272,7 @@ def print_manual_instructions() -> None: export XAI_API_KEY=xai-... - # or save permanently (grok_bridge will read XAI_API_KEY): + # or save permanently (grok_bridge will read the "api_key" field from config.json): mkdir -p ~/.config/grok-swarm echo '{"api_key": "xai-..."}' > ~/.config/grok-swarm/config.json chmod 600 ~/.config/grok-swarm/config.json From 58eb8f117059289facc23d387fd4f873ac407aa0 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:09:30 +0000 Subject: [PATCH 5/7] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit --- platforms/claude/commands/setup.md | 37 +++++++++++++++++++++--------- src/bridge/oauth_setup.py | 29 ++++++++++++++++++++--- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/platforms/claude/commands/setup.md b/platforms/claude/commands/setup.md index 0d8181d..be29259 100644 --- a/platforms/claude/commands/setup.md +++ b/platforms/claude/commands/setup.md @@ -15,14 +15,23 @@ flow ensures the key never passes through this conversation. Run: ```bash -python3 "$(dirname $(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1))/oauth_setup.py" --check 2>/dev/null || python3 "$(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1)" --check 2>/dev/null +OAUTH_PATH="$(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1)" +if [ -n "$OAUTH_PATH" ]; then + python3 "$(dirname "$OAUTH_PATH")/oauth_setup.py" --check 2>/dev/null +else + OAUTH_PATH="$(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1)" + [ -n "$OAUTH_PATH" ] && python3 "$OAUTH_PATH" --check 2>/dev/null +fi ``` Alternative (locate bridge relative to this command file): ```bash -BRIDGE_DIR="$(dirname $(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1) 2>/dev/null)" -if [ -z "$BRIDGE_DIR" ]; then - BRIDGE_DIR="$(dirname $(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1) 2>/dev/null)" +OAUTH_PATH="$(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1)" +if [ -n "$OAUTH_PATH" ]; then + BRIDGE_DIR="$(dirname "$OAUTH_PATH")" +else + OAUTH_PATH="$(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1)" + [ -n "$OAUTH_PATH" ] && BRIDGE_DIR="$(dirname "$OAUTH_PATH")" fi if [ -f "$BRIDGE_DIR/oauth_setup.py" ]; then python3 "$BRIDGE_DIR/oauth_setup.py" --check @@ -35,13 +44,16 @@ to run a test query. **Stop here.** ## Step 2 — Run the OAuth flow Locate `oauth_setup.py` in the plugin's `src/bridge/` directory and run it -with a 200-second timeout. The timeout is enforced by the Bash tool invocation +with a 240 seconds timeout. The timeout is enforced by the Bash tool invocation (via the `timeout` parameter in the tool call or CI runner timeout settings): ```bash -PLUGIN_ROOT="$(cd "$(dirname $(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1))/../.." 2>/dev/null && pwd)" -if [ -z "$PLUGIN_ROOT" ]; then - PLUGIN_ROOT="$(cd "$(dirname $(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1))/../.." 2>/dev/null && pwd)" +OAUTH_PATH="$(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1)" +if [ -n "$OAUTH_PATH" ]; then + PLUGIN_ROOT="$(cd "$(dirname "$OAUTH_PATH")/../.." 2>/dev/null && pwd)" +else + OAUTH_PATH="$(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1)" + [ -n "$OAUTH_PATH" ] && PLUGIN_ROOT="$(cd "$(dirname "$OAUTH_PATH")/../.." 2>/dev/null && pwd)" fi timeout 240s python3 "$PLUGIN_ROOT/src/bridge/oauth_setup.py" ``` @@ -81,9 +93,12 @@ Show the user the URL printed by the script and tell them: If the user says they want to use xAI directly (not OpenRouter), run: ```bash -PLUGIN_ROOT="$(cd "$(dirname $(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1))/../.." 2>/dev/null && pwd)" -if [ -z "$PLUGIN_ROOT" ]; then - PLUGIN_ROOT="$(cd "$(dirname $(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1))/../.." 2>/dev/null && pwd)" +OAUTH_PATH="$(find ~/.claude/plugins -name 'oauth_setup.py' 2>/dev/null | head -1)" +if [ -n "$OAUTH_PATH" ]; then + PLUGIN_ROOT="$(cd "$(dirname "$OAUTH_PATH")/../.." 2>/dev/null && pwd)" +else + OAUTH_PATH="$(find /usr /usr/local ~/.local -name 'oauth_setup.py' 2>/dev/null | head -1)" + [ -n "$OAUTH_PATH" ] && PLUGIN_ROOT="$(cd "$(dirname "$OAUTH_PATH")/../.." 2>/dev/null && pwd)" fi python3 "$PLUGIN_ROOT/src/bridge/oauth_setup.py" --provider xai ``` diff --git a/src/bridge/oauth_setup.py b/src/bridge/oauth_setup.py index d37e95b..171aca8 100644 --- a/src/bridge/oauth_setup.py +++ b/src/bridge/oauth_setup.py @@ -21,6 +21,7 @@ import secrets import socket import sys +import tempfile import urllib.parse import urllib.request from http.server import BaseHTTPRequestHandler, HTTPServer @@ -89,8 +90,22 @@ def _save_key(api_key: str) -> None: except (json.JSONDecodeError, OSError): pass existing["api_key"] = api_key - CONFIG_FILE.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8") - CONFIG_FILE.chmod(0o600) + + # Write to a temporary file with secure permissions, then atomically rename + json_bytes = (json.dumps(existing, indent=2) + "\n").encode("utf-8") + fd = os.open( + str(CONFIG_DIR / f".config.json.tmp.{os.getpid()}"), + os.O_WRONLY | os.O_CREAT | os.O_EXCL, + 0o600 + ) + try: + os.write(fd, json_bytes) + os.fsync(fd) + finally: + os.close(fd) + + # Atomically replace the target file + os.replace(str(CONFIG_DIR / f".config.json.tmp.{os.getpid()}"), str(CONFIG_FILE)) # --------------------------------------------------------------------------- @@ -161,6 +176,9 @@ def run_oauth_flow() -> int: Returns 0 on success, 1 on failure. """ + # Clear any stale codes from previous runs + _received_code.clear() + if not _check_port_available(): print( f"ERROR: Port {CALLBACK_PORT} is already in use.\n" @@ -238,7 +256,12 @@ def run_oauth_flow() -> int: print(f"FAILED\nERROR: {exc}", file=sys.stderr) return 1 - _save_key(api_key) + try: + _save_key(api_key) + except OSError as exc: + print(f"FAILED\nERROR: Could not save API key to {CONFIG_FILE}: {exc}", file=sys.stderr) + sys.exit(1) + print(f"OK\n\nSuccess! API key saved to {CONFIG_FILE}") return 0 From 5ec80ed998f8ee791b678045dd4a77c90981b74b Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:18:20 +0000 Subject: [PATCH 6/7] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- src/bridge/oauth_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bridge/oauth_setup.py b/src/bridge/oauth_setup.py index 171aca8..61f19fb 100644 --- a/src/bridge/oauth_setup.py +++ b/src/bridge/oauth_setup.py @@ -260,7 +260,7 @@ def run_oauth_flow() -> int: _save_key(api_key) except OSError as exc: print(f"FAILED\nERROR: Could not save API key to {CONFIG_FILE}: {exc}", file=sys.stderr) - sys.exit(1) + return 1 print(f"OK\n\nSuccess! API key saved to {CONFIG_FILE}") return 0 From ef4540181ddf3940a27318f5d77ba56ef25331c2 Mon Sep 17 00:00:00 2001 From: Billy Brenner <43256680+KHAEntertainment@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:29:42 -0700 Subject: [PATCH 7/7] Update platforms/claude/commands/setup.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- platforms/claude/commands/setup.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/platforms/claude/commands/setup.md b/platforms/claude/commands/setup.md index be29259..b5ff7a8 100644 --- a/platforms/claude/commands/setup.md +++ b/platforms/claude/commands/setup.md @@ -82,11 +82,6 @@ Show the user the URL printed by the script and tell them: - If the script exits 0: confirm success and suggest running `/grok-swarm:analyze Hello world` - If it exits 1 (timeout or port conflict): show the error message from the script and suggest the manual fallback: - ``` - mkdir -p ~/.config/grok-swarm - echo '{"api_key": "sk-or-v1-..."}' > ~/.config/grok-swarm/config.json - chmod 600 ~/.config/grok-swarm/config.json - ``` Direct them to https://openrouter.ai/keys for a key. ## xAI Direct Users