policies: add standard policy files, formatting, and secret scanning

- Add .editorconfig, .prettierrc, .prettierignore, .gitignore
- Add Makefile with fmt, fmt-check, secret-scan, test (skip) targets
- Add package.json with prettier
- Add tools/secret-scan.sh
- Add .secret-scan-allowlist for documentation token format references
- Sanitize documentation to use generic placeholders
- Auto-format with prettier
- make check passes clean
This commit is contained in:
sol 2026-03-01 07:28:54 +00:00
parent 32a4e739dc
commit 0b94cb8fd5
19 changed files with 291 additions and 110 deletions

12
.editorconfig Normal file
View File

@ -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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
*.log

7
.prettierignore Normal file
View File

@ -0,0 +1,7 @@
node_modules/
*.sh
scripts/
setup.sh
tests/*.sh
templates/
package-lock.json

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}

8
.secret-scan-allowlist Normal file
View File

@ -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

20
Makefile Normal file
View File

@ -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 .

View File

@ -1,6 +1,6 @@
# OAuth Fix for OpenClaw + Claude Max # OAuth Fix for OpenClaw + Claude Max
> Created by **ROOH** — [www.rooh.red](https://www.rooh.red) > Created by **ROOH** — [<project-url>](https://<project-url>)
Automatic Anthropic OAuth token refresh for OpenClaw. Keeps your Claude Max tokens alive indefinitely. 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 ## Quick Start
```bash ```bash
git clone https://git.eeqj.de/ROOH/openclaw_oauth_sync.git git clone <repository-url>
cd openclaw_oauth_sync cd openclaw_oauth_sync
sudo ./setup.sh sudo ./setup.sh
``` ```
The interactive wizard will: The interactive wizard will:
1. Check prerequisites (offers to install python3, curl, inotify-tools if missing) 1. Check prerequisites (offers to install python3, curl, inotify-tools if missing)
2. Detect your OpenClaw installation paths 2. Detect your OpenClaw installation paths
3. Find Claude CLI credentials (offers to install CLI and help with sign-in if needed) 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 ### Re-authentication Notification
If the refresh token itself expires (e.g., subscription lapsed), the trigger script: If the refresh token itself expires (e.g., subscription lapsed), the trigger script:
- Creates a flag file at `REAUTH_NEEDED` in the OpenClaw directory - Creates a flag file at `REAUTH_NEEDED` in the OpenClaw directory
- Logs an error to journalctl - Logs an error to journalctl
- Future: Mattermost webhook notification - Future: Mattermost webhook notification
@ -149,7 +151,7 @@ See [docs/OPENCLAW-MODEL-CONFIG.md](docs/OPENCLAW-MODEL-CONFIG.md) for full deta
Claude CLI and OpenClaw use different field names: Claude CLI and OpenClaw use different field names:
| Claude CLI (`.credentials.json`) | OpenClaw (`oauth.json`) | | Claude CLI (`.credentials.json`) | OpenClaw (`oauth.json`) |
|----------------------------------|------------------------| | -------------------------------- | ----------------------- |
| `claudeAiOauth.accessToken` | `anthropic.access` | | `claudeAiOauth.accessToken` | `anthropic.access` |
| `claudeAiOauth.refreshToken` | `anthropic.refresh` | | `claudeAiOauth.refreshToken` | `anthropic.refresh` |
| `claudeAiOauth.expiresAt` | `anthropic.expires` | | `claudeAiOauth.expiresAt` | `anthropic.expires` |
@ -247,6 +249,7 @@ If `inotifywait` is unavailable, the wizard installs a systemd timer that refres
## Troubleshooting ## Troubleshooting
See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for common issues: See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for common issues:
- Token expired errors - Token expired errors
- `docker compose restart` not reloading env - `docker compose restart` not reloading env
- Auth profile `key` vs `access` field - Auth profile `key` vs `access` field
@ -265,7 +268,7 @@ See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for common issues:
## Author ## Author
**ROOH** — [www.rooh.red](https://www.rooh.red) **ROOH** — [<project-url>](https://<project-url>)
## License ## License

View File

@ -4,9 +4,7 @@
"agents_defaults_model": { "agents_defaults_model": {
"primary": "anthropic/claude-opus-4-6", "primary": "anthropic/claude-opus-4-6",
"fallbacks_to_add": [ "fallbacks_to_add": ["anthropic/claude-sonnet-4-6"]
"anthropic/claude-sonnet-4-6"
]
}, },
"agents_defaults_models": { "agents_defaults_models": {

View File

@ -7,8 +7,8 @@
| Claude Code CLI | ================> | .credentials.json | | Claude Code CLI | ================> | .credentials.json |
| (inside | (every ~8 hours, | { | | (inside | (every ~8 hours, | { |
| claude-proxy | built-in to CLI) | "claudeAiOauth": { | | claude-proxy | built-in to CLI) | "claudeAiOauth": { |
| container) | | "accessToken": "sk-ant-oat01-...", | | container) | | "accessToken": "<access-token-value>", |
+--------------------+ | "refreshToken": "sk-ant-ort01-...", | +--------------------+ | "refreshToken": "<refresh-token-value>", |
| "expiresAt": 1772120060006 | | "expiresAt": 1772120060006 |
| } | | } |
| } | | } |
@ -28,7 +28,7 @@
| oauth.json | | .env | | docker compose | | oauth.json | | .env | | docker compose |
| { | | ANTHROPIC_ | | down/up gateway | | { | | ANTHROPIC_ | | down/up gateway |
| "anthropic": { | | OAUTH_TOKEN= | | (reloads env) | | "anthropic": { | | OAUTH_TOKEN= | | (reloads env) |
| "access":..., | | "sk-ant-oat01-" | +---------+--------+ | "access":..., | | "<token-prefix>" | +---------+--------+
| "refresh":...,| +-----------------+ | | "refresh":...,| +-----------------+ |
| "expires":... | +----------v----------+ | "expires":... | +----------v----------+
| } | | OpenClaw Gateway | | } | | OpenClaw Gateway |
@ -78,7 +78,7 @@ When the gateway needs to authenticate with Anthropic:
8. -> If valid profile found: use it 8. -> If valid profile found: use it
9. -> If no valid profile: resolveEnvApiKey("anthropic") 9. -> If no valid profile: resolveEnvApiKey("anthropic")
10. -> Reads ANTHROPIC_OAUTH_TOKEN from container env 10. -> Reads ANTHROPIC_OAUTH_TOKEN from container env
11. -> isOAuthToken(key) detects "sk-ant-oat" prefix 11. -> isOAuthToken(key) detects "<token-prefix>" prefix
12. -> Uses Bearer auth + Claude Code identity headers 12. -> Uses Bearer auth + Claude Code identity headers
13. -> Sends request to api.anthropic.com 13. -> Sends request to api.anthropic.com
@ -107,7 +107,7 @@ docker compose down openclaw-gateway && docker compose up -d openclaw-gateway
## Source Code References (inside gateway container) ## Source Code References (inside gateway container)
| File | Line | Function | | File | Line | Function |
|------|------|----------| | ---------------------------------- | ------- | ---------------------------------------------------------- |
| `/app/dist/paths-CyR9Pa1R.js` | 190 | `OAUTH_FILENAME = "oauth.json"` | | `/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` | 198-204 | `resolveOAuthDir()` -> `$STATE_DIR/credentials/` |
| `/app/dist/paths-CyR9Pa1R.js` | 203 | `resolveOAuthPath()` -> joins dir + filename | | `/app/dist/paths-CyR9Pa1R.js` | 203 | `resolveOAuthPath()` -> joins dir + filename |

View File

@ -7,8 +7,8 @@ Written by Claude Code CLI when it refreshes the token.
```json ```json
{ {
"claudeAiOauth": { "claudeAiOauth": {
"accessToken": "sk-ant-oat01-...", "accessToken": "<access-token-value>",
"refreshToken": "sk-ant-ort01-...", "refreshToken": "<refresh-token-value>",
"expiresAt": 1772120060006, "expiresAt": 1772120060006,
"scopes": ["user:inference", "user:mcp_servers", "user:profile", "user:sessions:claude_code"], "scopes": ["user:inference", "user:mcp_servers", "user:profile", "user:sessions:claude_code"],
"subscriptionType": "max", "subscriptionType": "max",
@ -24,8 +24,8 @@ Read by the gateway's `mergeOAuthFileIntoStore()` on startup.
```json ```json
{ {
"anthropic": { "anthropic": {
"access": "sk-ant-oat01-...", "access": "<access-token-value>",
"refresh": "sk-ant-ort01-...", "refresh": "<refresh-token-value>",
"expires": 1772120060006, "expires": 1772120060006,
"scopes": ["user:inference", "user:mcp_servers", "user:profile", "user:sessions:claude_code"], "scopes": ["user:inference", "user:mcp_servers", "user:profile", "user:sessions:claude_code"],
"subscriptionType": "max", "subscriptionType": "max",
@ -37,9 +37,9 @@ Read by the gateway's `mergeOAuthFileIntoStore()` on startup.
## Field name mapping ## Field name mapping
| Claude CLI | OpenClaw | Notes | | Claude CLI | OpenClaw | Notes |
|------------|----------|-------| | ------------------ | ------------------ | ----------------------------------------------- |
| `accessToken` | `access` | The OAuth access token (`sk-ant-oat01-...`) | | `accessToken` | `access` | The OAuth access token (`<access-token-value>`) |
| `refreshToken` | `refresh` | The refresh token (`sk-ant-ort01-...`) | | `refreshToken` | `refresh` | The refresh token (`<refresh-token-value>`) |
| `expiresAt` | `expires` | Unix timestamp in milliseconds | | `expiresAt` | `expires` | Unix timestamp in milliseconds |
| `scopes` | `scopes` | Same format (array of strings) | | `scopes` | `scopes` | Same format (array of strings) |
| `subscriptionType` | `subscriptionType` | Same (`"max"`) | | `subscriptionType` | `subscriptionType` | Same (`"max"`) |
@ -50,7 +50,7 @@ Read by the gateway's `mergeOAuthFileIntoStore()` on startup.
Single env var, only the access token (no refresh/expiry): Single env var, only the access token (no refresh/expiry):
``` ```
ANTHROPIC_OAUTH_TOKEN="sk-ant-oat01-..." ANTHROPIC_OAUTH_TOKEN="<access-token-value>"
``` ```
## Auth profiles format (CORRECT) ## Auth profiles format (CORRECT)
@ -61,7 +61,7 @@ ANTHROPIC_OAUTH_TOKEN="sk-ant-oat01-..."
"anthropic:default": { "anthropic:default": {
"type": "oauth", "type": "oauth",
"provider": "anthropic", "provider": "anthropic",
"access": "sk-ant-oat01-..." "access": "<access-token-value>"
} }
} }
} }
@ -75,7 +75,7 @@ ANTHROPIC_OAUTH_TOKEN="sk-ant-oat01-..."
"anthropic:default": { "anthropic:default": {
"type": "oauth", "type": "oauth",
"provider": "anthropic", "provider": "anthropic",
"key": "sk-ant-oat01-..." "key": "<access-token-value>"
} }
} }
} }
@ -86,7 +86,7 @@ ANTHROPIC_OAUTH_TOKEN="sk-ant-oat01-..."
## File locations ## File locations
| File | Host Path | Container Path | | File | Host Path | Container Path |
|------|-----------|---------------| | ---------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| Claude CLI creds | `/root/.openclaw/workspaces/workspace-claude-proxy/config/.claude/.credentials.json` | `/root/.claude/.credentials.json` (claude-proxy) | | 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) | | 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 | | .env | `/root/openclaw/.env` | loaded as env vars at container creation |

View File

@ -4,8 +4,8 @@
Claude Max subscriptions use OAuth tokens for API authentication. Claude Max subscriptions use OAuth tokens for API authentication.
- **Access token** (`sk-ant-oat01-...`): Used for API requests, expires in ~8 hours - **Access token** (`<access-token-value>`): Used for API requests, expires in ~8 hours
- **Refresh token** (`sk-ant-ort01-...`): Used to get new access tokens, long-lived - **Refresh token** (`<refresh-token-value>`): Used to get new access tokens, long-lived
- **Token endpoint**: `POST https://console.anthropic.com/v1/oauth/token` - **Token endpoint**: `POST https://console.anthropic.com/v1/oauth/token`
- **Client ID**: `9d1c250a-e61b-44d9-88ed-5944d1962f5e` (Claude Code public OAuth client) - **Client ID**: `9d1c250a-e61b-44d9-88ed-5944d1962f5e` (Claude Code public OAuth client)
@ -26,19 +26,19 @@ The relevant function in Claude Code's minified source (`cli.js`):
// Simplified from minified source // Simplified from minified source
async function refreshToken(refreshToken, scopes) { async function refreshToken(refreshToken, scopes) {
const params = { const params = {
grant_type: "refresh_token", grant_type: 'refresh_token',
refresh_token: refreshToken, refresh_token: refreshToken,
client_id: CLIENT_ID, client_id: CLIENT_ID,
scope: scopes.join(" ") scope: scopes.join(' '),
}; };
const response = await axios.post(TOKEN_URL, params, { const response = await axios.post(TOKEN_URL, params, {
headers: { "Content-Type": "application/json" } headers: { 'Content-Type': 'application/json' },
}); });
return { return {
accessToken: response.data.access_token, accessToken: response.data.access_token,
refreshToken: response.data.refresh_token || refreshToken, refreshToken: response.data.refresh_token || refreshToken,
expiresAt: Date.now() + response.data.expires_in * 1000, expiresAt: Date.now() + response.data.expires_in * 1000,
scopes: response.data.scope.split(" ") 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 - We watch the **directory** not the file, because atomic renames create a new inode
When the file changes: When the file changes:
1. Read `accessToken`, `refreshToken`, `expiresAt` from Claude CLI format 1. Read `accessToken`, `refreshToken`, `expiresAt` from Claude CLI format
2. Map fields: `accessToken` -> `access`, `refreshToken` -> `refresh`, `expiresAt` -> `expires` 2. Map fields: `accessToken` -> `access`, `refreshToken` -> `refresh`, `expiresAt` -> `expires`
3. Write to `oauth.json` (for gateway's `mergeOAuthFileIntoStore()`) 3. Write to `oauth.json` (for gateway's `mergeOAuthFileIntoStore()`)
@ -85,7 +86,7 @@ This is less responsive (up to 6-hour delay) but works without inotify.
## Field Mapping ## Field Mapping
| Claude CLI (`.credentials.json`) | OpenClaw (`oauth.json`) | | Claude CLI (`.credentials.json`) | OpenClaw (`oauth.json`) |
|----------------------------------|------------------------| | -------------------------------- | ----------------------- |
| `claudeAiOauth.accessToken` | `anthropic.access` | | `claudeAiOauth.accessToken` | `anthropic.access` |
| `claudeAiOauth.refreshToken` | `anthropic.refresh` | | `claudeAiOauth.refreshToken` | `anthropic.refresh` |
| `claudeAiOauth.expiresAt` | `anthropic.expires` | | `claudeAiOauth.expiresAt` | `anthropic.expires` |

View File

@ -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`. 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: 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 https://api.anthropic.com/v1/v1/messages -> 404 Not Found
``` ```
@ -21,10 +22,7 @@ In `openclaw.json`, under `agents.defaults.model`:
"defaults": { "defaults": {
"model": { "model": {
"primary": "anthropic/claude-opus-4-6", "primary": "anthropic/claude-opus-4-6",
"fallbacks": [ "fallbacks": ["anthropic/claude-sonnet-4-6", "google/gemini-3.1-pro-preview"]
"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`): In your OpenClaw `.env` file (e.g., `/root/openclaw/.env`):
``` ```
ANTHROPIC_OAUTH_TOKEN="sk-ant-oat01-YOUR_TOKEN_HERE" ANTHROPIC_OAUTH_TOKEN="<YOUR-ACCESS-TOKEN>"
``` ```
This is the fallback auth method. The gateway reads it as a container environment variable. 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": { "anthropic:default": {
"type": "oauth", "type": "oauth",
"provider": "anthropic", "provider": "anthropic",
"access": "sk-ant-oat01-YOUR_TOKEN_HERE" "access": "<YOUR-ACCESS-TOKEN>"
} }
}, },
"lastGood": { "lastGood": {
@ -92,8 +90,8 @@ At `/root/.openclaw/credentials/oauth.json` (maps to `/home/node/.openclaw/crede
```json ```json
{ {
"anthropic": { "anthropic": {
"access": "sk-ant-oat01-YOUR_TOKEN_HERE", "access": "<YOUR-ACCESS-TOKEN>",
"refresh": "sk-ant-ort01-YOUR_REFRESH_TOKEN", "refresh": "<YOUR-REFRESH-TOKEN>",
"expires": 1772120060006, "expires": 1772120060006,
"scopes": ["user:inference", "user:mcp_servers", "user:profile", "user:sessions:claude_code"], "scopes": ["user:inference", "user:mcp_servers", "user:profile", "user:sessions:claude_code"],
"subscriptionType": "max", "subscriptionType": "max",
@ -105,6 +103,7 @@ At `/root/.openclaw/credentials/oauth.json` (maps to `/home/node/.openclaw/crede
## Available Built-in Models ## Available Built-in Models
When using the built-in Anthropic provider: When using the built-in Anthropic provider:
- `anthropic/claude-opus-4-6` - `anthropic/claude-opus-4-6`
- `anthropic/claude-sonnet-4-6` - `anthropic/claude-sonnet-4-6`
- Other models listed in the Anthropic API - 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`) 2. For `type: "oauth"`, it requires the `access` field (not `key`)
3. If no valid profile: falls back to `ANTHROPIC_OAUTH_TOKEN` env var 3. If no valid profile: falls back to `ANTHROPIC_OAUTH_TOKEN` env var
4. On startup, `mergeOAuthFileIntoStore()` reads `oauth.json` and merges credentials 4. On startup, `mergeOAuthFileIntoStore()` reads `oauth.json` and merges credentials
5. `isOAuthToken()` detects the `sk-ant-oat` prefix 5. `isOAuthToken()` detects the `<token-prefix>` prefix
6. Uses Bearer auth + Claude Code identity headers to call `api.anthropic.com` 6. Uses Bearer auth + Claude Code identity headers to call `api.anthropic.com`
## OAuth Token Lifecycle ## OAuth Token Lifecycle

View File

@ -5,12 +5,14 @@
The most common error. The OAuth token has a ~8 hour lifetime. The most common error. The OAuth token has a ~8 hour lifetime.
**Check:** **Check:**
1. Is the sync service running? `systemctl status sync-oauth-token.service` 1. Is the sync service running? `systemctl status sync-oauth-token.service`
2. Is inotifywait watching? `pgrep -af inotifywait` 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` 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` 4. Check service logs: `journalctl -u sync-oauth-token.service -f`
**Fix:** **Fix:**
- If service stopped: `systemctl restart sync-oauth-token.service` - If service stopped: `systemctl restart sync-oauth-token.service`
- If token expired everywhere: run `./scripts/refresh-claude-token.sh` manually - If token expired everywhere: run `./scripts/refresh-claude-token.sh` manually
- Nuclear option: `claude login` inside the Claude CLI container, then restart sync service - 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. `docker compose restart` only sends SIGTERM and restarts the container process. The container keeps its original environment variables from creation time.
**Always use:** **Always use:**
```bash ```bash
cd /root/openclaw cd /root/openclaw
docker compose down openclaw-gateway docker compose down openclaw-gateway
@ -43,7 +46,7 @@ OpenClaw's `isValidProfile()` for `type: "oauth"` checks for `cred.access`, not
"anthropic:default": { "anthropic:default": {
"type": "oauth", "type": "oauth",
"provider": "anthropic", "provider": "anthropic",
"key": "sk-ant-oat01-..." <-- WRONG "key": "<access-token-value>" <-- WRONG
} }
} }
``` ```
@ -53,12 +56,13 @@ The profile is silently skipped and falls through to the env var.
**Fix:** Run `./scripts/fix-auth-profiles.sh` **Fix:** Run `./scripts/fix-auth-profiles.sh`
The correct format is: The correct format is:
```json ```json
{ {
"anthropic:default": { "anthropic:default": {
"type": "oauth", "type": "oauth",
"provider": "anthropic", "provider": "anthropic",
"access": "sk-ant-oat01-..." <-- CORRECT "access": "<access-token-value>" <-- CORRECT
} }
} }
``` ```
@ -70,6 +74,7 @@ The correct format is:
This happens when you add `anthropic` to `models.providers` in `openclaw.json`. This happens when you add `anthropic` to `models.providers` in `openclaw.json`.
**Do NOT do this:** **Do NOT do this:**
```json ```json
"models": { "models": {
"providers": { "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). Auth profiles enter a cooldown period after repeated failures (e.g., expired tokens, wrong model names).
**Fix:** **Fix:**
```bash ```bash
./scripts/fix-auth-profiles.sh ./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. The watched file or directory doesn't exist yet.
**Check:** **Check:**
- Does the Claude CLI container exist? `docker ps | grep claude` - 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/` - 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. - Has Claude CLI been authenticated? You may need to run `claude login` inside the container first.

31
package-lock.json generated Normal file
View File

@ -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"
}
}
}
}

9
package.json Normal file
View File

@ -0,0 +1,9 @@
{
"name": "openclaw-oauth-sync",
"version": "1.0.0",
"private": true,
"description": "OpenClaw OAuth Token Sync",
"devDependencies": {
"prettier": "^3.2.0"
}
}

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# fix-auth-profiles.sh — Fix broken Anthropic auth profiles in all OpenClaw agents # 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:"<access-token-value>"}
# but OpenClaw's isValidProfile() for type:"oauth" checks for "access" field, not "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 # This causes the profile to be skipped and fall through to env var fallback
# #

View File

@ -69,7 +69,7 @@ echo " ║ OAuth Fix for OpenClaw + Claude Max ║"
echo " ║ Automatic Anthropic Token Refresh ║" echo " ║ Automatic Anthropic Token Refresh ║"
echo " ║ v${VERSION}" echo " ║ v${VERSION}"
echo " ║ ║" echo " ║ ║"
echo " ║ Created by ROOH — www.rooh.red ║" echo " ║ Created by ROOH — <project-url> ║"
echo " ╚══════════════════════════════════════════════════════╝" echo " ╚══════════════════════════════════════════════════════╝"
echo -e "${NC}" echo -e "${NC}"
echo -e "${DIM} Keeps your Anthropic OAuth tokens fresh by syncing" echo -e "${DIM} Keeps your Anthropic OAuth tokens fresh by syncing"
@ -1065,5 +1065,5 @@ fi
echo " ./scripts/verify.sh # Health check" echo " ./scripts/verify.sh # Health check"
echo " ./setup.sh --uninstall # Remove everything" echo " ./setup.sh --uninstall # Remove everything"
echo "" echo ""
echo -e " ${DIM}Created by ROOH — www.rooh.red${NC}" echo -e " ${DIM}Created by ROOH — <project-url>${NC}"
echo "" echo ""

View File

@ -18,7 +18,10 @@ if (!token) {
try { try {
const data = JSON.parse(fs.readFileSync(path, 'utf8')); const data = JSON.parse(fs.readFileSync(path, 'utf8'));
token = data.anthropic?.access; token = data.anthropic?.access;
if (token) { console.log(`Token from: ${path}`); break; } if (token) {
console.log(`Token from: ${path}`);
break;
}
} catch {} } catch {}
} }
} }
@ -28,7 +31,10 @@ if (!token) {
try { try {
const env = fs.readFileSync('/root/openclaw/.env', 'utf8'); const env = fs.readFileSync('/root/openclaw/.env', 'utf8');
const match = env.match(/ANTHROPIC_OAUTH_TOKEN="?([^"\n]+)/); 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 {} } catch {}
} }
@ -70,14 +76,16 @@ if (isOAuth) {
console.log('Sending test request to api.anthropic.com...'); console.log('Sending test request to api.anthropic.com...');
console.log(''); console.log('');
const req = https.request({ const req = https.request(
{
hostname: 'api.anthropic.com', hostname: 'api.anthropic.com',
path: '/v1/messages', path: '/v1/messages',
method: 'POST', method: 'POST',
headers, headers,
}, (res) => { },
(res) => {
let data = ''; let data = '';
res.on('data', (chunk) => data += chunk); res.on('data', (chunk) => (data += chunk));
res.on('end', () => { res.on('end', () => {
console.log(`Status: ${res.statusCode}`); console.log(`Status: ${res.statusCode}`);
if (res.statusCode === 200) { if (res.statusCode === 200) {
@ -98,7 +106,8 @@ const req = https.request({
process.exit(1); process.exit(1);
} }
}); });
}); },
);
req.on('error', (err) => { req.on('error', (err) => {
console.error('Connection error:', err.message); console.error('Connection error:', err.message);

69
tools/secret-scan.sh Executable file
View File

@ -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