Remove CRS, make CLIProxyAPI the only multi-account path (rebased, supersedes #819) - #830
Conversation
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Important Review skippedToo many files! This PR contains 103 files, which is 3 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (103)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
CRS is being removed. This commit takes out the files that only exist to drive a claude-relay-service pool: the priority daemon, the 429 cooldown, the token feed, the 401 refresher, the health watch, the bedrock guard, the egress failover, the whole crsproxy-reauth Python tree, the three install-crs-* scripts, the five crs-* systemd units, the crs-* launchd plists, and the host-specific tuning runbook. Three modules in that group were never CRS-specific, only CRS-named, and other rotation code imports them. They keep their behaviour under neutral names: crs-pool-config.mjs is now rotation-config.mjs, crs-refresh-lock.mjs is refresh-lock.mjs, crs-reconciler-state.mjs is reconciler-state.mjs. rotation-config.mjs loses the pool-name mapping, the relay base URL and the relay admin password lookup, and reads its tunables from a `rotation` block, falling back to a legacy `crs` block so an existing config still works. refresh-tokens.mjs no longer shells out to sync-crs-account.mjs, which was never in this repo, and its state files migrate by reading the old name when the new one is absent. magic-link-autoloop.mjs loses the dead candidate picker that was the only remaining reason it imported pool config. production-recovery.test.mjs drove three of the deleted daemons; what is left of it is the Retry-After parsing that every backoff path still uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The relay backend is gone, so nothing may point a Claude session at one. route-state.mjs keeps its two modes but renames the live one from crs-oauth to oauth; crs-oauth and proxy-oauth are read as aliases so an existing ~/.claude/claude-routing-state.json still resolves. Both modes now write the same settings: no Bedrock keys, no base-URL override, no injected API key. It also drops the hardcoded operator host IP that sat in the base-URL regex. New exports detect and scrub a leftover relay pair, and settingsRouteSnapshot reports one as legacyRelay so `claude-stack doctor` can name it. bg-respawn.mjs loses the allowlist that routed background sessions through the relay, along with the health gate, the relay-down marker and the session-settings overlay that came with it. What replaces it strips any inherited relay base or cr_ token, drops a --settings relay overlay out of the persisted respawn flags, and respawns direct. That is the migration for a machine that was on the relay yesterday: nothing breaks, the routing just stops being re-applied. session-router.mjs replaces enforceDirectOrCrsPairing with enforceDirectAuth. claude-settings-mode.mjs no longer preserves a relay pair across a mode flip. launch-claude.mjs launches on Claude Code's own credential, and the harness env loader it used to validate cr_ keys with is deleted. provision-coordination.mjs no longer generates a systemd unit for a token feed script that no longer exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ops-accounts loses `crs` and `crs-tick` as working verbs. They stay in the shell dispatch table only so they answer with where multi-account rotation lives now (CLIProxyAPI) and exit 2, rather than a bare "unknown command". The Grok thin-hop probe and the relay migration hint go with them. ops-bg no longer injects a --settings overlay into every background session it spawns. The claude-account-rotation systemd unit no longer orders itself behind a relay compose unit that will never exist. The statusline suppressed a false Bedrock badge by matching a relay base URL and a cr_ token. It now matches any loopback base URL, so a machine on CLIProxyAPI gets the same treatment and one still carrying relay env keeps reading correctly instead of suddenly showing Bedrock. ops-cron-pocket-watcher.py and ops-memory-extractor.sh looked up a CRS_KEY keychain item and defaulted to a relay port. Both now use an explicitly configured proxy (base URL and token together, or neither) and otherwise fall through to the OAuth paths that were already there. ops-accounts-backend resolves a `cliproxy` backend; the retired relay values resolve to `local` so an install that still exports one keeps working. Provider adapters drop the vestigial per-account `crs` field, and the Cursor and Factory quota snapshots move out of a crs-keys directory. CI: run-all.sh drops the crsproxy-reauth Python suite along with the scripts it ran, and picks up test-claude-invoke.sh, which existed but was wired into nothing — it is the only cover for the credit-pool gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ops-accounts and ops-rotate skills advertised `crs` and `crs-tick` in their frontmatter and router tables. Those verbs no longer work, so they are gone from the description, the argument hint and the command tables, and the rows that described relay pooling now point at CLIProxyAPI and /ops:ops-fleet. The three docs under docs/ops were written as a migration story from the relay to a future gateway. That migration is over: OPS-ACCOUNTS-VISION.md now states what pooling looks like and why the relay was removed, OPS-ACCOUNTS-GATEWAY.md describes the local compat shim as the smaller separate thing it is, and HOST-VS-PLUGIN-ROTATE-GAP.md stops listing units and files that no longer exist. The rotator README replaces its CRS pool section with CLIProxyAPI setup and the one command that clears leftover relay credentials from an upgraded machine. CAPTCHA-CASCADE.md follows the env-var renames. CHANGELOG gets an entry covering what was deleted, what was renamed, and every compatibility shim, so an operator can tell what will keep working. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The survey opened with "nothing in this plan has been executed", which stopped being true. It also proposed re-offering CRS as an installable companion; the owner decided to remove it outright instead, so section 8 describes a package that does not exist. The header now says both. The analysis below it is left as the record of the pre-removal tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: auroracapital <201698397+auroracapital@users.noreply.github.com>
d321cac to
1a468ed
Compare
| case "${ANTHROPIC_BASE_URL:-}" in | ||
| http://127.0.0.1:*|http://localhost:*|http://[::1]:*) _is_local_proxy=1 ;; | ||
| esac |
There was a problem hiding this comment.
http://[::1]:* doesn't do what it looks like — in a case pattern the brackets are a glob bracket expression, so this matches http:// + one char from {:,1} + :. It never matches http://[::1]:8317, and it does match nonsense like http://1:8317:
$ case "http://[::1]:8317" in http://[::1]:*) echo M ;; *) echo N ;; esac
N
$ case "http://1:8317" in http://[::1]:*) echo M ;; *) echo N ;; esac
M
Needs quoting so the brackets are literal:
| case "${ANTHROPIC_BASE_URL:-}" in | |
| http://127.0.0.1:*|http://localhost:*|http://[::1]:*) _is_local_proxy=1 ;; | |
| esac | |
| case "${ANTHROPIC_BASE_URL:-}" in | |
| http://127.0.0.1:*|http://localhost:*|'http://[::1]:'*) _is_local_proxy=1 ;; | |
| esac |
Separately, this is a lot broader than the check it replaces (which required a cr_ token and a known relay port). Any loopback base URL now forces sub_type="max" and overrides an explicit CLAUDE_CODE_USE_BEDROCK=1 — and the result gets written to $billing_cache on the line below. So a genuinely-Bedrock session that happens to have a loopback proxy configured loses its billing badge and caches the wrong value. Worth keeping the token/port shape in the condition rather than "any loopback".
| const SECRET_FILES = [ | ||
| process.env.CLAUDE_ROTATOR_SECRETS_FILE, | ||
| join(homedir(), '.config', 'crs-sync', 'rotator-secrets.env'), | ||
| join(homedir(), '.config', 'claude-rotation', 'rotator-secrets.env'), |
There was a problem hiding this comment.
This one moves silently. Anyone with secrets at ~/.config/crs-sync/rotator-secrets.env today gets them dropped on upgrade — nothing creates or migrates the new path, and the loader just falls through to the next candidate with no warning. That's the 2captcha/BrightData credential source, so the failure surfaces much later as a captcha cascade that can't solve.
Cheap to keep both:
| join(homedir(), '.config', 'claude-rotation', 'rotator-secrets.env'), | |
| join(homedir(), '.config', 'claude-rotation', 'rotator-secrets.env'), | |
| // Legacy location — read so an existing install keeps its secrets after upgrade. | |
| join(homedir(), '.config', 'crs-sync', 'rotator-secrets.env'), |
| // Legacy env name kept last so an older install still resolves its config. | ||
| process.env.CRS_CONFIG, |
There was a problem hiding this comment.
The comment doesn't match the effect. resolveConfigPath() returns the first existing candidate, so moving CRS_CONFIG behind the five on-disk defaults means it only ever wins on a machine that has no config in any default location — i.e. effectively never for the installs this is meant to protect. An operator who points CRS_CONFIG at a custom file today silently starts reading ~/.claude/scripts/account-rotation/config.json instead.
An explicit env override should still beat implicit disk defaults; just keep it next to the current env name:
| // Legacy env name kept last so an older install still resolves its config. | |
| process.env.CRS_CONFIG, | |
| process.env.CLAUDE_ROTATOR_CONFIG, | |
| // Legacy env name, still honoured ahead of the on-disk defaults. | |
| process.env.CRS_CONFIG, |
(and drop the trailing entry)
| /** True when local EFG SOCKS (CLAUDE_ROT_EFG_SOCKS_*) accepts connections. */ | ||
| export async function residentialEgressHealthy() { | ||
| const host = process.env.CRS_EFG_SOCKS_HOST || '127.0.0.1'; | ||
| const port = Number(process.env.CRS_EFG_SOCKS_PORT || 1089); | ||
| const host = process.env.CLAUDE_ROT_EFG_SOCKS_HOST || '127.0.0.1'; | ||
| const port = Number(process.env.CLAUDE_ROT_EFG_SOCKS_PORT || 1089); | ||
| return await new Promise((resolve) => { |
There was a problem hiding this comment.
All the CRS_* names in this file get renamed with no fallback, which is inconsistent with how refresh-lock.mjs handled the same problem in this PR:
const envValue = (name) => process.env[`CLAUDE_REFRESH_${name}`] ?? process.env[`CRS_REFRESH_${name}`];The renames here are mostly tuning knobs, so the failure mode is quiet rather than loud: an install exporting CRS_CAPTCHA_SOLVER_MODE=off silently flips back to fallback and starts paying for solves, and CRS_EFG_SOCKS_PORT reverts to 1089 so solver tokens stop matching the residential browser IP. Same helper shape would cover the whole file:
const captchaEnv = (name) => process.env[`CLAUDE_ROT_${name}`] ?? process.env[`CRS_${name}`];reauth-env.mjs has the mirror of this — resolveReauthDisplay drops CRS_REAUTH_DISPLAY, and resolveReauthTimeoutMs keeps CRS_MAGIC_LINK_ROTATE_TIMEOUT_MS but quietly drops its CRS_MAGIC_LOOP_ROTATE_TIMEOUT_MS sibling.
| // A relay reachable somewhere else (a tailnet address, say) can be named in | ||
| // OPS_RELAY_HOSTS as a comma-separated host:port list; nothing site-specific | ||
| // is hardcoded here. | ||
| const LEGACY_RELAY_PORT_RE = /:(3000|3002|3005|8091|18091)(\/|$)/; |
There was a problem hiding this comment.
This widened from loopback-only to any-host. main required 127.0.0.1:<port> or a :<port>/api suffix; this matches those ports on any hostname. Combined with scrubLegacyRelayEnv() now running unconditionally on every spawn in session-router.mjs and bg-respawn.mjs, a CLIProxyAPI instance published on one of these ports (3000 is a very ordinary docker mapping) gets its ANTHROPIC_BASE_URL deleted out from under it — and that's the path this PR makes the only supported one.
The comment above says a relay reachable elsewhere should be named in OPS_RELAY_HOSTS, which is exactly the escape hatch that makes anchoring safe here:
| const LEGACY_RELAY_PORT_RE = /:(3000|3002|3005|8091|18091)(\/|$)/; | |
| const LEGACY_RELAY_PORT_RE = /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\]):(3000|3002|3005|8091|18091)(\/|$)/; |
|
|
||
| export async function listAccounts(ctx) { | ||
| const snapPath = join(ctx.home, '.claude/crs-keys/quota/cursor-latest.json'); | ||
| const snapPath = join(ctx.home, '.claude/quota-snapshots/cursor-latest.json'); |
There was a problem hiding this comment.
Nothing in the repo writes either path — rg 'crs-keys' comes back empty after this PR — so the snapshot producer is external to the plugin. Moving the read location without a fallback means existing snapshots go invisible and utilization() quietly returns { windows: [], source: 'unavailable' } rather than erroring, so it looks like a quota-API problem instead of a path change.
Since it's just a existsSync pick, worth reading either:
const snapPath = [
join(ctx.home, '.claude/quota-snapshots/cursor-latest.json'),
join(ctx.home, '.claude/crs-keys/quota/cursor-latest.json'),
].find(existsSync);Same applies to the two factory-droid.json sites in providers/factory.mjs.
Supersedes #819. Same removal, rebased onto current
main(v3.4.2), plus oneregression fix and the full-suite verification #819 never completed on its final
state.
Force-pushing #819's branch was not available, so this is a fresh PR off
crs-rebase. Please close #819 as superseded.Why this replaces #819
A ReferenceError the removal introduced.
refresh-tokens.mjshad twoexecSynccall sites. The removal deleted one and thechild_processimportalong with it, but the second survived at line 85, inside
readKeychain():Every
readKeychain()call would have thrownReferenceError: execSync is not defined— that is the keychain read at the centre of token refresh. Fixed byrestoring the import; no call sites changed.
Rebased onto current main. #819 was 7 commits behind and its
mergeablestate was unknown. The rebase applied cleanly and picks up the
rm -rfguardfix from #826.
Verification
Full
run-all.shon the final rebased state:test-safety-hooks.sh— 49 passed, 0 failed. It was red onmain; Fix the rm -rf guard, which blocked nothing on stock macOS #826fixed it and this branch inherits that.
test-no-secrets.sh— 27 passed, 0 failed.test-staged-enrollment.sh, assertingtimed out waiting for replacement-lock fault stop.That failure is pre-existing and unrelated, verified rather than assumed: a
pristine
git archiveoforigin/mainextracted to a clean directory fails thesame suite with the identical assertion. Being investigated separately.
What the removal does
Deletes the
crs-*daemons,scripts/crsproxy-reauth/, threeinstall-crs-*.sh,five systemd units, three plists, the tuning runbook, and
claude-harness-env.mjs.crs-health-watch.mjswent too — it importedcrs-heal-relay.mjs, which hasnever existed in this repo.
account-rotation/is not removed wholesale. Only ~14 of its 106 files wereCRS;
rotate.mjs, the credit ledger,claude-p-as.mjs, seat state and thecaptcha cascade all stay. Three modules were renamed rather than deleted because
surviving code imports them (
crs-pool-config→rotation-config,crs-refresh-lock→refresh-lock,crs-reconciler-state→reconciler-state).The PreToolUse hook was the highest-risk edit:
bedrock-fallback-guard.mjsimports
route-state.mjsat module load, so breaking it would throw on everytool call in every session.
route-state.mjskeeps two modes with the live onerenamed
crs-oauth→oauth, and legacy modes, config blocks,CRS_*env namesand reconciler state filenames all still resolve, so an existing install migrates
rather than breaking.
CLIProxyAPI is now the only supported path for multi-account rotation and OAuth
seat management.
🤖 Generated with Claude Code