feat: account safety, quota monitoring, and stability fixes (#530)

* fix(cliproxy): migrate deprecated gemini-claude-* model names to upstream claude-* names (#515)

* fix(cliproxy): migrate deprecated gemini-claude-* model names to upstream claude-* names

CLIProxyAPI registry no longer recognizes the gemini-claude-* prefix convention.
Model names in catalog, base config, and user settings are migrated to upstream
claude-* names. Auto-migration in env-builder rewrites existing user settings on
load and persists the change.

Closes #513

* fix: address code review feedback — sync UI layer and add migration tests

- Sync UI isNativeGeminiModel() with backend (remove gemini-claude- exclusion)
- Update UI model catalog agy entries from gemini-claude-* to claude-*
- Update CI/CD workflow and code-reviewer default model names
- Add unit tests for migrateDeprecatedModelNames() logic

* fix(hooks): isolate image type check before error-prone processing (#514)

* fix(hooks): isolate image type check before error-prone processing

Restructure processHook() into two phases so non-image Read calls
never see hook error messages. Phase 1 defensively checks tool name
and file extension, exiting 0 silently on any failure. Phase 2 only
runs for confirmed image/PDF files where errors are relevant.

Closes #511

* fix(hooks): sync image analyzer hook file on every profile launch

Add installImageAnalyzerHook() call to cliproxy executor, matching
the existing installWebSearchHook() pattern. This ensures the .cjs
file in ~/.ccs/hooks/ gets refreshed from the npm package on every
launch, so users receive hook updates after npm update.

* chore(release): 7.41.0-dev.1 [skip ci]

* fix(cliproxy): add fork:true for Claude model aliases in config generator (#523)

Config generator now outputs fork:true for Claude model alias entries,
ensuring both upstream (claude-*) and aliased (gemini-claude-*) model
names appear in /v1/models listings. Also preserves fork flag when
parsing user-added aliases during config regeneration.

Bumps config version to v7 to trigger regeneration on next ccs doctor.

Closes #522

* chore(release): 7.41.0-dev.2 [skip ci]

* feat(cliproxy): add account safety guards to prevent Google account bans (#516)

* feat(cliproxy): add account safety guards to prevent Google account bans

Implements cross-provider isolation to prevent Google from flagging
concurrent OAuth usage across different client IDs (ref: #509, #512).

Three pillars:
1. Auto-pause enforcement at session launch — conflicting accounts in
   other Google OAuth providers are paused so CLIProxyAPI can't use them,
   restored on session exit with crash recovery via auto-paused.json
2. Ban/disable detection — error responses matching Google ban patterns
   auto-pause the affected account to prevent further damage
3. Cross-provider conflict warnings during OAuth registration

Key design decisions:
- PID-based session tracking for crash recovery (dead PID = restore)
- Timestamp comparison prevents restoring ban-paused accounts on exit
- Schema validation on auto-paused.json prevents corrupted state
- Falls back to warn-only when another session is managing isolation

* fix(cliproxy): address code review feedback (attempt 1/5)

- Re-read auto-paused.json before write in enforceProviderIsolation to
  reduce concurrent write race window
- Use actual email from registry for display instead of raw accountId
- Export maskEmail for testability
- Add 27 unit tests covering ban detection, email masking,
  cross-provider duplicate detection, enforcement lifecycle,
  crash recovery, and timestamp-guarded restore

* fix(cliproxy): address remaining review feedback (attempt 2/5)

- Add handleBanDetection test verifying account pause on ban error
- Add warnCrossProviderDuplicates tests (true/false/non-Google)
- Document PID reuse limitation in isPidAlive JSDoc comment

* chore(release): 7.41.0-dev.3 [skip ci]

* feat(cliproxy): runtime quota monitoring during active sessions (#529)

* feat(cliproxy): add runtime quota monitoring during active sessions

Adds adaptive background quota polling to detect and respond to quota
exhaustion during active CLIProxy sessions. Prevents rate-limit-driven
account bans by auto-cooling exhausted accounts and switching defaults.

- Adaptive polling: 300s normal, 60s at 20% threshold, stops at 0%
- Stderr warnings at 20%, boxed exhaustion alerts at 0%
- Cooldown + default switch on exhaustion (existing patterns)
- Configurable via quota_management.runtime_monitor in config.yaml
- Timer.unref() prevents blocking process exit
- monitorStopped guard for in-flight poll safety

Closes #524

* fix: address code review feedback (attempt 1/5)

- M1: Round quotaPercent display with Math.round() to avoid ugly floats
- M2: Rename exhaust_threshold -> exhaustion_threshold for consistency
  with existing auto.exhaustion_threshold config field
- M3: Replace async not.toThrow() with direct await assertion pattern

* fix: address code review feedback (attempt 2/5)

- Remove .claude/agent-memory/ from tracking and add to .gitignore
- Unify cooldown_minutes default to 5 (was 10 in runtime_monitor, 5 in auto)
- Add threshold validation in startQuotaMonitor (warn > exhaustion)
- Document intentional post-switch monitoring gap in code comment

* chore(release): 7.41.0-dev.4 [skip ci]

* fix(cliproxy): mask email in ban detection and fix JSDoc default

- Use maskEmail() in handleBanDetection output for consistency
- Fix cooldown_minutes JSDoc: default is 5, not 10

* chore(release): 7.41.0-dev.5 [skip ci]

* fix(cliproxy): address all review feedback (Low + informational)

- Add sync constraint comment on process.exit handler (executor)
- Add TOCTOU race acceptability comment (account-safety)
- Mask email in handleQuotaExhaustion reason string
- Use realistic exhaustion_threshold (5) in test configs

* chore(release): 7.41.0-dev.6 [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Kai (Tam Nhu) Tranandgithub-actions[bot] authored and GitHub committed 2026-02-12 00:48:29 +07:00
1 parent 59e3e0286e
commit 051805074e
32 files changed
+2384 -110

No files matched your search

+26 -19
View File
@@ -761,14 +761,16 @@ process.stdin.on('error', () => {
/**
* Main hook processing logic
*
* Two-phase design: Phase 1 filters non-image Read calls silently (exit 0).
* Phase 2 only runs for confirmed image/PDF files, so error messages are
* always relevant and never confuse users reading code or text files.
*/
async function processHook() {
// Phase 1: Fast bail-out for non-image files
// Any failure here → pass through silently to native Read
let filePath;
try {
// Skip for native accounts or explicit disable
if (shouldSkipHook()) {
process.exit(0);
}
const data = JSON.parse(input);
// Only handle Read tool
@@ -776,23 +778,35 @@ async function processHook() {
process.exit(0);
}
const filePath = data.tool_input?.file_path || '';
filePath = data.tool_input?.file_path || '';
if (!filePath) {
process.exit(0);
}
// Check file extension BEFORE any other processing — this is the key gate
// that ensures non-image Read calls never see hook errors
if (!isAnalyzableFile(filePath)) {
process.exit(0);
}
} catch {
// stdin parse failure or unexpected error → pass through silently
process.exit(0);
}
// Phase 2: Image/PDF file processing — errors here are relevant to the user
try {
// Skip for native accounts or explicit disable
if (shouldSkipHook()) {
process.exit(0);
}
// Check if file exists
if (!fs.existsSync(filePath)) {
// Let native Read handle the error
process.exit(0);
}
// Check if file is analyzable
if (!isAnalyzableFile(filePath)) {
process.exit(0);
}
// Check file size
const stats = fs.statSync(filePath);
if (stats.size >= MAX_FILE_SIZE_BYTES) {
@@ -843,14 +857,7 @@ async function processHook() {
console.error('[CCS Hook] Error:', err.message);
}
// Try to extract file path from parsed input
let filePath = 'unknown file';
try {
const data = JSON.parse(input);
filePath = data.tool_input?.file_path || 'unknown file';
} catch {
// Ignore parse errors
}
// filePath is guaranteed set by Phase 1 — only image files reach here
// Categorize error by message pattern
const errMsg = err.message || '';