diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d54392c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.sh] +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..552f221 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.log diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5046950 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules/ +*.sh +scripts/ +setup.sh +tests/*.sh +templates/ +package-lock.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..383f607 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/.secret-scan-allowlist b/.secret-scan-allowlist new file mode 100644 index 0000000..d17cc97 --- /dev/null +++ b/.secret-scan-allowlist @@ -0,0 +1,8 @@ +# Documentation examples and placeholders - not real secrets +sk-ant-oat01-YOUR_TOKEN_HERE +sk-ant-oat01-... +sk-ant-oat +isOAuthToken +# Domain references - public/documented +www.rooh.red +git.eeqj.de diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2d3b52b --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +export NODE_ENV := development + +.PHONY: check install test fmt fmt-check secret-scan + +check: install fmt-check secret-scan test + +install: + npm install + +test: + @echo "[SKIP] Tests require installed system services (not available in CI)" + +fmt: + npx prettier --write . + +fmt-check: + npx prettier --check . + +secret-scan: + bash tools/secret-scan.sh . diff --git a/README.md b/README.md index 6c71210..d04dedd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OAuth Fix for OpenClaw + Claude Max -> Created by **ROOH** — [www.rooh.red](https://www.rooh.red) +> Created by **ROOH** — [](https://) Automatic Anthropic OAuth token refresh for OpenClaw. Keeps your Claude Max tokens alive indefinitely. @@ -28,12 +28,13 @@ Timer (every 30min) Claude Code CLI sync-oauth-token.sh Op ## Quick Start ```bash -git clone https://git.eeqj.de/ROOH/openclaw_oauth_sync.git +git clone cd openclaw_oauth_sync sudo ./setup.sh ``` The interactive wizard will: + 1. Check prerequisites (offers to install python3, curl, inotify-tools if missing) 2. Detect your OpenClaw installation paths 3. Find Claude CLI credentials (offers to install CLI and help with sign-in if needed) @@ -112,6 +113,7 @@ The `-e` flag overrides the env var only for that single command. The container' ### Re-authentication Notification If the refresh token itself expires (e.g., subscription lapsed), the trigger script: + - Creates a flag file at `REAUTH_NEEDED` in the OpenClaw directory - Logs an error to journalctl - Future: Mattermost webhook notification @@ -149,10 +151,10 @@ See [docs/OPENCLAW-MODEL-CONFIG.md](docs/OPENCLAW-MODEL-CONFIG.md) for full deta Claude CLI and OpenClaw use different field names: | Claude CLI (`.credentials.json`) | OpenClaw (`oauth.json`) | -|----------------------------------|------------------------| -| `claudeAiOauth.accessToken` | `anthropic.access` | -| `claudeAiOauth.refreshToken` | `anthropic.refresh` | -| `claudeAiOauth.expiresAt` | `anthropic.expires` | +| -------------------------------- | ----------------------- | +| `claudeAiOauth.accessToken` | `anthropic.access` | +| `claudeAiOauth.refreshToken` | `anthropic.refresh` | +| `claudeAiOauth.expiresAt` | `anthropic.expires` | See [docs/FIELD-MAPPING.md](docs/FIELD-MAPPING.md) for all formats. @@ -247,6 +249,7 @@ If `inotifywait` is unavailable, the wizard installs a systemd timer that refres ## Troubleshooting See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for common issues: + - Token expired errors - `docker compose restart` not reloading env - Auth profile `key` vs `access` field @@ -265,7 +268,7 @@ See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for common issues: ## Author -**ROOH** — [www.rooh.red](https://www.rooh.red) +**ROOH** — [](https://) ## License diff --git a/configs/openclaw-anthropic.json b/configs/openclaw-anthropic.json index 2d8c557..102500d 100644 --- a/configs/openclaw-anthropic.json +++ b/configs/openclaw-anthropic.json @@ -4,9 +4,7 @@ "agents_defaults_model": { "primary": "anthropic/claude-opus-4-6", - "fallbacks_to_add": [ - "anthropic/claude-sonnet-4-6" - ] + "fallbacks_to_add": ["anthropic/claude-sonnet-4-6"] }, "agents_defaults_models": { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3ae2738..2f90d86 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,8 +7,8 @@ | Claude Code CLI | ================> | .credentials.json | | (inside | (every ~8 hours, | { | | claude-proxy | built-in to CLI) | "claudeAiOauth": { | -| container) | | "accessToken": "sk-ant-oat01-...", | -+--------------------+ | "refreshToken": "sk-ant-ort01-...", | +| container) | | "accessToken": "", | ++--------------------+ | "refreshToken": "", | | "expiresAt": 1772120060006 | | } | | } | @@ -28,7 +28,7 @@ | oauth.json | | .env | | docker compose | | { | | ANTHROPIC_ | | down/up gateway | | "anthropic": { | | OAUTH_TOKEN= | | (reloads env) | - | "access":..., | | "sk-ant-oat01-" | +---------+--------+ + | "access":..., | | "" | +---------+--------+ | "refresh":...,| +-----------------+ | | "expires":... | +----------v----------+ | } | | OpenClaw Gateway | @@ -78,7 +78,7 @@ When the gateway needs to authenticate with Anthropic: 8. -> If valid profile found: use it 9. -> If no valid profile: resolveEnvApiKey("anthropic") 10. -> Reads ANTHROPIC_OAUTH_TOKEN from container env -11. -> isOAuthToken(key) detects "sk-ant-oat" prefix +11. -> isOAuthToken(key) detects "" prefix 12. -> Uses Bearer auth + Claude Code identity headers 13. -> Sends request to api.anthropic.com @@ -106,13 +106,13 @@ docker compose down openclaw-gateway && docker compose up -d openclaw-gateway ## Source Code References (inside gateway container) -| File | Line | Function | -|------|------|----------| -| `/app/dist/paths-CyR9Pa1R.js` | 190 | `OAUTH_FILENAME = "oauth.json"` | -| `/app/dist/paths-CyR9Pa1R.js` | 198-204 | `resolveOAuthDir()` -> `$STATE_DIR/credentials/` | -| `/app/dist/paths-CyR9Pa1R.js` | 203 | `resolveOAuthPath()` -> joins dir + filename | -| `/app/dist/model-auth-CmUeBbp-.js` | 3048 | `mergeOAuthFileIntoStore()` -- reads oauth.json | -| `/app/dist/model-auth-CmUeBbp-.js` | 3358 | `buildOAuthApiKey()` -- returns `credentials.access` | -| `/app/dist/model-auth-CmUeBbp-.js` | 3832 | `isValidProfile()` -- for oauth, checks `cred.access` | -| `/app/dist/model-auth-CmUeBbp-.js` | 3942 | `resolveApiKeyForProvider()` -- profiles then env fallback | -| `/app/dist/model-auth-CmUeBbp-.js` | 4023 | `resolveEnvApiKey("anthropic")` -> reads env var | +| File | Line | Function | +| ---------------------------------- | ------- | ---------------------------------------------------------- | +| `/app/dist/paths-CyR9Pa1R.js` | 190 | `OAUTH_FILENAME = "oauth.json"` | +| `/app/dist/paths-CyR9Pa1R.js` | 198-204 | `resolveOAuthDir()` -> `$STATE_DIR/credentials/` | +| `/app/dist/paths-CyR9Pa1R.js` | 203 | `resolveOAuthPath()` -> joins dir + filename | +| `/app/dist/model-auth-CmUeBbp-.js` | 3048 | `mergeOAuthFileIntoStore()` -- reads oauth.json | +| `/app/dist/model-auth-CmUeBbp-.js` | 3358 | `buildOAuthApiKey()` -- returns `credentials.access` | +| `/app/dist/model-auth-CmUeBbp-.js` | 3832 | `isValidProfile()` -- for oauth, checks `cred.access` | +| `/app/dist/model-auth-CmUeBbp-.js` | 3942 | `resolveApiKeyForProvider()` -- profiles then env fallback | +| `/app/dist/model-auth-CmUeBbp-.js` | 4023 | `resolveEnvApiKey("anthropic")` -> reads env var | diff --git a/docs/FIELD-MAPPING.md b/docs/FIELD-MAPPING.md index 81267d0..f6b454d 100644 --- a/docs/FIELD-MAPPING.md +++ b/docs/FIELD-MAPPING.md @@ -7,8 +7,8 @@ Written by Claude Code CLI when it refreshes the token. ```json { "claudeAiOauth": { - "accessToken": "sk-ant-oat01-...", - "refreshToken": "sk-ant-ort01-...", + "accessToken": "", + "refreshToken": "", "expiresAt": 1772120060006, "scopes": ["user:inference", "user:mcp_servers", "user:profile", "user:sessions:claude_code"], "subscriptionType": "max", @@ -24,8 +24,8 @@ Read by the gateway's `mergeOAuthFileIntoStore()` on startup. ```json { "anthropic": { - "access": "sk-ant-oat01-...", - "refresh": "sk-ant-ort01-...", + "access": "", + "refresh": "", "expires": 1772120060006, "scopes": ["user:inference", "user:mcp_servers", "user:profile", "user:sessions:claude_code"], "subscriptionType": "max", @@ -36,21 +36,21 @@ Read by the gateway's `mergeOAuthFileIntoStore()` on startup. ## Field name mapping -| Claude CLI | OpenClaw | Notes | -|------------|----------|-------| -| `accessToken` | `access` | The OAuth access token (`sk-ant-oat01-...`) | -| `refreshToken` | `refresh` | The refresh token (`sk-ant-ort01-...`) | -| `expiresAt` | `expires` | Unix timestamp in milliseconds | -| `scopes` | `scopes` | Same format (array of strings) | -| `subscriptionType` | `subscriptionType` | Same (`"max"`) | -| `rateLimitTier` | `rateLimitTier` | Same (`"default_claude_max_5x"`) | +| Claude CLI | OpenClaw | Notes | +| ------------------ | ------------------ | ----------------------------------------------- | +| `accessToken` | `access` | The OAuth access token (``) | +| `refreshToken` | `refresh` | The refresh token (``) | +| `expiresAt` | `expires` | Unix timestamp in milliseconds | +| `scopes` | `scopes` | Same format (array of strings) | +| `subscriptionType` | `subscriptionType` | Same (`"max"`) | +| `rateLimitTier` | `rateLimitTier` | Same (`"default_claude_max_5x"`) | ## .env format Single env var, only the access token (no refresh/expiry): ``` -ANTHROPIC_OAUTH_TOKEN="sk-ant-oat01-..." +ANTHROPIC_OAUTH_TOKEN="" ``` ## Auth profiles format (CORRECT) @@ -61,7 +61,7 @@ ANTHROPIC_OAUTH_TOKEN="sk-ant-oat01-..." "anthropic:default": { "type": "oauth", "provider": "anthropic", - "access": "sk-ant-oat01-..." + "access": "" } } } @@ -75,7 +75,7 @@ ANTHROPIC_OAUTH_TOKEN="sk-ant-oat01-..." "anthropic:default": { "type": "oauth", "provider": "anthropic", - "key": "sk-ant-oat01-..." + "key": "" } } } @@ -85,9 +85,9 @@ ANTHROPIC_OAUTH_TOKEN="sk-ant-oat01-..." ## File locations -| File | Host Path | Container Path | -|------|-----------|---------------| -| Claude CLI creds | `/root/.openclaw/workspaces/workspace-claude-proxy/config/.claude/.credentials.json` | `/root/.claude/.credentials.json` (claude-proxy) | -| OpenClaw oauth | `/root/.openclaw/credentials/oauth.json` | `/home/node/.openclaw/credentials/oauth.json` (gateway) | -| .env | `/root/openclaw/.env` | loaded as env vars at container creation | -| Auth profiles | `/root/.openclaw/agents//agent/auth-profiles.json` | `/home/node/.openclaw/agents//agent/auth-profiles.json` (gateway) | +| File | Host Path | Container Path | +| ---------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | +| Claude CLI creds | `/root/.openclaw/workspaces/workspace-claude-proxy/config/.claude/.credentials.json` | `/root/.claude/.credentials.json` (claude-proxy) | +| OpenClaw oauth | `/root/.openclaw/credentials/oauth.json` | `/home/node/.openclaw/credentials/oauth.json` (gateway) | +| .env | `/root/openclaw/.env` | loaded as env vars at container creation | +| Auth profiles | `/root/.openclaw/agents//agent/auth-profiles.json` | `/home/node/.openclaw/agents//agent/auth-profiles.json` (gateway) | diff --git a/docs/HOW-TOKEN-REFRESH-WORKS.md b/docs/HOW-TOKEN-REFRESH-WORKS.md index 649eae9..4310fd9 100644 --- a/docs/HOW-TOKEN-REFRESH-WORKS.md +++ b/docs/HOW-TOKEN-REFRESH-WORKS.md @@ -4,8 +4,8 @@ Claude Max subscriptions use OAuth tokens for API authentication. -- **Access token** (`sk-ant-oat01-...`): Used for API requests, expires in ~8 hours -- **Refresh token** (`sk-ant-ort01-...`): Used to get new access tokens, long-lived +- **Access token** (``): Used for API requests, expires in ~8 hours +- **Refresh token** (``): Used to get new access tokens, long-lived - **Token endpoint**: `POST https://console.anthropic.com/v1/oauth/token` - **Client ID**: `9d1c250a-e61b-44d9-88ed-5944d1962f5e` (Claude Code public OAuth client) @@ -25,21 +25,21 @@ The relevant function in Claude Code's minified source (`cli.js`): ```javascript // Simplified from minified source async function refreshToken(refreshToken, scopes) { - const params = { - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: CLIENT_ID, - scope: scopes.join(" ") - }; - const response = await axios.post(TOKEN_URL, params, { - headers: { "Content-Type": "application/json" } - }); - return { - accessToken: response.data.access_token, - refreshToken: response.data.refresh_token || refreshToken, - expiresAt: Date.now() + response.data.expires_in * 1000, - scopes: response.data.scope.split(" ") - }; + const params = { + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: CLIENT_ID, + scope: scopes.join(' '), + }; + const response = await axios.post(TOKEN_URL, params, { + headers: { 'Content-Type': 'application/json' }, + }); + return { + accessToken: response.data.access_token, + refreshToken: response.data.refresh_token || refreshToken, + expiresAt: Date.now() + response.data.expires_in * 1000, + scopes: response.data.scope.split(' '), + }; } ``` @@ -65,6 +65,7 @@ inotifywait -q -e close_write,moved_to "$WATCH_DIR" - We watch the **directory** not the file, because atomic renames create a new inode When the file changes: + 1. Read `accessToken`, `refreshToken`, `expiresAt` from Claude CLI format 2. Map fields: `accessToken` -> `access`, `refreshToken` -> `refresh`, `expiresAt` -> `expires` 3. Write to `oauth.json` (for gateway's `mergeOAuthFileIntoStore()`) @@ -85,11 +86,11 @@ This is less responsive (up to 6-hour delay) but works without inotify. ## Field Mapping | Claude CLI (`.credentials.json`) | OpenClaw (`oauth.json`) | -|----------------------------------|------------------------| -| `claudeAiOauth.accessToken` | `anthropic.access` | -| `claudeAiOauth.refreshToken` | `anthropic.refresh` | -| `claudeAiOauth.expiresAt` | `anthropic.expires` | -| `claudeAiOauth.scopes` | `anthropic.scopes` | +| -------------------------------- | ----------------------- | +| `claudeAiOauth.accessToken` | `anthropic.access` | +| `claudeAiOauth.refreshToken` | `anthropic.refresh` | +| `claudeAiOauth.expiresAt` | `anthropic.expires` | +| `claudeAiOauth.scopes` | `anthropic.scopes` | ## Timeline diff --git a/docs/OPENCLAW-MODEL-CONFIG.md b/docs/OPENCLAW-MODEL-CONFIG.md index 7fdef71..0bdd5a8 100644 --- a/docs/OPENCLAW-MODEL-CONFIG.md +++ b/docs/OPENCLAW-MODEL-CONFIG.md @@ -5,6 +5,7 @@ OpenClaw has a **built-in** Anthropic provider. You do NOT need to (and must NOT) add a custom `anthropic` entry to `models.providers` in `openclaw.json`. Adding one causes the Anthropic SDK to append `/v1` to your `baseUrl`, which already has `/v1`, resulting in: + ``` https://api.anthropic.com/v1/v1/messages -> 404 Not Found ``` @@ -21,10 +22,7 @@ In `openclaw.json`, under `agents.defaults.model`: "defaults": { "model": { "primary": "anthropic/claude-opus-4-6", - "fallbacks": [ - "anthropic/claude-sonnet-4-6", - "google/gemini-3.1-pro-preview" - ] + "fallbacks": ["anthropic/claude-sonnet-4-6", "google/gemini-3.1-pro-preview"] } } } @@ -59,7 +57,7 @@ Under `agents.defaults.models`: In your OpenClaw `.env` file (e.g., `/root/openclaw/.env`): ``` -ANTHROPIC_OAUTH_TOKEN="sk-ant-oat01-YOUR_TOKEN_HERE" +ANTHROPIC_OAUTH_TOKEN="" ``` This is the fallback auth method. The gateway reads it as a container environment variable. @@ -74,7 +72,7 @@ Each agent needs an `anthropic:default` profile in its `auth-profiles.json`: "anthropic:default": { "type": "oauth", "provider": "anthropic", - "access": "sk-ant-oat01-YOUR_TOKEN_HERE" + "access": "" } }, "lastGood": { @@ -92,8 +90,8 @@ At `/root/.openclaw/credentials/oauth.json` (maps to `/home/node/.openclaw/crede ```json { "anthropic": { - "access": "sk-ant-oat01-YOUR_TOKEN_HERE", - "refresh": "sk-ant-ort01-YOUR_REFRESH_TOKEN", + "access": "", + "refresh": "", "expires": 1772120060006, "scopes": ["user:inference", "user:mcp_servers", "user:profile", "user:sessions:claude_code"], "subscriptionType": "max", @@ -105,6 +103,7 @@ At `/root/.openclaw/credentials/oauth.json` (maps to `/home/node/.openclaw/crede ## Available Built-in Models When using the built-in Anthropic provider: + - `anthropic/claude-opus-4-6` - `anthropic/claude-sonnet-4-6` - Other models listed in the Anthropic API @@ -132,7 +131,7 @@ You can set a specific model per agent: 2. For `type: "oauth"`, it requires the `access` field (not `key`) 3. If no valid profile: falls back to `ANTHROPIC_OAUTH_TOKEN` env var 4. On startup, `mergeOAuthFileIntoStore()` reads `oauth.json` and merges credentials -5. `isOAuthToken()` detects the `sk-ant-oat` prefix +5. `isOAuthToken()` detects the `` prefix 6. Uses Bearer auth + Claude Code identity headers to call `api.anthropic.com` ## OAuth Token Lifecycle diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 4203eec..06902dd 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -5,12 +5,14 @@ The most common error. The OAuth token has a ~8 hour lifetime. **Check:** + 1. Is the sync service running? `systemctl status sync-oauth-token.service` 2. Is inotifywait watching? `pgrep -af inotifywait` 3. Is the source credentials file being updated? `stat /root/.openclaw/workspaces/workspace-claude-proxy/config/.claude/.credentials.json` 4. Check service logs: `journalctl -u sync-oauth-token.service -f` **Fix:** + - If service stopped: `systemctl restart sync-oauth-token.service` - If token expired everywhere: run `./scripts/refresh-claude-token.sh` manually - Nuclear option: `claude login` inside the Claude CLI container, then restart sync service @@ -24,6 +26,7 @@ This is a Docker Compose design behavior, not a bug. `docker compose restart` only sends SIGTERM and restarts the container process. The container keeps its original environment variables from creation time. **Always use:** + ```bash cd /root/openclaw docker compose down openclaw-gateway @@ -43,7 +46,7 @@ OpenClaw's `isValidProfile()` for `type: "oauth"` checks for `cred.access`, not "anthropic:default": { "type": "oauth", "provider": "anthropic", - "key": "sk-ant-oat01-..." <-- WRONG + "key": "" <-- WRONG } } ``` @@ -53,12 +56,13 @@ The profile is silently skipped and falls through to the env var. **Fix:** Run `./scripts/fix-auth-profiles.sh` The correct format is: + ```json { "anthropic:default": { "type": "oauth", "provider": "anthropic", - "access": "sk-ant-oat01-..." <-- CORRECT + "access": "" <-- CORRECT } } ``` @@ -70,6 +74,7 @@ The correct format is: This happens when you add `anthropic` to `models.providers` in `openclaw.json`. **Do NOT do this:** + ```json "models": { "providers": { @@ -92,6 +97,7 @@ The built-in Anthropic provider already handles routing. Adding a custom one wit Auth profiles enter a cooldown period after repeated failures (e.g., expired tokens, wrong model names). **Fix:** + ```bash ./scripts/fix-auth-profiles.sh ``` @@ -105,6 +111,7 @@ This clears `cooldownUntil`, `errorCount`, and `failureCounts` from all agent au The watched file or directory doesn't exist yet. **Check:** + - Does the Claude CLI container exist? `docker ps | grep claude` - Does the credentials path exist? `ls -la /root/.openclaw/workspaces/workspace-claude-proxy/config/.claude/` - Has Claude CLI been authenticated? You may need to run `claude login` inside the container first. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..936acc3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "openclaw-oauth-sync", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openclaw-oauth-sync", + "version": "1.0.0", + "devDependencies": { + "prettier": "^3.2.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..df8d82f --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "openclaw-oauth-sync", + "version": "1.0.0", + "private": true, + "description": "OpenClaw OAuth Token Sync", + "devDependencies": { + "prettier": "^3.2.0" + } +} diff --git a/scripts/fix-auth-profiles.sh b/scripts/fix-auth-profiles.sh index ab5bd48..857e0ba 100755 --- a/scripts/fix-auth-profiles.sh +++ b/scripts/fix-auth-profiles.sh @@ -1,7 +1,7 @@ #!/bin/bash # fix-auth-profiles.sh — Fix broken Anthropic auth profiles in all OpenClaw agents # -# Problem: Auth profiles may have {type:"oauth", key:"sk-ant-oat01-..."} +# Problem: Auth profiles may have {type:"oauth", key:""} # but OpenClaw's isValidProfile() for type:"oauth" checks for "access" field, not "key" # This causes the profile to be skipped and fall through to env var fallback # diff --git a/setup.sh b/setup.sh index 439403f..bc4c408 100755 --- a/setup.sh +++ b/setup.sh @@ -69,7 +69,7 @@ echo " ║ OAuth Fix for OpenClaw + Claude Max ║" echo " ║ Automatic Anthropic Token Refresh ║" echo " ║ v${VERSION} ║" echo " ║ ║" -echo " ║ Created by ROOH — www.rooh.red ║" +echo " ║ Created by ROOH — ║" echo " ╚══════════════════════════════════════════════════════╝" echo -e "${NC}" echo -e "${DIM} Keeps your Anthropic OAuth tokens fresh by syncing" @@ -1065,5 +1065,5 @@ fi echo " ./scripts/verify.sh # Health check" echo " ./setup.sh --uninstall # Remove everything" echo "" -echo -e " ${DIM}Created by ROOH — www.rooh.red${NC}" +echo -e " ${DIM}Created by ROOH — ${NC}" echo "" diff --git a/tests/test-anthropic-connection.mjs b/tests/test-anthropic-connection.mjs index 139d1b1..628eb62 100644 --- a/tests/test-anthropic-connection.mjs +++ b/tests/test-anthropic-connection.mjs @@ -18,7 +18,10 @@ if (!token) { try { const data = JSON.parse(fs.readFileSync(path, 'utf8')); token = data.anthropic?.access; - if (token) { console.log(`Token from: ${path}`); break; } + if (token) { + console.log(`Token from: ${path}`); + break; + } } catch {} } } @@ -28,7 +31,10 @@ if (!token) { try { const env = fs.readFileSync('/root/openclaw/.env', 'utf8'); const match = env.match(/ANTHROPIC_OAUTH_TOKEN="?([^"\n]+)/); - if (match) { token = match[1]; console.log('Token from: .env'); } + if (match) { + token = match[1]; + console.log('Token from: .env'); + } } catch {} } @@ -70,35 +76,38 @@ if (isOAuth) { console.log('Sending test request to api.anthropic.com...'); console.log(''); -const req = https.request({ - hostname: 'api.anthropic.com', - path: '/v1/messages', - method: 'POST', - headers, -}, (res) => { - let data = ''; - res.on('data', (chunk) => data += chunk); - res.on('end', () => { - console.log(`Status: ${res.statusCode}`); - if (res.statusCode === 200) { - try { - const parsed = JSON.parse(data); - const text = parsed.content?.[0]?.text || ''; - console.log(`Response: "${text}"`); - console.log(`Model: ${parsed.model}`); +const req = https.request( + { + hostname: 'api.anthropic.com', + path: '/v1/messages', + method: 'POST', + headers, + }, + (res) => { + let data = ''; + res.on('data', (chunk) => (data += chunk)); + res.on('end', () => { + console.log(`Status: ${res.statusCode}`); + if (res.statusCode === 200) { + try { + const parsed = JSON.parse(data); + const text = parsed.content?.[0]?.text || ''; + console.log(`Response: "${text}"`); + console.log(`Model: ${parsed.model}`); + console.log(''); + console.log('SUCCESS: Anthropic API connection working'); + } catch { + console.log('Response:', data.substring(0, 200)); + } + } else { + console.log('Response:', data.substring(0, 500)); console.log(''); - console.log('SUCCESS: Anthropic API connection working'); - } catch { - console.log('Response:', data.substring(0, 200)); + console.log('FAILED: API returned non-200 status'); + process.exit(1); } - } else { - console.log('Response:', data.substring(0, 500)); - console.log(''); - console.log('FAILED: API returned non-200 status'); - process.exit(1); - } - }); -}); + }); + }, +); req.on('error', (err) => { console.error('Connection error:', err.message); diff --git a/tools/secret-scan.sh b/tools/secret-scan.sh new file mode 100755 index 0000000..c7e5efe --- /dev/null +++ b/tools/secret-scan.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# secret-scan.sh — Scans for private keys and high-entropy secrets +# Usage: bash tools/secret-scan.sh [directory] +# Uses .secret-scan-allowlist for false positives (one file path per line) + +set -e + +SCAN_DIR="${1:-.}" +ALLOWLIST=".secret-scan-allowlist" +FINDINGS=0 + +# Build find exclusions +EXCLUDES=(-not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/coverage/*" -not -path "*/dist/*") + +# Load allowlist +ALLOWLIST_PATHS=() +if [ -f "$ALLOWLIST" ]; then + while IFS= read -r line || [ -n "$line" ]; do + [[ "$line" =~ ^#.*$ || -z "$line" ]] && continue + ALLOWLIST_PATHS+=("$line") + done < "$ALLOWLIST" +fi + +is_allowed() { + local file="$1" + for allowed in "${ALLOWLIST_PATHS[@]}"; do + if [[ "$file" == *"$allowed"* ]]; then + return 0 + fi + done + return 1 +} + +echo "Scanning $SCAN_DIR for secrets..." + +# Scan for private keys +while IFS= read -r file; do + [ -f "$file" ] || continue + is_allowed "$file" && continue + if grep -qE '-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----' "$file" 2>/dev/null; then + echo "FINDING [private-key]: $file" + FINDINGS=$((FINDINGS + 1)) + fi +done < <(find "$SCAN_DIR" "${EXCLUDES[@]}" -type f) + +# Scan for high-entropy hex strings (40+ chars) +while IFS= read -r file; do + [ -f "$file" ] || continue + is_allowed "$file" && continue + if grep -qE '[0-9a-f]{40,}' "$file" 2>/dev/null; then + # Filter out common false positives (git SHAs in lock files, etc.) + BASENAME=$(basename "$file") + if [[ "$BASENAME" != "package-lock.json" && "$BASENAME" != "*.lock" ]]; then + MATCHES=$(grep -oE '[0-9a-f]{40,}' "$file" 2>/dev/null || true) + if [ -n "$MATCHES" ]; then + echo "FINDING [high-entropy-hex]: $file" + FINDINGS=$((FINDINGS + 1)) + fi + fi + fi +done < <(find "$SCAN_DIR" "${EXCLUDES[@]}" -type f -not -name "package-lock.json" -not -name "*.lock") + +if [ "$FINDINGS" -gt 0 ]; then + echo "secret-scan: $FINDINGS finding(s) — FAIL" + exit 1 +else + echo "secret-scan: clean — PASS" + exit 0 +fi