From b918783293381a13f0ba90c17c57743a748fa733 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 26 Jul 2026 09:34:48 -0400 Subject: [PATCH 1/3] docs(product): refresh product and release contracts --- docs/project-overview-pdr.md | 508 +++++++++++++---------------------- docs/release-process.md | 169 ++++++------ 2 files changed, 284 insertions(+), 393 deletions(-) diff --git a/docs/project-overview-pdr.md b/docs/project-overview-pdr.md index a239fae2..b8a7dafa 100644 --- a/docs/project-overview-pdr.md +++ b/docs/project-overview-pdr.md @@ -1,374 +1,254 @@ -# CCS Product Development Requirements (PDR) - -Last Updated: 2026-05-07 +# CCS Product Development Requirements ## Product Overview -**Product Name**: CCS (Claude Codex Switch) +**Product name:** CCS (Claude Codex Switch) -**Tagline**: The multi-provider profile and runtime manager for Claude Code and compatible CLIs +**Purpose:** Provide one profile and runtime-management surface for Claude Code, +Codex CLI, Factory Droid, CLIProxy-backed OAuth providers, and compatible API +profiles. -**Description**: Multi-provider CLI/runtime manager enabling seamless switching between multiple Claude accounts, OAuth/API providers, and alternate targets such as Claude Code, Factory Droid, and Codex CLI. Includes a React-based dashboard for configuration management, plus support for local and remote CLIProxyAPI instances, hybrid quota management, and official Claude channel runtime setup for Telegram, Discord, and iMessage. +CCS includes: -**Current Version**: v7.34.x+ (First-class ImageAnalysis MCP tooling, WebSearch MCP, performance improvements) +- a TypeScript CLI and local server; +- a React dashboard for configuration, account, health, and usage workflows; +- isolated account contexts and per-profile settings; +- local or remote CLIProxy routing; +- optional managed tools such as WebSearch and image analysis; and +- an integrated Docker image containing CCS, CLIProxy, and the dashboard. ---- +Release versions and completed-release inventories belong in +[`CHANGELOG.md`](../CHANGELOG.md), not this evergreen requirements document. -## Problem Statement +## Problem -Developers using Claude Code face these challenges: +Developers need to switch between accounts, providers, and compatible CLIs +without repeatedly editing credentials or allowing one session's configuration +to leak into another. They also need a visible, reversible way to manage local +proxy state and diagnose provider readiness. -1. **Single Account Limitation**: Cannot run multiple Claude subscriptions simultaneously -2. **Provider Lock-in**: Stuck with Anthropic's API, cannot use alternatives -3. **No Concurrent Sessions**: Cannot work on different projects with different accounts -4. **Complex Configuration**: Manual env var and config file management -5. **No Usage Analytics**: Lack visibility into token usage and costs across providers +## Product Principles ---- +1. **CLI first:** Core configuration and launch behavior remains scriptable. +2. **Explicit state:** Users can inspect which profile, provider, target, and + proxy mode CCS selected. +3. **Isolation:** Account sessions use separate configuration roots where the + target supports them. +4. **Compatibility:** CCS adapts provider credentials to a target without + redefining the provider or target protocol. +5. **Reversibility:** Persistent writes are explicit and recoverable. +6. **Local ownership:** Credentials and profile state remain on infrastructure + selected by the user. -## Solution +## Users -CCS provides: - -1. **Multi-Account Claude**: Isolated instances via `CLAUDE_CONFIG_DIR` -2. **OAuth Providers**: Zero-config Gemini, Codex, xAI/Grok, Antigravity, Kiro, and other active OAuth integrations, with deprecated Copilot compatibility for existing setups -3. **AI Providers**: Dedicated CLIProxy dashboard for Gemini, Codex, Claude, Vertex, and OpenAI-compatible API-key families -4. **API Profiles**: GLM, Kimi, OpenRouter, any Anthropic-compatible API -5. **Visual Dashboard**: React SPA for configuration management -6. **Automatic WebSearch**: First-class local WebSearch tool with deterministic provider chain for third-party providers -7. **Automatic Image Analysis**: First-class local ImageAnalysis tool with direct provider routing for third-party profiles -8. **Usage Analytics**: Token tracking, cost analysis, model breakdown -9. **Official Claude Channels**: Runtime auto-enable plus dashboard token/config flow for Telegram, Discord, and macOS-only iMessage -10. **Routing Strategy Guidance**: First-class `round-robin` vs `fill-first` controls in CLI and dashboard, with explicit opt-in changes and no account-based guessing - ---- - -## Target Users - -| User Type | Use Case | Primary Features | -|-----------|----------|------------------| -| Individual Developer | Work/personal separation | Multi-account Claude | -| Agency/Contractor | Client account isolation | Profile switching | -| Cost-conscious Dev | GLM for bulk operations | API profiles, analytics | -| Enterprise | Custom LLM integration | OpenAI-compatible endpoints | -| Power User | Multiple providers | OpenRouter 300+ models | - ---- +| User | Primary need | +| --- | --- | +| Individual developer | Separate accounts, projects, and provider profiles | +| Consultant or agency | Isolate client contexts | +| API consumer | Reuse Anthropic-compatible and OpenAI-compatible providers | +| Power user | Manage OAuth accounts, routing, health, and quota state | +| Team operator | Run a shared remote CLIProxy or integrated Docker service | ## Functional Requirements -### FR-001: Profile Switching -- Switch between profiles with `ccs ` command -- Support default profile when no argument provided -- Pass through all Claude CLI arguments +### FR-001: Profile resolution and launch -### FR-002: Multi-Account Claude -- Create isolated Claude instances -- Maintain separate sessions, todolists, logs per account -- Share commands, skills, agents across accounts +- Launch the default profile with `ccs`. +- Launch named settings, account, and CLIProxy profiles. +- Pass target-specific arguments without losing CCS-owned routing constraints. +- Resolve provider and target aliases through canonical registries. -### FR-003: OAuth Provider Integration -- Support Gemini, Codex, xAI/Grok, Antigravity, Kiro, and deprecated Copilot compatibility OAuth flows -- Browser-based authentication with provider-specific Authorization Code, Device Code, or polling flows -- Token caching and refresh +### FR-002: Account isolation -### FR-004: API Profile Management -- Configure custom API endpoints -- Support Anthropic-compatible APIs -- Model mapping and configuration -- OpenRouter integration with 300+ models +- Maintain an account registry. +- Use isolated `CLAUDE_CONFIG_DIR` roots for Claude account profiles. +- Keep shared resources and instance-owned state distinguishable. +- Prevent one account's provider overrides from leaking into another launch. -### FR-004A: CLIProxy AI Provider Management -- Configure CLIProxy-managed Gemini, Codex, Claude, Vertex, and OpenAI-compatible API-key entries -- Keep provider authoring separate from CCS API Profile creation -- Support local config editing and remote CLIProxy management parity where available +### FR-003: Provider integration -### FR-005: Dashboard UI -- Visual profile management -- Real-time health monitoring -- Usage analytics with cost tracking -- Modular page architecture (settings, analytics, auth-monitor) +- Support API-key profiles and OAuth-backed CLIProxy providers. +- Support local and remote CLIProxy operation. +- Keep original and Plus CLIProxy backends explicit; do not silently substitute + one when a requested provider requires the other. +- Treat provider capability metadata as code-owned, not prose-owned. See + [`src/cliproxy/provider-capabilities.ts`](../src/cliproxy/provider-capabilities.ts) + and + [`src/cliproxy/types/provider-types.ts`](../src/cliproxy/types/provider-types.ts). -### FR-006: Health Diagnostics -- Verify Claude CLI installation -- Check config file integrity -- Validate symlinks and permissions +### FR-004: Target adapters -### FR-007: WebSearch Fallback -- Expose a CCS-managed local WebSearch tool for third-party profiles that cannot reach Anthropic's native tool -- Suppress native `WebSearch` on third-party launches and steer Claude toward the CCS-owned path when it is available -- Support Exa, Tavily, Brave, and DuckDuckGo real search backends -- Keep Gemini CLI, OpenCode, and Grok as optional legacy fallback -- Graceful fallback chain +- Support Claude Code, Factory Droid, and Codex CLI through target adapters. +- Deliver credentials in the form owned by each target: + environment variables for Claude launches, managed custom-model state for + Droid, and transient configuration overrides for CCS-routed Codex launches. +- Preserve user-owned target configuration outside the explicitly managed + fields. -### FR-007A: First-Class Image Analysis -- Expose a CCS-managed local `ImageAnalysis` MCP tool for third-party profiles that need provider-backed vision -- Resolve the provider route before launch and send requests directly to `/api/provider//v1/messages` -- Use editable prompt templates for `default`, `screenshot`, and `document` analysis modes -- Suppress the old CCS-managed `Read` hook during healthy MCP launches so it cannot compete with the primary path -- Keep the old `Read` hook as compatibility fallback only when MCP provisioning fails but provider-backed analysis is still viable -- Auto-heal stale CCS-managed image hooks and missing isolated MCP sync through launch-time cleanup, dashboard provisioning, and `ccs doctor --fix` -- Fall back to native `Read` without failing the whole launch when managed runtime, auth, or proxy readiness is unavailable +### FR-005: Configuration management -### FR-008: Remote CLIProxy Support -- Connect to remote CLIProxyAPI instances -- CLI flags for proxy configuration (--proxy-host, --proxy-port, etc.) -- Environment variable configuration (CCS_PROXY_HOST, etc.) -- Fallback to local proxy when remote unreachable -- Protocol-based default ports (443 for HTTPS, 8317 for HTTP) -- Dashboard UI for remote server configuration and testing +- Store CCS configuration under the directory resolved by + [`getCcsDir()`](../src/utils/config-manager.ts). +- Store API profile launch settings in `.settings.json` files under + that directory. +- Require all environment values written to settings files to be strings. +- Keep shared Claude settings unchanged during normal profile launches. +- Allow the explicit `ccs persist` workflow to merge a profile into + `~/.claude/settings.json`; back up the existing file and write atomically. +- Reject unsafe settings-file targets such as symlinks. -### FR-009: Quota Management (v7.14) -- Pause/resume individual accounts via `ccs cliproxy pause/resume ` -- Check quota status via `ccs cliproxy status [account]` -- Inspect the current proxy-wide routing strategy via `ccs cliproxy routing` -- Explicitly switch `round-robin` vs `fill-first` from CLI or dashboard -- Keep `round-robin` as the default until the user explicitly changes it -- Never infer routing strategy from account count, tier mix, or paused/default account state -- Auto-failover when account exhausted -- Tier detection: free/pro/ultra/unknown -- Distinguish entitlement failures from temporary capacity exhaustion -- Pre-flight quota checks before session start -- Dashboard UI with pause/resume toggles, tier badges, and quota-detail guidance +### FR-006: Dashboard and diagnostics -### FR-010: Docker Deployment -- Multi-stage Dockerfile with bun 1.2.21 and node:20-bookworm-slim -- Docker Compose setup with resource limits and healthcheck -- Persistent volumes for config, credentials, and CLI tools -- Pre-installed CLIs: claude, gemini, grok, opencode, ccs -- Ports: 3000 (Dashboard), 8317 (CLIProxy) -- Entrypoint with privilege dropping and usage help -- Environment variable configuration support +- Provide local APIs and a React UI for supported configuration workflows. +- Surface health, authentication, provider, routing, and usage state without + exposing credentials. +- Keep dashboard changes aligned with the same configuration contracts used by + the CLI. -### FR-011: Third-Party Tool Integration -- Export shell-evaluable env vars via `ccs env` command -- Support OpenAI, Anthropic, raw output formats -- Auto-detect shell (bash/zsh, fish, PowerShell) from $SHELL -- Security: single-quoted output, key sanitization, shell-specific escaping -- Cross-platform compatibility (macOS, Linux, Windows) +### FR-007: Managed tools -### FR-012: Official Claude Channels -- Support Telegram, Discord, and iMessage selection via `ccs config channels` and the dashboard -- Auto-inject `--channels` only for native Claude `default` and `account` sessions -- Store Telegram/Discord bot tokens in Claude's own `~/.claude/channels//.env` state or the official `*_STATE_DIR` override path when one is configured -- Treat iMessage as macOS-only, tokenless, and dependent on Claude-side install plus OS permissions -- Require Bun, Claude Code v2.1.80+, and verified `claude.ai` auth before runtime auto-enable -- Keep `--dangerously-skip-permissions` optional and never add it when the user already made an explicit permission choice -- Surface platform/auth/version/setup blockers clearly in both CLI and dashboard flows -- Preserve dashboard token drafts when save/refresh fails, and let already-selected unsupported iMessage entries be turned off without allowing re-enable on unsupported platforms +- Provide WebSearch and image-analysis integration for profiles that need + CCS-managed alternatives. +- Prefer explicit provider routes. +- Fail closed when enabled third-party WebSearch cannot prepare its constrained + MCP replacement. +- Allow image analysis to use compatible native behavior when its managed route + is unavailable. ---- +### FR-008: Remote proxy + +- Resolve remote proxy settings from supported CLI flags, environment + variables, and CCS configuration. +- Verify reachability before use. +- Fall back to a local proxy only when fallback is enabled. +- Fail instead of falling back when remote-only mode is selected. + +### FR-009: Quota and account-pool management + +- Display supported quota and account health data. +- Let users pause and resume accounts. +- Keep routing-strategy changes explicit. +- Temporarily remove exhausted accounts from rotation only under the + CCS-managed cooldown contract and restore only CCS-created pauses. + +### FR-010: Docker deployment + +- Publish an integrated multi-architecture image containing CCS, CLIProxy, and + the dashboard. +- Persist CCS state and service logs in declared volumes. +- Expose dashboard and CLIProxy service ports. +- Health-check both services. +- Do not bundle target AI CLIs into the integrated image; consumers that need + them run sibling containers or install them separately. + +### FR-011: Shell and editor integration + +- Export shell-safe environment values through `ccs env`. +- Support the documented shell output formats. +- Keep persistent shared settings behind `ccs persist`. +- Keep editor-specific writes scoped to the selected editor and user layer. ## Non-Functional Requirements -### NFR-001: Performance -- CLI startup < 100ms -- Dashboard load < 2s -- Minimal memory footprint +### NFR-001: Security + +- Do not print or log credentials. +- Bind local proxy services to loopback unless the user explicitly selects a + deployment that exposes them. +- Validate file types and use safe replacement semantics for managed sensitive + configuration. +- Keep remote transport and authentication choices explicit. ### NFR-002: Reliability -- Idempotent operations -- Graceful error handling -- Automatic recovery where possible -### NFR-003: Security -- Local-only proxy binding (127.0.0.1) -- No credential exposure in logs -- Secure token storage +- Make setup and repair operations idempotent where practical. +- Preserve existing user state when merging managed configuration. +- Report recovery actions in actionable error messages. +- Clean up child processes and temporary runtime state on exit. -### NFR-004: Cross-Platform -- Support Linux, macOS, Windows -- Bash 3.2+, PowerShell 5.1+, Node.js 14+ -- Identical behavior across platforms +### NFR-003: Portability -### NFR-005: Maintainability -- Files < 200 lines (with documented exceptions) -- Domain-based organization -- Barrel exports for clean imports -- 90%+ test coverage +- Support macOS, Linux, and Windows for the host CLI where target dependencies + allow. +- Support Node.js 18 or newer, as declared by + [`package.json`](../package.json). +- Support Bun 1.0 or newer for development and supported runtime workflows. +- Keep terminal output ASCII-only and respect `NO_COLOR` and TTY detection. ---- +### NFR-004: Maintainability -## Technical Requirements +- Keep provider metadata centralized. +- Use target adapters instead of target checks scattered through dispatch code. +- Validate CLI, server, and dashboard contracts with focused tests. +- Keep generated or volatile inventories out of evergreen architecture prose. -### TR-001: Runtime Dependencies -- Node.js 14+ or Bun 1.0+ -- Claude Code CLI installed -- Internet access for OAuth/API calls +## Runtime and Deployment Requirements -### TR-002: Optional Dependencies -- CLIProxyAPI binary (auto-managed) -- Exa/Tavily/Brave API keys for higher-quality WebSearch -- Gemini CLI for legacy WebSearch fallback -- Bun plus Claude Code v2.1.80+ with `claude.ai` auth for Official Channels auto-enable - -### TR-003: Configuration -- YAML-based config (`~/.ccs/config.yaml`) -- JSON settings per profile -- Environment variable overrides -- Official channel bot tokens stored in Claude-managed `~/.claude/channels//.env` - ---- +| Surface | Requirement | +| --- | --- | +| Host npm install | Node.js 18+ | +| Development and repository gates | Bun 1.0+ and Node.js 18+ | +| Claude launches | Claude Code installed and authenticated as required by the selected profile | +| Droid launches | Factory Droid installed | +| Codex launches | Codex CLI installed | +| Local OAuth proxy | CCS-managed CLIProxy binary | +| Integrated Docker | Docker or compatible container runtime; target CLIs are not bundled | ## Architecture Constraints -### AC-001: CLI-First Design -- All features accessible via CLI -- Dashboard is convenience layer, not required -- Scriptable and automatable +### AC-001: Profile state and shared settings are separate -### AC-002: Non-Invasive -- Never modify `~/.claude/settings.json` -- Use environment variables for configuration -- Reversible changes only +Normal launches consume CCS-owned profile files and environment state. +`~/.claude/settings.json` is written only through an explicit persistence or +approved settings-management workflow. -### AC-003: Proxy Pattern -- Use local proxy for provider routing -- Claude CLI communicates with localhost -- Proxy handles upstream API calls +### AC-002: Provider and target are independent axes ---- +A provider supplies credentials and routing. A target adapter determines how a +compatible CLI receives them. Unsupported combinations must fail clearly. -## Success Metrics +### AC-003: Proxy trust boundary is visible -| Metric | Target | Current | -|--------|--------|---------| -| Startup time | < 100ms | Achieved | -| Dashboard load | < 2s | Achieved | -| Error rate | < 1% | Achieved | -| Test coverage | > 90% | 90% (1440 tests, 6 skipped) | -| File size compliance | 100% < 200 lines | 95% | +Local CLIProxy, remote CLIProxy, and direct API profiles have different +transport and credential boundaries. CCS must not present them as equivalent or +silently cross those boundaries. ---- +### AC-004: Source owns volatile capability data -## Release Criteria +Provider IDs, aliases, OAuth flow types, callback ports, refresh ownership, +backend restrictions, and quota support must be read from the provider +registries and tests. Documentation describes how to find them rather than +copying a second mutable table. -### v1.0 Release (Complete) -- [x] Multi-account Claude support -- [x] OAuth provider integration (Gemini, Codex, AGY) -- [x] API profile management -- [x] Dashboard UI -- [x] Health diagnostics -- [x] WebSearch fallback -- [x] Cross-platform support +## Acceptance Criteria -### v7.0 Release (Complete) -- [x] OpenRouter integration with 300+ models -- [x] Interactive model picker -- [x] Dynamic model discovery -- [x] Tier mapping (opus/sonnet/haiku) -- [x] Settings page modularization (20 files) -- [x] Analytics page modularization (8 files) -- [x] Auth monitor modularization (8 files) -- [x] Comprehensive test infrastructure (539 CLI + 99 UI tests) - -### v7.1 Release (Complete) -- [x] Remote CLIProxy routing support -- [x] CLI flags for remote proxy (--proxy-host, --proxy-port, etc.) -- [x] Environment variables for proxy config (CCS_PROXY_*) -- [x] Dashboard remote proxy configuration UI -- [x] Connection testing with latency display -- [x] Fallback to local when remote unreachable -- [x] Protocol-based default ports (HTTPS:443, HTTP:8317) - -### v7.2 Release (Complete) -- [x] Kiro (AWS) OAuth provider support via CLIProxyAPIPlus -- [x] GitHub Copilot (ghcp) OAuth provider via Device Code flow (deprecated compatibility) -- [x] Authorization Code flow for Kiro (port 9876) -- [x] Device Code flow for ghcp (no local port needed) - -### v7.14 Release (Complete) -- [x] Hybrid quota management with auto-failover -- [x] `ccs cliproxy pause/resume/status` commands -- [x] API tier detection (free/pro/ultra/unknown) -- [x] Dashboard pause/resume toggles and tier badges -- [x] Pre-flight quota checks before session start - -### v7.23 Release (Complete) -- [x] Docker deployment support (PR #345) -- [x] Multi-stage Dockerfile with bun 1.2.21 -- [x] Docker Compose with resource limits and healthcheck -- [x] Persistent volumes for config and credentials -- [x] Pre-installed AI CLI tools (claude, gemini, grok, opencode) -- [x] Entrypoint with privilege dropping - -### v7.34 Release (Complete) -- [x] First-class `ImageAnalysis` MCP tool for third-party launches -- [x] Direct provider-scoped routing for image analysis requests -- [x] Prompt template selection for default / screenshot / document flows -- [x] Hook fallback retained only for compatibility -- [x] Non-fatal native `Read` fallback when managed runtime is unavailable -- [x] `ccs config image-analysis` CLI command -- [x] Doctor integration for hook validation -- [x] 791-line E2E test suite for image analysis -- [x] Performance: Replace busy-wait with Atomics.wait in config lock -- [x] Network error handling with noRetryPatterns -- [x] Quota 429 rate limit handling improvements -- [x] WebSocket maxPayload limit (DoS prevention) - -### v7.39 Release (Complete) -- [x] `ccs env` command for third-party tool integration (OpenCode, Cursor, Continue) -- [x] Multi-format output: openai, anthropic, raw -- [x] Multi-shell support: bash/zsh, fish, PowerShell (auto-detected) -- [x] CLIProxy profile support (gemini, codex, agy, qwen) -- [x] Settings profile support (glm, kimi, custom API) -- [x] Security: single-quoted output, key sanitization, shell-specific escaping -- [x] Shell completion updated (bash, zsh, fish, PowerShell) -- [x] 34 unit tests for env command - -### v8.0 Release (Planned - Q1 2026) -- [ ] Multiple CLIProxyAPI instances (load balancing, failover) -- [ ] Native git worktree support -- [ ] Critical bug fixes (#158, #155, #124) - -### v9.0 Release (Future - Q2 2026) -- [ ] Team collaboration features -- [ ] Cloud sync for profiles -- [ ] Plugin system -- [ ] CLI extension framework - ---- - -## Dependencies - -### External Services -- Anthropic Claude API -- Google Gemini API -- GitHub Codex API -- GitHub Copilot (ghcp - deprecated Device Code OAuth compatibility) -- AWS Kiro (Authorization Code OAuth) -- Z.AI GLM API -- OpenRouter API -- Moonshot Kimi API -- DeepSeek API -- Alibaba Qwen API -- Minimax API -- Azure Foundry API - -### Third-Party Libraries -- Express.js (web server) -- React (dashboard) -- Vite (build tool) -- shadcn/ui (UI components) -- CLIProxyAPI (proxy binary) -- Vitest (testing) - ---- +- A user can create or select a supported profile and launch it on a compatible + target without manual credential-file editing. +- Concurrent account profiles do not share target session state accidentally. +- Local and remote proxy failures follow the configured fallback policy. +- Persistent settings writes preserve unrelated keys and create a recovery + path. +- Dashboard operations produce configuration compatible with CLI operations. +- Release automation publishes only from the documented branches and lanes. +- Documentation links resolve and architecture claims are traceable to source. ## Risks and Mitigations -| Risk | Probability | Impact | Mitigation | -|------|-------------|--------|------------| -| Claude CLI API changes | Medium | High | Version pinning, compatibility layer | -| Provider API deprecation | Low | High | Fallback chain, multiple providers | -| OAuth token expiry | Medium | Medium | Auto-refresh, clear error messages | -| Binary compatibility | Low | Medium | Multi-platform builds, fallback | - ---- +| Risk | Mitigation | +| --- | --- | +| Provider auth contracts change | Central capability registry plus provider-specific tests | +| Target CLI configuration changes | Adapter boundary and compatibility checks | +| Credential leakage | Local storage, redaction, safe file handling, no secret logging | +| Remote proxy outage | Reachability checks and explicit fallback policy | +| Configuration corruption | Validation, backup, locking, and atomic replacement where supported | +| Documentation drift | Link volatile details to code; keep this document version-neutral | ## Related Documentation -- [Codebase Summary](./codebase-summary.md) - Technical structure -- [Code Standards](./code-standards.md) - Development conventions -- [System Architecture](./system-architecture/index.md) - Architecture diagrams -- [Project Roadmap](./project-roadmap.md) - Development phases and GitHub issues +- [Codebase Summary](./codebase-summary.md) +- [Code Standards](./code-standards.md) +- [System Architecture](./system-architecture/index.md) +- [Provider Flows](./system-architecture/provider-flows.md) +- [Release Process](./release-process.md) +- [Project Roadmap](./project-roadmap.md) diff --git a/docs/release-process.md b/docs/release-process.md index bdbd7ba2..f83a5e58 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -1,106 +1,117 @@ # CCS Release Process -CCS uses a decoupled release model: every merge to `main` immediately publishes -a stable npm `@latest` release and an immutable Docker `:` tag. Docker -mutable tags (`:latest`, `:`, `:`) require a separate manual -promote step after an operator-verified soak window. This decouples the npm -ecosystem from the Docker stability gate. +CCS has separate development, stable npm, and Docker promotion lanes. A branch +push starts the relevant workflow. Eligible `dev` pushes publish the next custom +development prerelease after their gates pass; `main` publishes only when +semantic-release finds release-worthy commits. -## Phase 1 — Automatic stable release (on every merge to `main`) +## Release lanes -1. A PR is merged into `main` with a conventional commit (`feat:`, `fix:`, etc.). -2. `release.yml` triggers semantic-release, which reads `.releaserc.cjs`. -3. Because `main` is a stable channel, semantic-release cuts a GitHub release - tagged `vX.Y.Z` and publishes the npm package to the `@latest` dist-tag - immediately. No rc channel, no soak delay on npm. -4. `docker-release.yml` triggers on the `release: published` event and: - - Validates the tag as stable semver (`vX.Y.Z`). - - Builds the integrated image for `linux/amd64` and `linux/arm64`. - - Pushes **only the immutable** `ghcr.io/kaitranntt/ccs:X.Y.Z` tag. - - Signs the image with cosign (keyless OIDC). - - Runs smoke tests (`smoke-test` job). - - Mutable tags (`:latest`, `:`, `:`) are **not** added at - this stage — `promote-mutable-tags` only runs on explicit - `workflow_dispatch` with `promote_to_latest=true`. +| Source | Workflow | Result | +| --- | --- | --- | +| Push to `dev` | [`dev-release.yml`](../.github/workflows/dev-release.yml) | Custom development prerelease and npm `@dev` publication | +| Push to `main` | [`release.yml`](../.github/workflows/release.yml) | Semantic-release stable version, npm `@latest`, tag, and GitHub release when commits require a release | +| Published stable or `rc` GitHub release | [`docker-release.yml`](../.github/workflows/docker-release.yml) | Immutable integrated Docker version tag, signature, and smoke test | +| Manual stable promotion | [`promote-release.yml`](../.github/workflows/promote-release.yml) | Docker `:latest`, major, and minor aliases | +| Stable GitHub release | [`sync-dev-after-release.yml`](../.github/workflows/sync-dev-after-release.yml) | Merge released `main` state back into `dev` | -## Phase 2 — Manual promotion to Docker mutable tags (rc.1 soak window) +## Development prereleases -After the immutable `:` Docker image has soaked (typically 24 h with no -reported issues), the operator promotes mutable tags: +`Dev Release` runs on pushes to `dev` and can also be dispatched manually. +After build and validation gates, it calls +[`scripts/dev-release.sh`](../scripts/dev-release.sh). That script owns the +`-dev.` version sequence and npm `@dev` publication. It is +intentionally separate from the production semantic-release configuration. -1. Verify the immutable image is healthy: +Generated `chore(release): ...` pushes are skipped by the workflow guard to +prevent release recursion. - ```bash - docker pull ghcr.io/kaitranntt/ccs:X.Y.Z - docker run --rm -p 3000:3000 -p 8317:8317 ghcr.io/kaitranntt/ccs:X.Y.Z - # check http://localhost:3000 and http://localhost:8317 - ``` +## Stable npm and GitHub releases -2. Optionally verify the cosign signature: +`Release` runs on `main`. It builds the CLI and dashboard, runs the fast, slow, +and end-to-end gates, then invokes semantic-release with +[`.releaserc.cjs`](../.releaserc.cjs). - ```bash - cosign verify \ - --certificate-identity-regexp "https://github.com/kaitranntt/ccs/.github/workflows/docker-release.yml" \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com \ - ghcr.io/kaitranntt/ccs:X.Y.Z - ``` +Semantic-release analyzes commits since the previous stable release: -3. Run the `promote-release` workflow via GitHub Actions UI or CLI: +- `feat` produces at least a minor release; +- `fix`, `hotfix`, `refactor`, and `style` produce patch releases under the + repository rules; +- breaking-change notation produces the appropriate major release; and +- commits without a matching release rule may produce no release. - ```bash - gh workflow run promote-release.yml \ - --field tag=vX.Y.Z - ``` +When a release is required, the lane updates `CHANGELOG.md` and `package.json`, +publishes npm `@latest`, creates the stable Git tag and GitHub release, and +pushes the generated release commit to `main`. Do not bump versions or create +release tags manually. - This dispatches `docker-release.yml` with `promote_to_latest=true`, which - triggers the `promote-mutable-tags` job to add `:latest`, `:`, and - `:` via `docker buildx imagetools create`. +## Docker publication and promotion - Alternatively, dispatch `docker-release.yml` directly: +The supported integrated image is `ghcr.io/kaitranntt/ccs`. - ```bash - gh workflow run "Publish Docker Image" \ - --field tag=vX.Y.Z \ - --field promote_to_latest=true - ``` +On a published stable `vX.Y.Z` or release-candidate `vX.Y.Z-rc.N` GitHub +release, `Publish Docker Image`: -## Why npm and Docker have different soak windows +1. validates the release tag; +2. checks out that tag; +3. builds the integrated image for `linux/amd64` and `linux/arm64`; +4. publishes only the matching immutable version tag; +5. signs the image digest with keyless cosign; and +6. smoke-tests the published image. -- **npm `@latest`**: Published immediately on every `main` merge. npm users who - pin a version are unaffected; users who run `npm install -g @kaitranntt/ccs` - get the latest immediately. Rollback is `npm install -g @kaitranntt/ccs@X.Y.Z`. -- **Docker `:latest`**: Promoted only after operator confirmation. Users who - pull `:latest` or run `docker pull` without a pinned tag are shielded from - a bad image. The immutable `:` tag is always available for pinned usage - from the moment of release. - -## Verifying the promotion +Mutable aliases are a separate operator decision. After verifying the immutable +image and allowing the desired soak period, dispatch `promote-release.yml`: ```bash -# Confirm :latest points to the promoted digest -docker buildx imagetools inspect ghcr.io/kaitranntt/ccs:latest +gh workflow run promote-release.yml --field tag=vX.Y.Z +``` -# Confirm npm @latest updated (happens automatically at Phase 1) +The promotion workflow verifies that the stable GitHub release and immutable +image exist, then dispatches `docker-release.yml` with +`promote_to_latest=true`. The promotion job creates `:latest`, `:X`, and +`:X.Y` aliases from the immutable image digest. + +The deprecated `ccs-dashboard` image has its own sunset compatibility job. +Do not use its tag behavior as the contract for the supported integrated image. + +## Post-release development sync + +A published, non-prerelease `vX.Y.Z` release targeting `main` triggers +`Sync Dev After Main Release`. The workflow merges `main` into `dev`, resolves +known generated version-file conflicts in favor of the released `main` state, +and pushes `dev`. That push intentionally triggers the normal `Push CI` and +development-release lanes. + +## Verification + +```bash +# npm channels npm view @kaitranntt/ccs dist-tags + +# immutable integrated image +docker buildx imagetools inspect ghcr.io/kaitranntt/ccs:X.Y.Z + +# mutable alias after promotion +docker buildx imagetools inspect ghcr.io/kaitranntt/ccs:latest ``` -## Rollback +Verify the GitHub Actions run and tag point to the expected commit before +announcing a release. -If a promoted release is found to be bad: +## Recovery -```bash -# Repoint :latest to the previous known-good immutable tag -docker buildx imagetools create \ - --tag ghcr.io/kaitranntt/ccs:latest \ - ghcr.io/kaitranntt/ccs:PREVIOUS.VERSION +- **Bad npm release:** publish a corrected patch. Do not unpublish a version + used by downstream consumers. +- **Bad immutable Docker image:** leave the immutable tag unchanged and publish + a corrected version. +- **Bad mutable Docker promotion:** promote a known-good immutable digest back + to the mutable aliases through the controlled workflow. +- **Failed `dev` sync:** repair the merge against current `main` and `dev`; + never overwrite branch history. -# For npm, publish a fix as a new patch release (do not unpublish) -# Unpublishing npm packages causes downstream breakage for pinned consumers. -``` +## Branch and tag summary -## Branch / tag taxonomy - -| Branch | Semantic-release channel | npm dist-tag | Docker tag (on release event) | Docker mutable (on promote) | -|--------|--------------------------|--------------|-------------------------------|------------------------------| -| `main` | stable | `@latest` | `:` (immutable, immediate) | `:latest`, `:`, `:` (after soak) | -| `dev` | `dev` prerelease | `@dev` | not published | not published | +| Branch | Package channel | npm dist-tag | Integrated Docker | +| --- | --- | --- | --- | +| `dev` | Development prerelease | `@dev` | None | +| `main` | Stable semantic release | `@latest` | Immutable tag on GitHub release; mutable aliases after manual promotion | From ebe174645938c8f86f92eafaaaa57a0db94482a6 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 26 Jul 2026 09:35:00 -0400 Subject: [PATCH 2/3] docs(architecture): reconcile provider and target contracts --- docs/system-architecture/index.md | 641 +++++-------- docs/system-architecture/provider-flows.md | 776 ++++------------ docs/system-architecture/target-adapters.md | 960 ++++---------------- 3 files changed, 571 insertions(+), 1806 deletions(-) diff --git a/docs/system-architecture/index.md b/docs/system-architecture/index.md index 280f9c94..21d0b0e9 100644 --- a/docs/system-architecture/index.md +++ b/docs/system-architecture/index.md @@ -1,465 +1,246 @@ # CCS System Architecture -Last Updated: 2026-04-14 +CCS separates profile resolution, provider routing, and target execution. This +document describes stable boundaries; source registries and tests own mutable +provider and command inventories. -High-level architecture overview for the CCS (Claude Codex Switch) system. +## System context ---- - -## System Overview - -CCS is a multi-provider profile and runtime manager that enables seamless switching between multiple Claude accounts, alternative AI providers, and multiple CLI targets (Claude Code, Factory Droid, Codex CLI) for credential delivery. - -The system consists of two main components: - -1. **CLI Application** (`src/`) - Node.js TypeScript CLI -2. **Dashboard UI** (`ui/`) - React web application served by Express - -Dashboard localization (i18n) architecture and contributor workflow are documented in [Dashboard i18n Guide](../i18n-dashboard.md). - -CCS v7.34 adds Image Analysis Hook for vision model proxying through CLIProxy with automatic injection for all profile types. -CCS v7.67 adds a native structured logging lane for CCS-owned runtime events, backed by `src/services/logging/`, bounded JSONL files under `~/.ccs/logs/`, and a dedicated dashboard `/logs` route. -CCS PR review now uses PR-Agent in GitHub Actions, with reviews running on the self-hosted `cliproxy` runner, existing `AI_REVIEW_*` workflow variables and secrets preserved as the runtime contract, and repo-level guidance stored in `.pr_agent.toml`. - -``` -+===========================================================================+ -| CCS System | -+===========================================================================+ -| | -| +------------------+ +-----------------+ +----------------+ | -| | User Terminal | ---> | CCS CLI | ---> | Target CLI | | -| | (ccs command) | | (src/ccs.ts) | | (claude/droid/codex) | | -| +------------------+ +-----------------+ +----------------+ | -| | | | -| v v | -| +------------------+ +-----------------+ +----------------+ | -| | Dashboard UI | <--> | Express | ---> | Provider APIs | | -| | (React SPA) | | Web Server | | (Claude/GLM/ | | -| +------------------+ +-----------------+ | Gemini/etc) | | -| | +----------------+ | -| v | -| +---------------------+ | -| | CLIProxyAPI | | -| | (Local or Remote) | | -| +---------------------+ | -| | -+===========================================================================+ -``` - ---- - -## Component Architecture - -### Multi-Target Adapter System - -CCS v7.45 introduces the Target Adapter pattern, enabling seamless integration with different CLI implementations. - -**Key architecture:** - -``` -Profile Resolution (CLIProxy, Settings/API, Account-based) +```text +User or automation | v -Target Resolution (--target flag > runtime entrypoint / argv[0] > config > default) - | - v -Get Target Adapter (Claude, Droid, or Codex) - | - +---> detectBinary() (find CLI on system) - | - +---> prepareCredentials() (write config or set env) - | - +---> buildArgs() (construct CLI arguments) - | - +---> buildEnv() (prepare environment variables) - | - v -Spawn Target Process +CCS CLI ------------------------> Target CLI + | | + | profile/provider state | provider protocol + v v +CCS local server <-----------> CLIProxy or direct API + | + v +React dashboard ``` -**Each target adapter implements different credential delivery:** +The main implementation surfaces are: -- **Claude Adapter**: Env var delivery (existing behavior) - - `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL` - - No config files needed +| Surface | Ownership | +| --- | --- | +| `src/` | CLI, dispatch, server, provider integration, target adapters | +| `ui/src/` | Dashboard application | +| `dist/` and `dist/ui/` | Build outputs | +| `docker/` | Integrated and legacy container definitions | +| `tests/` | Unit, integration, end-to-end, native, and Docker contracts | -- **Droid Adapter**: Config file delivery to `~/.factory/settings.json` - - Writes custom model entry: `custom:ccs-` - - Spawns: `droid -m custom:ccs- ` - - Model config includes baseUrl, apiKey, provider +Dashboard localization is documented in the +[Dashboard i18n Guide](../i18n-dashboard.md). -- **Codex Adapter**: Transient runtime overrides plus user-layer dashboard inspection - - Uses `codex -c key=value` only for CCS-routed launches - - Preserves native `~/.codex/config.toml` ownership - - Dashboard page reads/writes only the user config layer with explicit runtime-vs-provider warnings +## Execution pipeline -**Runtime entrypoints (built-in bins) and argv[0]-style aliases:** - -``` -ccs → Target: claude (default) -ccs-droid → Target: droid (explicit alias) -ccsd → Target: droid (legacy shortcut) -ccs-codex → Target: codex (explicit alias) -ccsx → Target: codex (short alias) -ccsxp → Target: codex (native cliproxy shortcut; prepends `--config model_provider="cliproxy"`) -``` - -For details on the adapter architecture, see [Target Adapters](./target-adapters.md). - -### CLI Layer - -``` -+===========================================================================+ -| CLI Architecture | -+===========================================================================+ - - User Input (ccs [--target ] [args]) - | - v - +-------------+ - | ccs.ts | Entry point, command routing - +-------------+ - | - +---> [Version/Help/Doctor/etc.] ---> Exit - | - v - +------------------+ - | Target Resolution | Determine which CLI to use - +------------------+ - | - v - +-------------+ - | Profile | Determines execution path - | Detection | - +-------------+ - | - +---> [Native Claude Account] ---> execClaude() - | | - +---> [CLIProxy Provider] ---> execClaudeWithCLIProxy() - | | - +---> [Settings/API Profile] ---> normalize legacy glmt if needed - | - v - +------------------+ - | Target Adapter | Get appropriate adapter - +------------------+ - | - v - +------------------+ - | Prepare Creds | Deliver credentials - +------------------+ - | - v - +------------------+ - | Target CLI | Claude Code or Droid - +------------------+ -``` - ---- - -## Data Flow Architecture - -### CLI Execution Flow - -``` -+===========================================================================+ -| CLI Execution Flow | -+===========================================================================+ - - 1. Parse Arguments - | - v - 2. Resolve Target Type - | - v - 3. Detect Profile Type - | - +---> Native Claude ---> 3a. Load Account Settings - | | - | v - | 4a. Set CLAUDE_CONFIG_DIR - | | - | v - | 5a. Get Claude Target Adapter - | - +---> CLIProxy -------> 3b. Ensure Binary Installed - | | - | v - | 4b. Generate Config - | | - | v - | 5b. Resolve Target Adapter - | | - | v - | 6b. Prepare Credentials - | | - | v - | 7b. Spawn via Adapter - | - +---> Settings/API ---> 3c. Load settings env - | - v - 4c. Normalize legacy glmt if needed - | - v - 5c. Resolve Target Adapter - | - v - 6c. Spawn via Adapter -``` - ---- - -## Provider Integration Architecture - -For detailed provider flows (CLIProxyAPI, legacy GLMT compatibility, quota management), see [Provider Flows](./provider-flows.md). - ---- - -## Configuration Architecture - -### CCS Logging Architecture - -- Shared logging contract lives in `src/services/logging/` and is used for CCS-owned runtime diagnostics, request tracing, and bounded recent-entry reads. -- Config lives at top-level `logging.*` in `~/.ccs/config.yaml`; `cliproxy.logging.*` still controls upstream CLIProxy runtime files only. -- CCS-owned runtime logs write to `~/.ccs/logs/current.jsonl` and rotate into `~/.ccs/logs/archive/` based on policy. -- Dashboard exposure uses native `/api/logs/config`, `/api/logs/sources`, and `/api/logs/entries` endpoints plus the `System -> Logs` React page. -- Request logging explicitly skips `/api/logs` reads so the log viewer does not recursively log itself. - -### Config File Hierarchy - -``` -+===========================================================================+ -| Configuration Hierarchy | -+===========================================================================+ - - ~/.ccs/ +```text +Parse command | - +---> config.yaml # Main CCS config (unified) +Resolve command vs launch | - +---> profiles.json # Claude account registry +Resolve target | - +---> .settings.json # Per-profile settings +Resolve profile type + +-- account profile ------> isolated target config root + +-- settings profile -----> profile environment + +-- CLIProxy provider ----> local or remote proxy route | - +---> cliproxy/ - | | - | +---> config.yaml # CLIProxy configuration - | +---> auth/ # OAuth tokens - | +---> bin/ # CLIProxy binary +Prepare target credentials | - +---> shared/ # Symlinked resources - | - +---> commands/ # Claude Code commands - +---> skills/ # Custom skills - +---> agents/ # Agent configurations - +---> plugins/ - | - +---> cache/ # Shared plugin payload/cache data - +---> marketplaces/ # Shared marketplace payload directories - +---> installed_plugins.json - - ~/.ccs/instances// - | - +---> plugins/ - | - +---> known_marketplaces.json # Instance-local registry for active CLAUDE_CONFIG_DIR validation - - ~/.factory/ (Droid CLI) - | - +---> settings.json # Droid config (custom models) +Spawn target and forward lifecycle signals ``` -Plugin ownership note: -- `commands/`, `skills/`, `agents/`, and `settings.json` remain shared through the existing symlink/copy flow. -- Marketplace payload directories stay shared, but `known_marketplaces.json` is reconciled per instance so Claude Code can validate `installLocation` against that instance's `CLAUDE_CONFIG_DIR/plugins/marketplaces`. +Command routing stops before profile execution for management commands such as +configuration, diagnostics, proxy management, and environment export. -### Config Loading Order +### Profile resolution -``` - 1. Environment Variables (highest priority) - | - v - 2. CLI Arguments (including --target) - | - v - 3. Profile-specific settings (~/.ccs/.settings.json) - | - v - 4. Main config (~/.ccs/config.yaml) - | - v - 5. Default values (lowest priority) +Profile resolution distinguishes: + +1. built-in CLIProxy provider shortcuts; +2. user-defined CLIProxy profiles; +3. settings/API profiles; and +4. registered account profiles. + +Canonical provider IDs and aliases come from +[`src/cliproxy/provider-capabilities.ts`](../../src/cliproxy/provider-capabilities.ts). +The detector and dispatcher consume those registries; documentation must not +maintain a second provider list. + +### Target resolution + +Provider and target are independent axes. The selected target is resolved from +explicit flags and runtime entry points before falling back to configuration +and the default target. + +Each adapter owns credential delivery: + +- **Claude Code:** launch environment and optional isolated + `CLAUDE_CONFIG_DIR`; +- **Factory Droid:** CCS-managed custom-model entries in + `~/.factory/settings.json`; and +- **Codex CLI:** transient `-c` overrides for CCS-routed launches while native + user configuration remains separately owned. + +See [Target Adapters](./target-adapters.md) for the detailed compatibility +contract. + +## Provider routing + +CCS supports three routing boundaries: + +| Route | Credential and transport owner | +| --- | --- | +| Direct settings/API profile | Target receives the selected provider's environment | +| Local CLIProxy | CCS manages a local proxy binary, config, and auth directory | +| Remote CLIProxy | CCS connects to the configured remote service and applies the selected fallback policy | + +Local backend choice is explicit. `original` is the default. `plus` is an +opt-in backend for provider capabilities unavailable in the original backend. +Compatibility restrictions are defined in +[`src/cliproxy/types/provider-types.ts`](../../src/cliproxy/types/provider-types.ts) +and enforced before local execution. + +See [Provider Flows](./provider-flows.md). + +## Configuration ownership + +The effective CCS directory is resolved by +[`src/utils/config-manager.ts`](../../src/utils/config-manager.ts). The normal +default is `~/.ccs`; tests and scoped workflows can override it. + +```text +CCS directory +├── config.yaml +├── profiles.json +├── .settings.json +├── instances/ +├── logs/ +└── cliproxy/ + ├── config.yaml + ├── auth/ + └── bin/ ``` ---- +### Settings-write contract -## WebSocket Architecture +Normal launches do not rewrite shared Claude settings. API profiles store +string-valued launch environment in CCS-owned per-profile settings. -### Real-time Communication +Persistent shared configuration is explicit: -``` -+===========================================================================+ -| WebSocket Communication | -+===========================================================================+ +- `ccs persist` reads and validates `~/.claude/settings.json`; +- it refuses unsafe symlink targets; +- it preserves unrelated settings while updating the requested managed fields; +- it creates a backup when an existing file is present; and +- it writes the replacement atomically under a settings-directory lock. - Dashboard (React) Server (Express) - | | - |<------ Connection Established ------>| - | | - |<------ health:update ----------------| Health status - | | - |<------ auth:status ------------------| Auth changes - | | - |<------ usage:update -----------------| Usage stats - | | - |------- action:refresh -------------->| User requests - | | +Target-owned writers follow their own boundary. For example, the Droid adapter +manages CCS custom-model entries in `~/.factory/settings.json`, not arbitrary +user settings. + +## Local server and dashboard + +The Express server exposes APIs used by the React dashboard for supported +configuration, auth, usage, health, and logging workflows. The dashboard is a +management surface over shared services; it must not implement a competing +configuration model. + +Real-time updates use server-owned WebSocket messages. Event names and payloads +are code contracts and should be read from the server and UI implementations +rather than copied into this overview. + +## Logging + +CCS-owned structured runtime logging lives under `src/services/logging/`. +Top-level `logging.*` configuration controls CCS JSONL logs under the CCS +directory. `cliproxy.logging.*` controls upstream CLIProxy files and is a +separate contract. + +The dashboard log reader excludes its own log-read requests from request +logging to prevent recursive noise. See [Logging Contract](../logging-contract.md). + +## Managed tool preparation + +WebSearch and image analysis are prepared before the target launch when the +selected profile needs CCS-managed tooling. They use provider-aware routes but +have different failure contracts: enabled third-party WebSearch fails closed if +its managed MCP replacement cannot be prepared, while image analysis can use a +compatible native path when available. + +- [WebSearch](../websearch.md) +- [Provider Flows](./provider-flows.md) + +## Security and trust boundaries + +### Local host + +CCS reads and writes user-authorized configuration and starts target processes. +That local filesystem access is more privileged than a provider API request. +Sensitive values must not enter logs or dashboard responses. + +### Local proxy + +The host CLI uses loopback for locally managed CLIProxy traffic. Local auth +files and the management API remain sensitive even when the transport never +leaves the machine. + +### Remote proxy + +A remote CLIProxy crosses a network and administrative boundary. TLS, +authentication, certificate policy, reachability, and local fallback are +explicit configuration choices. Remote-only mode must not silently start a +local proxy. + +### Container deployment + +The integrated container exposes dashboard and proxy ports through the +operator's port mappings. The image therefore does not inherit the host +installation's loopback-only assumption. Network exposure and access control +belong to the deployment operator. + +## Build and distribution + +```text +src/ -------- TypeScript --------> dist/ +ui/src/ ----- Vite --------------> dist/ui/ + | + v + npm package + | + v + integrated Docker image ``` ---- +The package requires Node.js 18 or newer. Repository development supports Bun +1.0 or newer. CI can pin newer tool versions independently; those workflow pins +are not the minimum consumer runtime contract. -## Security Architecture +The integrated Docker image is built from +[`docker/Dockerfile.integrated`](../../docker/Dockerfile.integrated). It layers +CCS onto a digest-pinned CLIProxy base, runs CLIProxy and the dashboard under +supervision, and health-checks both services. It does not bundle Claude Code, +Gemini CLI, Codex CLI, Droid, or other target CLIs. -### Authentication Flow +Release lane details are in [Release Process](../release-process.md). -See [Provider Flows](./provider-flows.md) → Authentication Flow section. +## Architecture invariants -### Security Boundaries +- Provider identity comes from the provider registry. +- Target compatibility is enforced at the adapter boundary. +- Environment values persisted in settings are strings. +- Shared target configuration changes require an explicit workflow. +- Local and remote proxy modes do not silently cross trust boundaries. +- Dashboard and CLI use the same domain services and configuration schema. +- Volatile capability details remain source-owned. -``` - +------------------+ - | User Terminal | - +------------------+ - | - | Local only (no network exposure) - v - +------------------+ - | CCS CLI | - +------------------+ - | - | Localhost only (127.0.0.1) - v - +------------------+ - | CLIProxy/Legacy | Binds to localhost only - +------------------+ - | - | TLS encrypted - v - +------------------+ - | Target CLI | Spawned locally (claude/droid) - +------------------+ - | - | TLS encrypted - v - +------------------+ - | Provider APIs | External endpoints - +------------------+ -``` +## Related documentation ---- - -## Build and Distribution - -### Build Pipeline - -``` -+===========================================================================+ -| Build Pipeline | -+===========================================================================+ - - src/ (TypeScript) ui/src/ (React TSX) - | | - v v - TypeScript Compiler Vite Build - | | - v v - dist/ (JavaScript) dist/ui/ (Static assets) - | | - +---------------+---------------------+ - | - v - npm package (@kaitranntt/ccs) - | - v - npm registry / GitHub releases -``` - -### Package Contents - -``` - @kaitranntt/ccs - | - +---> dist/ # Compiled CLI - +---> dist/ui/ # Built dashboard - +---> lib/ # Native scripts - | +---> ccs # Bash bootstrap - | +---> ccs.ps1 # PowerShell bootstrap - +---> package.json -``` - ---- - -## Deployment Architecture - -### Local Installation - -``` - npm install -g @kaitranntt/ccs - | - v - Global node_modules - | - +---> Creates symlink: ccs --> dist/ccs.js - | - +---> Runtime aliases: ccs-droid / ccsd → ccs (auto-select droid target) - | - +---> First run creates: ~/.ccs/ -``` - -### PR Review Lane - -Automated pull request review stays in `.github/workflows/ai-review.yml`, but the workflow now runs PR-Agent instead of the old Claude action. Reviews run on the existing self-hosted `cliproxy` runner, while the workflow preserves the existing `AI_REVIEW_BASE_URL`, `AI_REVIEW_MODEL`, and `AI_REVIEW_API_KEY` contract by mapping those values into PR-Agent env keys such as `OPENAI.*`, `config.*`, and `github_action_config.*`. Repo-specific reviewer guidance lives in `.pr_agent.toml`. - -``` -GitHub Actions `ai-review.yml` - | - v -Self-hosted `cliproxy` runner - | - v -PR-Agent action - | - v -CLIProxy - | - v -Configured model from `.pr_agent.toml` -``` - -- `ai-review.yml` owns automation wiring such as runner selection, PR-Agent action usage, and runtime values mapped from `AI_REVIEW_*` into `OPENAI.*`, `config.*`, and `github_action_config.*`. -- `.pr_agent.toml` in the repo root owns review instructions for this repository. -- Contributors should treat PR-Agent comments and trusted `/review` reruns as the primary AI review path for PRs targeting CCS. - -### Runtime Dependencies - -``` - +------------------+ +------------------+ - | Node.js 14+ | | Claude CLI | - | (required) | | (required) | - +------------------+ +------------------+ - - +------------------+ +------------------+ - | CLIProxyAPI | | Droid CLI | - | (auto-managed) | | (optional) | - +------------------+ +------------------+ -``` - ---- - -## Related Documentation - -- [Codebase Summary](../codebase-summary.md) - Detailed directory structure -- [Code Standards](../code-standards.md) - Coding conventions & patterns -- [Target Adapters](./target-adapters.md) - Multi-CLI adapter architecture -- [Provider Flows](./provider-flows.md) - CLIProxy, legacy GLMT compatibility, authentication flows -- [Project Roadmap](../project-roadmap.md) - Development phases +- [Codebase Summary](../codebase-summary.md) +- [Code Standards](../code-standards.md) +- [Target Adapters](./target-adapters.md) +- [Provider Flows](./provider-flows.md) +- [Release Process](../release-process.md) +- [Project Roadmap](../project-roadmap.md) diff --git a/docs/system-architecture/provider-flows.md b/docs/system-architecture/provider-flows.md index 39b516c3..c7a7a4d5 100644 --- a/docs/system-architecture/provider-flows.md +++ b/docs/system-architecture/provider-flows.md @@ -1,641 +1,247 @@ # Provider Integration Flows -Last Updated: 2026-07-16 +CCS routes profiles through direct target configuration or CLIProxy. This +document describes the durable flow and trust boundaries. Mutable provider +capabilities are linked to their source registries. -Detailed provider integration flows including CLIProxyAPI, legacy GLMT compatibility transforms, remote CLIProxy, quota management, and authentication. +## Sources of truth ---- +| Detail | Authoritative source | +| --- | --- | +| Canonical provider IDs, aliases, auth-flow type, callback ports, refresh ownership | [`src/cliproxy/provider-capabilities.ts`](../../src/cliproxy/provider-capabilities.ts) | +| Original vs Plus backend restrictions | [`src/cliproxy/types/provider-types.ts`](../../src/cliproxy/types/provider-types.ts) | +| Provider/backend enforcement | [`src/cliproxy/services/variant-service.ts`](../../src/cliproxy/services/variant-service.ts) | +| Remote proxy precedence and fallback | [`src/cliproxy/proxy/proxy-config-resolver.ts`](../../src/cliproxy/proxy/proxy-config-resolver.ts) | +| Provider capability regression tests | [`src/cliproxy/__tests__/provider-capabilities.test.ts`](../../src/cliproxy/__tests__/provider-capabilities.test.ts) | -## CLIProxyAPI Flow +Do not copy provider inventories, callback ports, or quota-supported lists into +this document. They change independently of the architecture. -### Overview +## Route selection -CLIProxyAPI is a local OAuth proxy binary that enables seamless integration with multiple AI providers. CCS manages the binary and configuration automatically. - -### Local Backend Choice - -CCS defaults to the original `router-for-me/CLIProxyAPI` backend because it is the stable MIT upstream. The `plus` backend is an explicit opt-in path that downloads the community-maintained `kaitranntt/CLIProxyAPIPlus` fork for providers that still require Plus-only support, such as Kiro, Cursor, GitLab, CodeBuddy, Kilo, and deprecated GitHub Copilot compatibility. CCS does not silently downgrade `backend: plus` to `original`; users choose that backend deliberately when they need those providers. - -Generated local CLIProxy configs also keep the management dashboard aligned with the selected backend. `backend: original` uses upstream CPAMC (`router-for-me/Cli-Proxy-API-Management-Center`), while `backend: plus` uses the CCS-maintained dashboard fork (`kaitranntt/Cli-Proxy-API-Management-Center`). Advanced users can override the generated `remote-management.panel-github-repository` value by setting `cliproxy.management_panel_repository` in `~/.ccs/config.yaml`; CCS will regenerate stale local CLIProxy configs when the expected dashboard repository changes. - -``` -+===========================================================================+ -| CLIProxyAPI Integration | -+===========================================================================+ - - Claude CLI - | - | ANTHROPIC_BASE_URL = localhost:XXXX - v - +------------------+ - | CLIProxyAPI | Local proxy binary (Plus fork opt-in for plus-only providers) - | (binary) | - +------------------+ - | - +---> OAuth Authentication - | | - | +---> Authorization Code Flow (port-based) - | | - Gemini, Codex, Antigravity, iFlow, Claude, GitLab - | | - Opens browser for user auth - | | - Callback to localhost:PORT - | | - | +---> Device Code Flow (no port needed) - | - xAI/Grok, Kimi, Kiro, Copilot, CodeBuddy, Kilo, and Qoder - | - User enters a code on the provider verification page - | - Polls for token completion - | | - | +---> Browser URL Polling (no callback port) - | - Cursor - | - Opens provider login URL returned by CLIProxyAPIPlus - | - Polls auth state until token is saved - | | - | v - | +------------------+ - | | OAuth Server | Browser-based auth - | +------------------+ - | - +---> Request Transformation - | | - | v - | Anthropic Format --> Provider Format - | - +---> Image Analysis Hook (v7.34) - | | - | v - | Vision Model Proxying (gemini, codex, agy, clipproxy) - | - Auto-injected via claude-hooks - | - Skip for Claude Sub accounts (native vision) - | - Fallback with deprecated block-image-read - | - +---> Provider APIs +```text +Resolved profile + | + +-- account profile + | └── target-native authenticated context + | + +-- settings/API profile + | └── direct or compatible upstream URL and credential environment + | + └-- CLIProxy provider + | + +-- reachable configured remote proxy + | + └-- local proxy, when selected or fallback is allowed | - +---> Google (Gemini) - +---> OpenAI (Codex) - +---> xAI (Grok) - +---> Antigravity (AGY) - +---> AWS Kiro (Claude-powered) - +---> GitHub Copilot (ghcp, deprecated compatibility) - +---> OpenAI-compatible endpoints + └── original or Plus backend ``` -### Supported Built-In Providers +The provider route is resolved before the target adapter prepares credentials. +This keeps provider selection independent from whether Claude Code, Droid, or +Codex receives the route. -| Provider | ID | Auth Method | Callback Port | Backend | -|----------|----|----|------|--------| -| Gemini | `gemini` | Authorization Code | 8085 | Original | -| Codex | `codex` | Authorization Code | 1455 | Original | -| xAI (Grok) | `xai` (`grok` CLI alias) | Device Code | none | Original | -| Antigravity | `agy` | Authorization Code | 51121 | Original | -| Qwen | `qwen` | Account linking unavailable in bundled runtime | none | Original | -| iFlow | `iflow` | Authorization Code | 11451 | Original | -| Claude | `claude` | Authorization Code | 54545 | Original | -| Kimi | `kimi` | Device Code | none | Original | -| Kiro (AWS) | `kiro` | Method-aware (default: Device Code) | none by default | Plus | -| GitHub Copilot (deprecated) | `ghcp` | Device Code | none | Plus | -| Cursor | `cursor` | Browser URL polling | none | Plus | -| GitLab Duo | `gitlab` | Authorization Code or PAT | 17171 | Plus | -| CodeBuddy | `codebuddy` | Device-style polling | none | Plus | -| Kilo AI | `kilo` | Device Code | none | Plus | -| Qoder | `qoder` | Device Code | none | Plus | +## Local CLIProxy -xAI uses CLIProxyAPI's `--xai-login` device flow. CCS stores and discovers the resulting -`xai-*.json` credentials under the canonical `xai` provider. `ccs grok` is a command alias only, -so it shares the same accounts, settings, live model catalog, and routing as `ccs xai`. +CCS manages the local binary, generated configuration, auth files, lifecycle, +and provider-specific launch environment. -### Codex Duplicate-Email Account Identity +The local backend contract is: -Codex can legitimately produce multiple auth files for the same email when the user has both a team/business login and a personal/free login. CCS now treats those as separate accounts instead of collapsing them by email. +- `original` is the default upstream CLIProxy backend; +- `plus` is an explicit opt-in community-maintained distribution; +- providers declared Plus-only fail clearly on the original backend; and +- CCS does not silently downgrade a configured Plus backend. -- Internal account IDs stay duplicate-aware for Codex only: `email#variant` -- Variant keys are derived from the auth filename, for example `kaidu.kd@gmail.com#04a0f049-team` and `kaidu.kd@gmail.com#free` -- Dashboard surfaces continue to show the canonical email, with a compact variant badge such as `Team` or `Free` -- Quota fetch resolves the exact registry `tokenFile` for the selected account instead of scanning by email and taking the first match -- Live usage/account monitor stats key by `provider + account identity`, so duplicate Codex emails no longer merge into one runtime bucket +The management-panel repository is generated to match the selected backend +unless the user supplies the supported override. -This preserves the user-visible distinction between business and personal Codex sessions while keeping other providers on their existing email-backed identity model. +Local target traffic uses a provider-compatible endpoint and an internal +credential. CLIProxy owns upstream OAuth token use and request translation +according to the selected provider. -### Built-In Provider Detection +## Remote CLIProxy -CCS derives canonical providers from `provider-capabilities.ts`. `profile-detector.ts` resolves only -explicit CLI aliases before routing through the CLIProxy execution flow. +Remote mode delegates proxy lifecycle and stored authentication to another +CLIProxy installation. -```typescript -const provider = resolveCLIProxyProviderShortcut(profileName); - -if (provider) { - return execClaudeWithCLIProxy(claudeCli, provider, args); -} +```text +CLI flags and environment + | + v +CCS proxy configuration + | + v +Reachability check + +-----+-----+ + | | +reachable unreachable + | | + remote remote-only? ---- yes ---> fail + | + no + | + fallback enabled? -- no ----> fail + | + yes + | + local proxy ``` ---- +Configuration precedence and accepted environment names are implemented in +[`proxy-config-resolver.ts`](../../src/cliproxy/proxy/proxy-config-resolver.ts). +The stable behavioral contract is: -## Legacy GLMT Compatibility Flow +- CLI flags override other sources where supported; +- environment can select a remote host; +- configuration supplies persistent remote settings; +- HTTPS and HTTP defaults remain protocol-aware; +- `--remote-only` disables local fallback; and +- an unreachable remote proxy falls back only when fallback is enabled. -### Overview +A remote proxy is a separate trust boundary. Its operator can receive provider +traffic and owns its stored OAuth state. Use TLS and authentication appropriate +to that boundary. -GLMT is no longer a marketed runtime surface in CCS. Existing `glmt` profiles are kept as a compatibility path and normalized at launch to the direct GLM endpoint. The `src/glmt/` module remains because Cursor response translation still imports its transformer pipeline. +## Authentication -``` -+===========================================================================+ -| Legacy GLMT Compatibility + Internal Transforms | -+===========================================================================+ +CCS asks the selected CLIProxy backend to start the provider's supported auth +flow. The provider registry identifies whether that is an authorization-code +flow, device-code flow, browser polling flow, or currently unsupported account +linking. - Claude CLI +```text +Select canonical provider | - | legacy glmt settings detected - v - +------------------+ - | Compatibility | normalizeDeprecatedGlmtEnv() - | Layer | (src/utils/glmt-deprecation.ts) - +------------------+ +Validate backend and auth-start support | - v - +------------------+ - | Direct GLM API | https://api.z.ai/api/anthropic - +------------------+ +Start backend-owned auth flow | - v - +------------------+ - | src/glmt/* | retained for Cursor translation - +------------------+ +User completes provider interaction + | +Backend writes provider auth file + | +CCS reconciles account identity and readiness + | +Launch through provider route ``` -### Supported Migration Targets +Provider tokens live under the configured CLIProxy auth directory. Documentation +and examples must use neutral account labels; raw email addresses, token +filenames, access tokens, and refresh tokens are not architecture data. -| Provider | Config Key | Endpoint | Auth | -|----------|------------|----------|------| -| Z.AI (GLM) | `glm` | https://api.z.ai/api/anthropic | API key | -| Kimi API | `km` | https://api.kimi.com/coding/ | API key | -| Legacy compatibility | `glmt` | normalized to direct GLM at runtime | existing profile only | +Some providers can produce more than one account with the same display email. +CCS keeps runtime identity tied to the exact registered auth file rather than +collapsing accounts by display text. The dashboard can show a safe variant +label without exposing the internal identifier. -Use `ccs glm` for Z.AI profiles and `ccs km` for reasoning-first Kimi API profiles. Keep `glmt` only when migrating an existing settings file. +## API-key profiles -### Runtime Handling +API-key profiles store string-valued environment under the CCS profile +settings file. -CCS detects the deprecated `glmt` profile name and normalizes legacy proxy-only settings before dispatching through the normal settings-profile flow: - -```typescript -if (isDeprecatedGlmtProfileName(profileName)) { - const normalized = normalizeDeprecatedGlmtEnv(settingsEnv); - // warn user, validate against direct GLM endpoint, continue through settings flow -} +```text +Create or edit API profile + | +Write CCS-owned .settings.json + | +Resolve direct/native vs compatible proxy-style auth + | +Target adapter prepares credentials + | +Spawn target ``` ---- +Native Anthropic profiles use `ANTHROPIC_API_KEY` without forcing a proxy base +URL. Compatible providers typically use `ANTHROPIC_BASE_URL` and +`ANTHROPIC_AUTH_TOKEN`. The profile writer owns that distinction; callers +should not infer it from documentation examples. -## Remote CLIProxy Flow (v7.1) +Normal launches do not write `~/.claude/settings.json`. Users who want shared +Claude settings must select the explicit `ccs persist` workflow. -### Overview +## Legacy GLMT compatibility -Remote CLIProxy enables CCS to delegate authentication to a central proxy server instead of spawning a local binary. +`glmt` is a compatibility input, not a current provider architecture. Existing +legacy settings are normalized to the direct GLM path before normal +settings-profile dispatch. Internal GLMT transformer modules can remain in use +by other compatibility surfaces without making GLMT a supported standalone +runtime. -``` -+===========================================================================+ -| Remote CLIProxy Architecture (v7.1) | -+===========================================================================+ +New configuration should use current API profile commands and provider names. - Config Resolution (proxy-config-resolver.ts) +## Quota and account-pool flow + +Quota support is provider-specific and intentionally sourced from +[`provider-capabilities.ts`](../../src/cliproxy/provider-capabilities.ts). + +The stable pool contract is: + +1. Reconcile registered accounts with live auth files. +2. Exclude manually paused accounts from rotation. +3. Fetch quota only for providers with an implemented quota fetcher. +4. Distinguish an exhausted account from a provider-wide or transient failure. +5. When a healthy fallback exists, CCS can create a temporary quota pause for + an exhausted account. +6. Persist the cooldown so later launches observe the same state. +7. Auto-resume only pauses created by CCS quota management; never override a + user's manual pause. + +Routing strategy remains an explicit user choice. CCS must not infer +round-robin or fill-first from account count, plan tier, or quota state. + +## Image analysis + +For profiles that need managed vision support, CCS prepares the image-analysis +route before launch: + +```text +Resolve profile and provider | - +---> Priority: CLI flags > ENV vars > config.yaml > defaults +Resolve supported provider backend | - v - +------------------+ - | ResolvedProxyConfig | - | mode: local|remote | - +------------------+ +Provision managed MCP/runtime configuration | - +---> [mode = local] ---> Spawn local CLIProxyAPI binary - | | - | v - | localhost:8317 +Target invokes ImageAnalysis | - +---> [mode = remote] ---> Connect to remote server - | - v - +------------------+ - | Health Check | remote-proxy-client.ts - | /v1/models | 2s timeout - +------------------+ - | - +---> [reachable] ---> Use remote - | | - | v - | protocol://host:port - | - +---> [unreachable] ---> Fallback decision - | - +-----------------------------+ - | - +---> [fallbackEnabled] ---> Start local - | - +---> [remoteOnly] ---> Fail with error - - CLI Flags: - --proxy-host Remote hostname/IP - --proxy-port Port (default: 8317 HTTP, 443 HTTPS) - --proxy-protocol http or https - --proxy-auth-token Bearer authentication - --local-proxy Force local mode - --remote-only Fail if remote unreachable - - Environment Variables: - CCS_PROXY_HOST Remote hostname - CCS_PROXY_PORT Remote port - CCS_PROXY_PROTOCOL Protocol (http/https) - CCS_PROXY_AUTH_TOKEN Auth token - CCS_PROXY_FALLBACK_ENABLED Enable fallback (true/false) +CCS sends provider-scoped request + | +Return text result ``` -### Configuration Resolution +The managed route must not expose its internal credential in logs or user +settings. If managed preparation, authentication, or proxy readiness is +unavailable, CCS falls back to compatible native behavior where possible +instead of failing the entire target launch. -```typescript -// proxy-config-resolver.ts: Priority order -const resolved = { - ...DEFAULT_CONFIG, // 4. Defaults (lowest) - ...yamlConfig, // 3. config.yaml - ...envConfig, // 2. Environment variables - ...cliFlags, // 1. CLI flags (highest) -}; -``` +Provider-specific vision mappings are implementation data and should be read +from the image-analysis routing services and tests. -### Health Check +## Session and observability boundary -```typescript -// remote-proxy-client.ts -async function checkRemoteProxyHealth(config: ResolvedProxyConfig): Promise { - try { - const url = `${config.protocol}://${config.host}:${config.port}/v1/models`; - const response = await fetch(url, { - headers: config.authToken ? { Authorization: `Bearer ${config.authToken}` } : {}, - timeout: 2000, - }); - return response.ok; - } catch { - return false; - } -} -``` +Execution paths can record non-secret metadata such as profile identity, +profile type, canonical provider, target type, timestamps, duration, and +numeric process exit status. Logs must not contain provider tokens, API keys, +raw auth payloads, or personal account identifiers. ---- +Textual CCS log codes and numeric child-process exit statuses are different +contracts. See [Logging Contract](../logging-contract.md). -## Quota Management Flow (v7.14) +## Invariants -### Overview +- Canonical provider identity is registry-driven. +- Provider aliases normalize before execution. +- Backend restrictions are validated before starting local auth or routing. +- Remote-only mode never starts a local fallback. +- Persistent environment values are strings. +- Account display text is not a unique runtime identity. +- Manual pauses are never auto-resumed by quota management. +- Provider credentials never appear in architecture examples. -Hybrid quota management enables automatic detection of exhausted accounts and failover to next available account. -Before local CLIProxy startup, CCS reconciles the whole active account pool for the provider, not only the default account. When CCS detects any quota-exhausted account and a healthy fallback exists, it temporarily pauses the exhausted account out of CLIProxy rotation and automatically resumes that pause after the configured cooldown expires. This durable self-pause uses the same account registry and token movement path as dashboard/manual pause, so the dashboard shows the account as paused and CLIProxy cannot rediscover its token from the live `auth/` folder. +## Related documentation -``` -+===========================================================================+ -| Quota Management Architecture (v7.14) | -+===========================================================================+ - - Pre-Flight Check (before session start) - | - v - +------------------+ - | quota-manager.ts | Hybrid quota management - +------------------+ - | - +---> Get all active accounts for provider - | - +---> For each account: - | | - | v - | +------------------+ - | | quota-fetcher.ts | Provider-specific API calls - | +------------------+ - | | - | +---> Check isPaused flag --> Skip if paused - | | - | +---> Fetch quota from provider API - | | - Antigravity: fetchAvailableModels - | | - Claude: policy limits endpoint - | | - Codex: ChatGPT usage windows - | | - Gemini CLI: Code Assist quota buckets - | | - GitHub Copilot: copilot_internal/user snapshots (deprecated compatibility) - | | - | +---> Detect tier (free/paid/unknown) - | | - | +---> Check exhaustion status - | - +---> Pause exhausted non-default accounts when another healthy account exists - | - +---> Select best account (not paused, not exhausted) - | - +---> Auto-failover to next account if current exhausted - | - +---> Temporarily pause exhausted account when fallback exists - | - move token out of live auth discovery - | - persist cooldown expiry across launches - | - auto-resume only CCS-created quota pauses - - CLI Commands: - ccs cliproxy pause --> Set isPaused=true in account-manager - ccs cliproxy resume --> Set isPaused=false - ccs cliproxy status [account] --> Display quota + tier info - - Dashboard UI: - - Pause/Resume toggle per account - - Tier badge (free/paid/unknown) - - Quota usage display -``` - -### Account Selection Algorithm - -```typescript -// quota-manager.ts: Best account selection -function selectBestAccount(accounts: AccountInfo[]): AccountInfo | null { - // Priority: - // 1. Not paused - // 2. Not exhausted - // 3. Paid tier over free tier - // 4. Highest remaining quota - - return accounts - .filter(acc => !acc.isPaused && !acc.isExhausted) - .sort((a, b) => { - if (a.tier !== b.tier) return (a.tier === 'paid' ? -1 : 1); - return (b.remainingQuota || 0) - (a.remainingQuota || 0); - })[0] || null; -} -``` - ---- - -## Authentication Flow - -### OAuth Providers - Authorization Code Flow - -**Providers**: Gemini, Codex, Antigravity, Kiro (aws method) - -``` -+===========================================================================+ -| OAuth - Authorization Code Flow (Port-based) | -+===========================================================================+ - - 1. User runs: ccs codex - | - v - 2. Check token cache (~/.ccs/cliproxy/auth/) - | - +---> [Valid token] ---> Use cached token - | - +---> [No/Expired token] - | - v - 3. Start local OAuth server (localhost:9876) - | - v - 4. Open browser with OAuth request - | https://oauth-provider/authorize?redirect_uri=http://localhost:9876/callback - v - 5. User authorizes in browser - | - v - 6. OAuth provider redirects to localhost:9876/callback?code=XXXX - | - v - 7. Exchange auth code for access token - | - v - 8. Cache token locally (~/.ccs/cliproxy/auth/gemini.json) - | - v - 9. Proceed with Claude CLI -``` - -### OAuth Providers - Device Code Flow - -**Providers**: GitHub Copilot (ghcp, deprecated compatibility) - -Provider identity note: -- Providers that do not expose a reliable email no longer require a manual nickname during first auth. -- CCS derives a stable internal account identifier from the token/cache context and still allows the user to rename the account later. - -``` -+===========================================================================+ -| OAuth - Device Code Flow (No Port Needed) | -+===========================================================================+ - - 1. User runs: ccs ghcp - | - v - 2. Check token cache (~/.ccs/cliproxy/auth/) - | - +---> [Valid token] ---> Use cached token - | - +---> [No/Expired token] - | - v - 3. Request device code from GitHub - | - v - 4. Display user code + verification URL - | "Enter code XXXX-XXXX at github.com/login/device" - v - 5. User opens URL in browser and enters code - | - v - 6. Poll GitHub for token completion - | - v - 7. Receive and cache token locally - | - v - 8. Proceed with Claude CLI -``` - -### Kiro OAuth - Method-Aware Flow - -**Supported methods**: -- `aws`: Device Code (default, AWS org friendly) -- `aws-authcode`: Authorization Code via CLI flow -- `google`: Social OAuth via management API -- `github`: Social OAuth via management API (Dashboard flow) - -``` -+===========================================================================+ -| Kiro OAuth - Method-Aware Flow | -+===========================================================================+ - - Configuration: - ccs_profile: - target: claude - cliproxy: - provider: kiro - kiro_method: aws # or aws-authcode, google, github - - Flow: - Device Code (aws) - → /start endpoint (no callback port) - → Opens browser - → User enters code - → Poll /status - - Authorization Code (aws-authcode, google, github) - → /start-url endpoint - → Returns auth_url - → User visits URL - → Callback handled - → Poll /status for completion - - Key behavior: - - Device Code method uses /start route (no callback port) - - Callback/social methods use /start-url + status polling - - Some management flows return state first, auth_url later - - Manual nicknames are optional when the upstream provider does not return an email - - Account storage uses a stable internal identifier so reauth/update flows do not depend on dashboard list order -``` - -### API Key Profiles (GLM, Kimi) - -``` -+===========================================================================+ -| API Key Profile (Non-OAuth) | -+===========================================================================+ - - 1. User configures API key in settings - | - v - 2. Key stored in ~/.ccs/.settings.json - | - v - 3. Profile detection: APIKeyProfile - | - v - 4. Key passed via ANTHROPIC_AUTH_TOKEN env var - | - v - 5. Target adapter (Claude/Droid) handles delivery - | - └─ Claude: env var - └─ Droid: config file (~/.factory/settings.json) -``` - -### Anthropic Direct API Key - -``` -+===========================================================================+ -| Anthropic Direct API Key (Native Auth) | -+===========================================================================+ - - 1. User creates profile: ccs api create --preset anthropic - | - v - 2. Key stored in ~/.ccs/.settings.json - | env: { ANTHROPIC_API_KEY: "sk-ant-..." } - | (NO ANTHROPIC_BASE_URL, NO ANTHROPIC_AUTH_TOKEN) - v - 3. Profile detection: settings-based - | - v - 4. Key passed via ANTHROPIC_API_KEY env var - | Claude CLI uses native endpoint (api.anthropic.com) - v - 5. Claude CLI authenticates with x-api-key header - - Detection logic (profile-writer.ts): - - apiKey.startsWith('sk-ant-') -> native mode - - baseUrl.includes('api.anthropic.com') -> native mode - - Otherwise -> proxy mode (existing behavior) -``` - ---- - -## Image Analysis Hook Flow (v7.34) - -### Overview - -Image Analysis Hook enables vision model proxying through CLIProxy with automatic injection for all profile types. - -``` -+===========================================================================+ -| Image Analysis Hook Flow (v7.34) | -+===========================================================================+ - - Claude CLI with image input - | - v - Hook Installer (ensureProfileHooks) - | - +---> Check ~/.claude/hooks/openai-vision-hook.cjs exists - | - +---> If missing: auto-install via image-analyzer-hook-installer - | - v - Hook Configuration - | - +---> Set ANTHROPIC_IMAGE_HOOK_URL - | (proxy endpoint URL) - | - v - Claude CLI processes image request - | - v - Claude prefers ImageAnalysis MCP tool - | - v - CCS provider-backed image analysis - | - +---> Provider route resolved before launch - | - +---> Direct request to /api/provider//v1/messages - | - +---> Native Read fallback if runtime/auth/proxy is unavailable - | - v - Text description returned to Claude CLI -``` - -### Runtime Environment - -```typescript -// getImageAnalysisHookEnv() -{ - CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL: 'http://127.0.0.1:8317', - CCS_IMAGE_ANALYSIS_RUNTIME_PATH: '/api/provider/agy', - CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY: 'ccs-internal-managed', -} -``` - -### Provider Support - -| Provider | Vision Support | Notes | -|----------|---|---| -| Gemini | ✓ | Via CCS ImageAnalysis provider route | -| Codex | ✓ | Via CCS ImageAnalysis provider route | -| Antigravity | ✓ | Via CCS ImageAnalysis provider route | -| Kiro | ✓ | Via mapped CCS provider route when configured | -| Copilot | ✓ | Deprecated compatibility route via mapped ghcp provider | -| GLM/Kimi | ✓ | Via explicit or fallback backend mapping | - ---- - -## Session Tracking - -All execution paths record session metadata including target CLI used: - -```typescript -{ - profileName: 'gemini', - profileType: 'clipproxy', - provider: 'google-gemini', - targetCli: 'claude', // NEW: which target was used - timestamp: '2026-02-16T10:40:00Z', - duration: 12345, - exitCode: 0, - model: 'claude-opus-4-6', -} -``` - -This enables analytics on target CLI usage and adoption. - ---- - -## Related Documentation - -- [System Architecture Index](./index.md) — Overall system design -- [Target Adapters](./target-adapters.md) — Multi-CLI adapter pattern -- [Codebase Summary](../codebase-summary.md) — Module structure -- [Code Standards](../code-standards.md) — Implementation guidelines +- [System Architecture](./index.md) +- [Target Adapters](./target-adapters.md) +- [Logging Contract](../logging-contract.md) +- [Codebase Summary](../codebase-summary.md) +- [Code Standards](../code-standards.md) diff --git a/docs/system-architecture/target-adapters.md b/docs/system-architecture/target-adapters.md index a2b11eb4..6218f087 100644 --- a/docs/system-architecture/target-adapters.md +++ b/docs/system-architecture/target-adapters.md @@ -1,803 +1,181 @@ # Target Adapters -Last Updated: 2026-05-18 +Target adapters are the last-mile boundary between CCS profile resolution and a +supported CLI runtime. Profile discovery and credential resolution happen +before this boundary; the selected adapter owns binary detection, target-native +credential delivery, argument and environment construction, and child-process +execution. -Detailed documentation of the target adapter pattern and implementations. +Related architecture: ---- +- [System architecture](./index.md) +- [Provider flows](./provider-flows.md) -## Overview +## Contract -The target adapter system enables CCS to dispatch credential-resolved profiles to different CLI implementations while maintaining a unified configuration and profile system. +The canonical interface and data types live in +[`src/targets/target-adapter.ts`](../../src/targets/target-adapter.ts). Each +adapter must: -**Key insight**: Profile resolution (detecting provider, loading auth, building credentials) is target-agnostic. Only the final credential delivery and process spawning differ per target. +1. detect its runtime binary without changing user configuration; +2. reject unsupported profile types before launch; +3. prepare only the target-owned configuration needed for the launch; +4. construct an argument vector and environment without exposing credentials in + arguments; +5. spawn the target with inherited stdio and forward process signals. ---- +The registry in +[`src/targets/target-registry.ts`](../../src/targets/target-registry.ts) maps a +target type to its adapter. Target names, built-in aliases, legacy alias +environment variables, and persistence eligibility are centralized in +[`src/targets/target-metadata.ts`](../../src/targets/target-metadata.ts). -## Target Adapter Interface - -Each CLI target implements the `TargetAdapter` contract: - -```typescript -export interface TargetAdapter { - readonly type: TargetType; // 'claude' | 'droid' | 'codex' - readonly displayName: string; // "Claude Code" | "Factory Droid" | "Codex CLI" - - /** Detect if the target CLI binary exists on system */ - detectBinary(): TargetBinaryInfo | null; - - /** Prepare credentials for delivery to target CLI */ - prepareCredentials(creds: TargetCredentials): Promise; - - /** Build spawn arguments for the target CLI */ - buildArgs( - profile: string, - userArgs: string[], - options?: { - creds?: TargetCredentials; - profileType?: ProfileType; - binaryInfo?: TargetBinaryInfo; - } - ): string[]; - - /** Build environment variables for the target CLI */ - buildEnv(creds: TargetCredentials, profileType: string): NodeJS.ProcessEnv; - - /** Spawn the target CLI process (replaces current process flow) */ - exec(args: string[], env: NodeJS.ProcessEnv, options?: { cwd?: string }): void; - - /** Check if a profile type is supported by this target */ - supportsProfileType(profileType: string): boolean; -} -``` - -### Type Definitions - -```typescript -export type TargetType = 'claude' | 'droid' | 'codex'; - -export interface TargetCredentials { - baseUrl: string; // API endpoint - apiKey: string; // Auth token - model?: string; // Model ID - provider?: 'anthropic' | 'openai' | 'generic-chat-completion-api'; - envVars?: NodeJS.ProcessEnv; // Additional env vars -} - -export interface TargetBinaryInfo { - path: string; // Full path to binary - needsShell: boolean; // Windows .cmd/.bat/.ps1? - version?: string; // Optional version string - features?: readonly string[]; // Capability probes -} -``` - ---- +Do not duplicate the TypeScript interface in this guide. Interface signatures +and credential fields change with runtime requirements; the source files are +the contract checked by the compiler. ## Target Resolution -CCS resolves which adapter to use via priority-ordered checks: - -### Resolution Priority - -``` -1. --target flag (CLI argument) — highest priority - └─ ccs --target droid glm - └─ ccs --target codex - -2. Explicit runtime entrypoint (`CCS_INTERNAL_ENTRY_TARGET`) — dedicated bin shims - └─ ccs-droid / ccsd → droid - └─ ccs-codex / ccsx → codex - └─ ccsxp → codex, then prepends `--config model_provider="cliproxy"` - -3. argv[0] detection (runtime alias pattern) — binary name mapping for same-binary/custom aliases - └─ ccs-droid (explicit alias) → droid - └─ ccsd (legacy shortcut) → droid - └─ ccs-codex (explicit alias) → codex - └─ ccsx (short alias) → codex - └─ ccs (regular command) → default - -4. Per-profile config (from ~/.ccs/config.yaml or settings.json) - └─ persisted targets are currently only `claude` and `droid` - └─ profiles: - glm: - target: droid - -5. Fallback: 'claude' — lowest priority -``` - -### Implementation - -```typescript -// src/targets/target-resolver.ts - -export function resolveTargetType( - args: string[], - profileConfig?: { target?: TargetType } -): TargetType { - // 1. Parse --target flags (supports --target value and --target=value) - // Repeated flags: last one wins. - const parsed = parseTargetFlags(args); - if (parsed.targetOverride) { - return parsed.targetOverride; - } - - // 2. Check explicit runtime entrypoint shim - const entrypointTarget = resolveEntrypointTarget(); - if (entrypointTarget) { - return entrypointTarget; - } - - // 3. Check argv[0] (binary name / custom alias map) - const binName = path.basename(process.argv[1] || process.argv0 || '').replace(/\.(cmd|bat|ps1|exe)$/i, ''); - if (ARGV0_TARGET_MAP[binName]) { - return ARGV0_TARGET_MAP[binName]; - } - - // 4. Check profile config - if (profileConfig?.target) { - // Persisted targets are validated before profile configuration is saved. - return profileConfig.target; - } - - // 5. Default to claude - return 'claude'; -} -``` - ---- - -## Claude Adapter - -### Implementation - -```typescript -// src/targets/claude-adapter.ts - -export class ClaudeAdapter implements TargetAdapter { - readonly type: TargetType = 'claude'; - readonly displayName = 'Claude Code'; - - detectBinary(): TargetBinaryInfo | null { - const info = getClaudeCliInfo(); - if (!info) return null; - return { path: info.path, needsShell: info.needsShell }; - } - - async prepareCredentials(_creds: TargetCredentials): Promise { - // No-op: Claude receives credentials via environment variables - } - - buildArgs(_profile: string, userArgs: string[]): string[] { - return userArgs; // Pass through user arguments unchanged - } - - buildEnv(creds: TargetCredentials, profileType: string): NodeJS.ProcessEnv { - const webSearchEnv = getWebSearchHookEnv(); - - // For native profiles, strip stale proxy env to prevent interference - const baseEnv = - profileType === 'account' || profileType === 'default' - ? stripAnthropicEnv(process.env) - : process.env; - - const env: NodeJS.ProcessEnv = { ...baseEnv, ...webSearchEnv }; - - if (creds.envVars) { - Object.assign(env, creds.envVars); - } - - // Deliver credentials via environment variables - if (creds.baseUrl) env['ANTHROPIC_BASE_URL'] = creds.baseUrl; - if (creds.apiKey) env['ANTHROPIC_AUTH_TOKEN'] = creds.apiKey; - if (creds.model) env['ANTHROPIC_MODEL'] = creds.model; - - return env; - } - - exec(args: string[], env: NodeJS.ProcessEnv, _options?: { cwd?: string }): void { - const claudeCli = detectClaudeCli(); - if (!claudeCli) { - void ErrorManager.showClaudeNotFound(); - process.exit(1); - return; - } - - // Handle Windows shell requirements - const isWindows = process.platform === 'win32'; - const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli); - - let child: ChildProcess; - if (needsShell) { - const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' '); - child = spawn(cmdString, { shell: true, stdio: 'inherit', env }); - } else { - child = spawn(claudeCli, args, { stdio: 'inherit', env }); - } - - // Handle process termination - const onSigInt = () => child.kill('SIGINT'); - const onSigTerm = () => child.kill('SIGTERM'); - process.once('SIGINT', onSigInt); - process.once('SIGTERM', onSigTerm); - child.on('exit', () => { - process.removeListener('SIGINT', onSigInt); - process.removeListener('SIGTERM', onSigTerm); - }); - } - - supportsProfileType(profileType: string): boolean { - // Claude supports all profile types - return true; - } -} -``` - -Native Claude launches keep user arguments session-scoped. The launch layer validates and normalizes -`--effort low|medium|high|xhigh|max` before spawning Claude, then passes it through without writing -to Claude or CCS configuration. CLIProxy-backed Claude launches still treat `--effort` as the CCS -thinking alias handled by CLIProxy. - -### Credential Delivery - -**Method**: Environment variables - -```bash -export ANTHROPIC_BASE_URL=https://api.anthropic.com -export ANTHROPIC_AUTH_TOKEN=sk-ant-... -export ANTHROPIC_MODEL=claude-opus-4-6 -export WEBSEARCH_HOOK_ENV=... # Image analysis, websearch -``` - -### Execution - -```bash -# Direct invocation -ccs codex -→ claude "args..." - with ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN set - -# With --target override -ccs --target claude glm -→ claude "args..." - with ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN set -``` - ---- - -## Droid Adapter - -### Implementation - -```typescript -// src/targets/droid-adapter.ts - -export class DroidAdapter implements TargetAdapter { - readonly type: TargetType = 'droid'; - readonly displayName = 'Factory Droid'; - - detectBinary(): TargetBinaryInfo | null { - const info = getDroidBinaryInfo(); - if (!info) return null; - - // Non-blocking version compatibility check - checkDroidVersion(info.path); - return info; - } - - async prepareCredentials(creds: TargetCredentials): Promise { - // Write custom model entry to ~/.factory/settings.json - await upsertCcsModel(creds.profile, { - model: creds.model || 'claude-opus-4-6', - displayName: `CCS ${creds.profile}`, - baseUrl: creds.baseUrl, - apiKey: creds.apiKey, - provider: creds.provider || 'anthropic', - }); - } - - buildArgs(profile: string, userArgs: string[]): string[] { - // Droid uses -m syntax for model selection - return ['-m', `custom:ccs-${profile}`, ...userArgs]; - } - - buildEnv(_creds: TargetCredentials, _profileType: string): NodeJS.ProcessEnv { - // Droid reads from config file — minimal env needed - return { ...process.env }; - } - - exec(args: string[], env: NodeJS.ProcessEnv, _options?: { cwd?: string }): void { - const droidPath = detectDroidCli(); - if (!droidPath) { - console.error('[X] Droid CLI not found. Install: npm i -g @factory/cli'); - process.exit(1); - return; - } - - // Handle Windows shell requirements - const isWindows = process.platform === 'win32'; - const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(droidPath); - - let child: ChildProcess; - if (needsShell) { - const cmdString = [droidPath, ...args].map(escapeShellArg).join(' '); - child = spawn(cmdString, { shell: true, stdio: 'inherit', env }); - } else { - child = spawn(droidPath, args, { stdio: 'inherit', env }); - } - - // Handle process termination - const onSigInt = () => child.kill('SIGINT'); - const onSigTerm = () => child.kill('SIGTERM'); - process.once('SIGINT', onSigInt); - process.once('SIGTERM', onSigTerm); - child.on('exit', () => { - process.removeListener('SIGINT', onSigInt); - process.removeListener('SIGTERM', onSigTerm); - }); - } - - supportsProfileType(profileType: string): boolean { - // Droid currently supports direct settings/default paths only - return profileType === 'settings' || profileType === 'default'; - } -} -``` - -### Credential Delivery - -**Method**: Config file (`~/.factory/settings.json`) - -```json -{ - "customModels": [ - { - "model": "claude-opus-4-6", - "displayName": "CCS gemini", - "baseUrl": "https://generativelanguage.googleapis.com/v1beta/openai/", - "apiKey": "AIza...", - "provider": "openai" - }, - { - "model": "glm-4", - "displayName": "CCS glm", - "baseUrl": "https://open.bigmodel.cn/api/paas/v4/", - "apiKey": "your-glm-key", - "provider": "openai" - } - ] -} -``` - -### Execution - -```bash -# Direct invocation -ccs codex -→ droid -m custom:ccs-codex "args..." - (credentials loaded from ~/.factory/settings.json) - -# With --target override -ccs --target droid glm -→ droid -m custom:ccs-glm "args..." - (credentials loaded from ~/.factory/settings.json) -``` - -### Runtime Alias Pattern - -```bash -# Built-in package bin aliases -ccs-droid glm -→ Target: droid (forced by runtime alias) -→ droid -m custom:ccs-glm "args..." - -# Legacy shortcut still works -ccsd glm -→ Target: droid (forced by runtime alias) -→ droid -m custom:ccs-glm "args..." -``` - -On Windows, `ccs-droid.cmd`, `ccsd.cmd`, `ccsd.bat`, `ccsd.ps1`, and `ccsd.exe` wrappers are also recognized. - -Additional alias names can be configured at runtime after you create a matching -symlink or another launcher that preserves the invoked basename. Use `CCS_TARGET_ALIASES` (preferred, -`target=alias1,alias2;...`) or legacy `CCS_DROID_ALIASES` (comma-separated). -Example: - -```bash -ln -s /path/to/ccs /path/to/mydroid -CCS_TARGET_ALIASES=droid=mydroid -``` - ---- - -## Codex Adapter - -### Implementation - -The Codex adapter keeps CCS-backed Codex launches transient. It does not rewrite -`~/.codex/config.toml`. Instead it: - -- passes through native default Codex sessions unchanged -- probes the installed Codex binary for `--config ` support -- injects CCS-backed provider credentials through temporary `-c` overrides -- stores the routed API key only in process env via `CCS_CODEX_API_KEY` - -```typescript -// src/targets/codex-adapter.ts - -export class CodexAdapter implements TargetAdapter { - readonly type: TargetType = 'codex'; - readonly displayName = 'Codex CLI'; - - detectBinary(): TargetBinaryInfo | null { - return getCodexBinaryInfo(); - } - - async prepareCredentials(_creds: TargetCredentials): Promise { - // No file writes. Codex uses transient -c overrides plus env_key injection. - } - - buildArgs(profile: string, userArgs: string[], options?: BuildOptions): string[] { - if ((options?.profileType || 'default') === 'default') { - return userArgs; - } - - if (!codexBinarySupportsConfigOverrides(options?.binaryInfo)) { - throw new Error('Upgrade Codex before using CCS-backed Codex profiles.'); - } - - return [ - '-c', - 'model_provider=\"ccs_runtime\"', - '-c', - 'model_providers.ccs_runtime.base_url=\"http://127.0.0.1:8317/api/provider/codex\"', - '-c', - 'model_providers.ccs_runtime.env_key=\"CCS_CODEX_API_KEY\"', - '-c', - 'model_providers.ccs_runtime.wire_api=\"responses\"', - ...userArgs, - ]; - } - - buildEnv(creds: TargetCredentials, profileType: string): NodeJS.ProcessEnv { - const env = { ...stripAnthropicEnv(process.env) }; - if (profileType !== 'default') { - env['CCS_CODEX_API_KEY'] = creds.apiKey; - } - return env; - } -} -``` - -### Support Matrix - -Codex is a real runtime target, but it is intentionally narrower than Claude or Droid in v1: - -| Profile Type | Codex Target | Notes | -|--------------|--------------|-------| -| `default` | Yes | Uses existing native Codex auth/config | -| `cliproxy` provider=`codex` | Yes | Routed through CLIProxy Codex Responses bridge | -| `cliproxy` composite | No | Not proven native-Codex-safe | -| `settings` with Codex bridge metadata | Yes | Only when the API profile resolves to a Codex CLIProxy bridge | -| `settings` generic API profile | No | Claude/Droid only | -| `account` | No | Claude-only account isolation concept | -| `copilot` | No | Not a native Codex provider path | - -### Codex Dashboard Surface - -CCS also exposes a dedicated dashboard route at `ccs config` -> `Compatible` -> `Codex CLI`. -That page is intentionally narrower than the Droid dashboard in overall scope, but it is no -longer read-mostly: - -- reads and writes only the user config layer: `~/.codex/config.toml` or `$CODEX_HOME/config.toml` -- provides guided controls for top-level settings, project trust, profiles, model providers, - MCP servers, and supported feature flags -- keeps a raw `config.toml` editor as the escape hatch for unsupported or fidelity-sensitive edits -- shows binary detection, user-layer config summaries, support-matrix guidance, and upstream docs -- normalizes TOML formatting and drops comments on structured saves -- keeps structured controls disabled while raw TOML is dirty or invalid, validates project trust - paths as absolute or `~/...`, and lets feature flags reset back to Codex defaults -- warns that transient CCS runtime overrides such as `codex -c key=value` and - `CCS_CODEX_API_KEY` can change the effective runtime without persisting into the file editor - -This keeps the dashboard honest about Codex's merged configuration model while still giving users -one place to inspect and manage the user-owned layer safely. - -### Runtime Entrypoints and argv[0] Fallback - -```bash -# Built-in package bin entrypoints -ccs-codex -→ dist/bin/codex-runtime.js -→ CCS_INTERNAL_ENTRY_TARGET=codex - -ccsx -→ dist/bin/codex-runtime.js -→ CCS_INTERNAL_ENTRY_TARGET=codex -→ passes native Codex diagnostics plus known upstream Codex subcommands and aliases through before CCS profile detection -→ reserves CCS-owned `auth`, `doctor`, and `update` - -ccsxp -→ dist/bin/ccsxp-runtime.js -→ CCS_INTERNAL_ENTRY_TARGET=codex -→ injects native `model_provider="cliproxy"` override -→ pins CODEX_HOME to native `~/.codex` unless `CCSXP_CODEX_HOME` is set -→ repairs `[model_providers.cliproxy]` in the active Codex `config.toml` -→ preserves valid custom `base_url` values for remote or non-default CLIProxy endpoints -→ injects the effective CCS CLIProxy auth token into the provider's configured `env_key` -→ ignores the configured CCS default account/profile and stays in native Codex default mode -``` - -If a user launches CCS through a custom shim instead of the built-in package bins, target -resolution falls back to `argv[0]` aliases from `CCS_TARGET_ALIASES` or legacy -`CCS_CODEX_ALIASES`: - -```bash -ln -s /path/to/ccs /path/to/mycodex -CCS_TARGET_ALIASES='codex=mycodex' -# Legacy fallback: -CCS_CODEX_ALIASES='mycodex' -``` - ---- - -## Registry and Lookup - -The target registry is a simple map-based store for adapters: - -```typescript -// src/targets/target-registry.ts - -const adapters = new Map(); - -export function registerTarget(adapter: TargetAdapter): void { - adapters.set(adapter.type, adapter); -} - -export function getTarget(type: TargetType): TargetAdapter { - const adapter = adapters.get(type); - if (!adapter) { - throw new Error(`Unknown target "${type}"`); - } - return adapter; -} - -export function getDefaultTarget(): TargetAdapter { - return getTarget('claude'); -} -``` - -### Adapter Registration - -At startup, adapters self-register: - -```typescript -// src/ccs.ts (initialization) - -registerTarget(new ClaudeAdapter()); -registerTarget(new DroidAdapter()); -registerTarget(new CodexAdapter()); -``` - ---- - -## Execution Flow - -### Step-by-Step - -``` -1. Parse command-line arguments - └─ args: ['--target', 'droid', 'glm'] - -2. Resolve target type - └─ resolveTargetType(args) → 'droid' - └─ stripTargetFlag(args) → ['glm'] - -3. Detect and resolve profile - └─ detectProfile(['glm']) → { profile: 'glm', ... } - └─ Load credentials from config/CLIProxy/env - -4. Build credentials object - └─ TargetCredentials { - baseUrl: '...', - apiKey: '...', - model: 'claude-opus-4-6', - envVars: { CCS_PROFILE_NAME: 'glm', ... } - } - -5. Get target adapter - └─ getTarget('droid') → DroidAdapter instance - -6. Prepare credentials - └─ adapter.prepareCredentials(creds) - └─ DroidAdapter: writes to ~/.factory/settings.json - -7. Build spawn arguments - └─ adapter.buildArgs('glm', []) → ['-m', 'custom:ccs-glm'] - -8. Build environment - └─ adapter.buildEnv(creds, profileType) → process.env - -9. Spawn target CLI - └─ adapter.exec(spawnArgs, env) - └─ exec spawn('droid', ['-m', 'custom:ccs-glm', ...]) - -10. Replace current process - └─ Child process inherits stdio - └─ Signal handlers propagate to child -``` - ---- - -## Adding a New Target - -To support a new CLI (e.g., MyAI CLI), follow this pattern: - -### 1. Create Adapter Class - -```typescript -// src/targets/myai-adapter.ts - -export class MyAiAdapter implements TargetAdapter { - readonly type: TargetType = 'myai'; - readonly displayName = 'MyAI CLI'; - - detectBinary(): TargetBinaryInfo | null { - const path = which.sync('myai', { nothrow: true }); - if (!path) return null; - return { path, needsShell: process.platform === 'win32' }; - } - - async prepareCredentials(creds: TargetCredentials): Promise { - // Write to ~/.myai/config or similar - } - - buildArgs(profile: string, userArgs: string[]): string[] { - return ['-p', profile, ...userArgs]; - } - - buildEnv(creds: TargetCredentials, _profileType: string): NodeJS.ProcessEnv { - return { - ...process.env, - MYAI_API_KEY: creds.apiKey, - MYAI_API_URL: creds.baseUrl, - }; - } - - exec(args: string[], env: NodeJS.ProcessEnv): void { - const myaiPath = this.detectBinary()?.path; - if (!myaiPath) { - console.error('[X] MyAI CLI not found'); - process.exit(1); - } - spawn(myaiPath, args, { stdio: 'inherit', env }); - } - - supportsProfileType(profileType: string): boolean { - return true; // or implement specific logic - } -} -``` - -### 2. Update Type Definition - -```typescript -// src/targets/target-adapter.ts - -export type TargetType = 'claude' | 'droid' | 'codex' | 'myai'; -``` - -### 3. Register in ccs.ts - -```typescript -registerTarget(new MyAiAdapter()); -``` - -### 4. Update Documentation - -- Add to [Codebase Summary](../codebase-summary.md) -- Update Code Standards adapter examples -- Document CLI-specific behavior - ---- - -## Cross-Platform Considerations - -### Windows Shell Detection - -Both adapters check for shell-requiring binaries: - -```typescript -const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(binaryPath); - -if (needsShell) { - const cmdString = [binaryPath, ...args].map(escapeShellArg).join(' '); - spawn(cmdString, { shell: true, stdio: 'inherit' }); -} else { - spawn(binaryPath, args, { stdio: 'inherit' }); -} -``` - -### Environment Variable Escaping - -Arguments passed to shell are escaped to prevent injection: - -```typescript -export function escapeShellArg(arg: string): string { - // Wrap in quotes and escape internal quotes - return `"${arg.replace(/"/g, '\\"')}"`; -} -``` - -### Signal Handling - -Both adapters propagate signals from parent to child: - -```typescript -const onSigInt = () => child.kill('SIGINT'); -const onSigTerm = () => child.kill('SIGTERM'); -process.once('SIGINT', onSigInt); -process.once('SIGTERM', onSigTerm); - -child.on('exit', () => { - process.removeListener('SIGINT', onSigInt); - process.removeListener('SIGTERM', onSigTerm); -}); -``` - -This ensures CTRL+C and graceful shutdowns work correctly. - ---- - -## Testing Target Adapters - -### Unit Tests - -```typescript -describe('ClaudeAdapter', () => { - it('detects Claude CLI', () => { - const adapter = new ClaudeAdapter(); - const binary = adapter.detectBinary(); - expect(binary).not.toBeNull(); - }); - - it('builds env with credentials', () => { - const adapter = new ClaudeAdapter(); - const env = adapter.buildEnv({ - baseUrl: 'https://api.anthropic.com', - apiKey: 'sk-ant-...', - model: 'claude-opus-4-6', - }, 'cliproxy'); - - expect(env['ANTHROPIC_AUTH_TOKEN']).toBe('sk-ant-...'); - }); -}); -``` - -### Integration Tests - -```bash -# Test Claude adapter -ccs --target claude help - -# Test Droid adapter (if installed) -ccs --target droid help - -# Test Codex adapter (if installed) -ccs --target codex -ccs-codex -ccsxp - -# Test argv[0] detection -ccs-droid help -ccsx -``` - ---- - -## Related Documentation - -- [Codebase Summary](../codebase-summary.md) — Module structure -- [Code Standards](../code-standards.md) — Adapter pattern guidelines -- [System Architecture Index](./index.md) — Overall system design +[`src/targets/target-resolver.ts`](../../src/targets/target-resolver.ts) selects +the runtime in this order: + +1. `--target ` or `--target=`; if repeated, the last flag wins. +2. A trusted package runtime entrypoint identified by + `CCS_INTERNAL_ENTRY_TARGET`. +3. The invoked binary name, including built-in or configured aliases. +4. A persisted per-profile target. +5. `claude`. + +All `--target` flags are removed before the remaining arguments reach the +runtime. Parsing stops at the `--` option terminator. + +The built-in runtime aliases are derived from target metadata: + +| Target | Built-in aliases | +| --- | --- | +| Claude Code | Base `ccs` command | +| Factory Droid | `ccs-droid`, `ccsd` | +| Codex CLI | `ccs-codex`, `ccsx`, `ccsxp` | + +Custom aliases use `CCS_TARGET_ALIASES` with entries such as +`droid=team-droid;codex=team-codex`. The target-specific legacy environment +variables remain compatibility inputs. Alias values are validated and cannot +replace reserved package binary names. + +## Runtime Compatibility + +Adapter-level checks are deliberately conservative. Flow-specific compatibility +is evaluated in +[`src/targets/target-runtime-compatibility.ts`](../../src/targets/target-runtime-compatibility.ts), +which has the provider and bridge context needed for an accurate decision. + +| Profile flow | Claude | Droid | Codex | +| --- | --- | --- | --- | +| Native default | Supported | Conditional: requires resolved `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN` | Supported | +| Settings/API profile | Supported | Supported | Codex CLIProxy bridge only | +| CLIProxy profile | Supported | Supported | Non-composite `codex` provider only | +| Claude account | Supported | Not supported | Not supported | +| Copilot | Supported | Not supported | Not supported | +| Cursor local proxy | Supported | Not supported | Not supported | + +When changing compatibility, update both the runtime evaluator and its focused +tests. The authoritative coverage is in +[`tests/unit/targets/target-runtime-compatibility.test.ts`](../../tests/unit/targets/target-runtime-compatibility.test.ts). + +The compatibility evaluator accepts Droid's native-default flow, but adapter +preparation still validates the resolved credentials. A default Droid launch +cannot proceed without both a non-empty base URL and auth token. + +## Credential and Configuration Boundaries + +### Claude Code + +[`src/targets/claude-adapter.ts`](../../src/targets/claude-adapter.ts) delivers +resolved provider values through the child environment. Native account and +default launches remove stale Anthropic routing variables before execution. +Browser and WebSearch launch preparation may add runtime-specific arguments and +environment values. + +The adapter does not persist provider credentials to Claude settings. + +### Factory Droid + +[`src/targets/droid-adapter.ts`](../../src/targets/droid-adapter.ts) validates +the resolved base URL and token, then delegates the target-owned write to +[`src/targets/droid-config-manager.ts`](../../src/targets/droid-config-manager.ts). +That manager updates the CCS custom model and active model in Factory settings. +The adapter passes user arguments through; it does not inject a `-m` selector. + +Factory settings are a persistent user-owned surface. Writes must preserve +unrelated settings and use the configuration manager rather than direct JSON +replacement. + +### Codex CLI + +[`src/targets/codex-adapter.ts`](../../src/targets/codex-adapter.ts) keeps normal +CCS-backed launches transient: + +- native default sessions keep native Codex authentication and configuration; +- CCS-backed sessions use `-c key=value` overrides for a Responses-compatible + runtime provider; +- the resolved API key is supplied through the provider's environment key, not + on the command line; +- stale Anthropic and nested Codex-session variables are removed before spawn. + +The `ccsxp` shortcut is the explicit exception. It may repair the dedicated +`cliproxy` provider block in the active Codex configuration through +[`src/targets/codex-cliproxy-provider-config.ts`](../../src/targets/codex-cliproxy-provider-config.ts). +That repair preserves a valid custom base URL and the configured environment-key +name. General Codex launches must not rewrite `config.toml`. + +## Execution Invariants + +The dispatcher owns the order of operations: + +1. parse CCS-owned arguments and resolve the target; +2. resolve the profile and provider credentials; +3. evaluate target/profile/provider compatibility; +4. detect the target binary; +5. call `prepareCredentials`; +6. build target arguments and environment; +7. execute the child runtime. + +Flow implementations live under +[`src/dispatcher/flows/`](../../src/dispatcher/flows/). Shared target execution +logic lives in +[`src/dispatcher/target-executor.ts`](../../src/dispatcher/target-executor.ts). + +All adapters must preserve these invariants: + +- user credentials never appear in documented examples, logs, or spawn + arguments; +- target-owned persistent writes are explicit and scoped; +- stale routing variables from another runtime do not leak into the child; +- Windows wrapper handling does not use a shell unless the wrapper format + requires one; +- child signals and exit behavior propagate to the CCS process; +- binary and launch failures run registered cleanup before exit. + +## Adding or Changing a Target + +Change the smallest complete set: + +1. update `TargetType` and the adapter contract only when required; +2. add target metadata and aliases in `target-metadata.ts`; +3. implement and register the adapter through `src/targets/index.ts`; +4. add flow-aware compatibility rules; +5. add focused resolver, adapter, compatibility, and integration tests; +6. update CLI help and dashboard controls if the target becomes user + configurable. + +Start with these test suites: + +- [`tests/unit/targets/target-resolver.test.ts`](../../tests/unit/targets/target-resolver.test.ts) +- [`tests/unit/targets/target-registry.test.ts`](../../tests/unit/targets/target-registry.test.ts) +- [`tests/unit/targets/target-runtime-compatibility.test.ts`](../../tests/unit/targets/target-runtime-compatibility.test.ts) +- target-specific tests under + [`tests/unit/targets/`](../../tests/unit/targets/) + +Do not describe a target as supported from adapter registration alone. Support +requires a compatible profile flow, safe credential delivery, binary detection, +execution behavior, and tests for the full combination. From 51e806299564da03ac8ce98cad35cc962ec01078 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 26 Jul 2026 09:35:10 -0400 Subject: [PATCH 3/3] docs(operations): refresh runtime contracts --- docs/i18n-dashboard.md | 29 +++-- docs/logging-contract.md | 243 ++++++++++++++++--------------------- docs/websearch.md | 252 +++++++++++++++++++-------------------- 3 files changed, 244 insertions(+), 280 deletions(-) diff --git a/docs/i18n-dashboard.md b/docs/i18n-dashboard.md index 618e18ba..7caa416a 100644 --- a/docs/i18n-dashboard.md +++ b/docs/i18n-dashboard.md @@ -1,7 +1,5 @@ # Dashboard i18n Guide -Last Updated: 2026-04-14 - This document describes the internationalization (i18n) architecture used by the CCS Dashboard (`ui/`), how locale selection works, and how to add new languages safely. --- @@ -15,6 +13,7 @@ Dashboard i18n currently covers UI text rendered by React components. - `zh-CN` (Simplified Chinese) - `vi` (Vietnamese) - `ja` (Japanese) + - `ko` (Korean) - Locale state is persisted in browser localStorage using `ccs-ui-locale`. - Fallback language is `en`. @@ -40,6 +39,7 @@ Out of scope: - `zh-CN.translation` - `vi.translation` - `ja.translation` + - `ko.translation` - Uses `initReactI18next` for React integration. ### Locale utilities @@ -59,8 +59,11 @@ Out of scope: ### Test bootstrap - File: `ui/tests/setup/vitest-setup.ts` -- Test setup must import `ui/src/lib/i18n.ts` so direct `useTranslation()` consumers resolve the same singleton instance as the app. -- If a test mocks `useTranslation()`, keep the mocked key surface aligned with the component output or the assertions will drift. +- Test setup must import `ui/src/lib/i18n.ts` so components using the + react-i18next translation hook resolve the same singleton instance as the + app. +- If a test mocks the translation hook, keep the mocked key surface aligned + with the component output or the assertions will drift. --- @@ -104,11 +107,8 @@ When adding a locale such as Thai (`th`): 5. Run UI validation and i18n tests: - `cd ui && bun run validate` - `cd ui && bun run test:run tests/unit/ui/i18n/language-switcher.test.tsx` -6. Add or update key-parity tests to catch locale drift. - -Current issue driving the Vietnamese rollout: - -- https://github.com/kaitranntt/ccs/issues/659 +6. Keep the key-parity test passing. It compares every non-English resource + against English and verifies interpolation placeholders match. --- @@ -121,3 +121,14 @@ Before opening a PR that touches i18n: - [ ] No unsafe HTML injection path introduced for translated content. - [ ] `ui` validate/test commands pass. - [ ] This document is updated if architecture or conventions changed. + +## References + +- [`ui/src/lib/locales.ts`](../ui/src/lib/locales.ts) - supported locale ids, + normalization, persistence, and formatting locale +- [`ui/src/lib/i18n.ts`](../ui/src/lib/i18n.ts) - translation resources and + i18next initialization +- [`ui/src/components/layout/language-switcher.tsx`](../ui/src/components/layout/language-switcher.tsx) + - locale selection UI +- [`ui/tests/unit/ui/i18n/language-switcher.test.tsx`](../ui/tests/unit/ui/i18n/language-switcher.test.tsx) + - selection, persistence, key-parity, and placeholder coverage diff --git a/docs/logging-contract.md b/docs/logging-contract.md index b51c68a9..99c05a1b 100644 --- a/docs/logging-contract.md +++ b/docs/logging-contract.md @@ -1,178 +1,139 @@ # Logging Contract -Single source of truth for structured backend logging in CCS CLI. Companion to GitHub issues #1138 (umbrella) and #1141 (backend instrumentation). +CCS structured logs are a machine-readable JSONL channel for backend and +runtime events. They are separate from terminal UX output and must never be +used as a substitute for user-facing recovery messages. -## Overview +The canonical schema is +[`src/services/logging/log-types.ts`](../src/services/logging/log-types.ts). +Defaults are defined in +[`src/config/schemas/logging.ts`](../src/config/schemas/logging.ts). -CCS emits structured JSONL log entries for backend behavior (proxy daemons, OAuth flows, target spawn lifecycle, executor errors, etc.). This document defines the canonical schema, request-correlation pattern, lifecycle stages, and redaction policy. +## Entry Schema -> CLI text output (`ok / info / warn / fail` from `src/utils/ui.ts`) is **NOT** affected by this contract. Logs are a separate channel — never printed to stdout/stderr. +| Field | Type | Required | Contract | +| --- | --- | --- | --- | +| `id` | `string` | yes | Unique entry id. | +| `timestamp` | `string` | yes | ISO 8601 emission time. | +| `level` | `error`, `warn`, `info`, or `debug` | yes | Severity. | +| `source` | `string` | yes | Stable module-scoped producer id. | +| `event` | `string` | yes | Stable machine-readable event name. | +| `message` | `string` | yes | Short human-readable summary. | +| `processId` | `number` | yes | Emitting process id. | +| `runId` | `string` | yes | Stable for the current process. | +| `context` | object | no | Event fields after configured redaction. | +| `requestId` | `string` | no | Cross-stage correlation id. | +| `stage` | `LogStage` | no | Canonical lifecycle stage. | +| `latencyMs` | `number` | no | Elapsed milliseconds, normally at completion. | +| `error` | `LogErrorInfo` | no | Structured error metadata. | -## Schema (`LogEntry`) - -Defined in `src/services/logging/log-types.ts`. - -| Field | Type | Required | Notes | -|-------|------|----------|-------| -| `id` | `string` | yes | UUID per entry. | -| `timestamp` | `string` | yes | ISO 8601. | -| `level` | `'error'\|'warn'\|'info'\|'debug'` | yes | | -| `source` | `string` | yes | Module-scoped identifier (e.g. `proxy:openai-compat:messages`). | -| `event` | `string` | yes | Dotted machine-readable event name (e.g. `request.received`). | -| `message` | `string` | yes | Human-readable summary. | -| `processId` | `number` | yes | `process.pid`. | -| `runId` | `string` | yes | Stable per-process id. | -| `context` | `object` | no | Free-form structured fields (redacted). | -| `requestId` | `string` | no | Correlates entries belonging to one inbound request across stages. | -| `stage` | `LogStage` | no | Lifecycle stage tag. | -| `latencyMs` | `number` | no | Elapsed ms (typically on `respond` / `cleanup`). | -| `error` | `{name, message, code?, stack?}` | no | Structured error metadata; never raw token strings. | - -Old free-form entries (no `requestId` / `stage`) are still valid; new fields are additive. - -### Example - -```jsonl -{"id":"...","timestamp":"2026-04-30T12:34:56.000Z","level":"info","source":"proxy:openai-compat:messages","event":"request.received","message":"Proxy /v1/messages request received","processId":42,"runId":"r1","requestId":"a1b2...","stage":"intake","context":{"method":"POST"}} -``` +Additive optional fields preserve compatibility with older readers. Consumers +must not assume every event has a stage, request id, latency, or structured +error. ## Lifecycle Stages -`LogStage` is one of: +Use `logger.stage()` for events that map to the canonical lifecycle: -| Stage | When to emit | -|-------|--------------| -| `intake` | Inbound request received at an entry edge (HTTP handler, CLI dispatch). | -| `route` | Destination/profile/target resolution. | -| `auth` | Authentication / authorization (token exchange, profile auth). | -| `dispatch` | Outbound request prepared / child process spawned. | -| `upstream` | Upstream call in flight (provider HTTP / spawned child running). | -| `transform` | Payload translation (request/response shape conversion). | -| `respond` | Response written / dispatched (`latencyMs` typically populated). | -| `cleanup` | Error path, abort, teardown. | +| Stage | Meaning | +| --- | --- | +| `intake` | Request or command entered a CCS boundary. | +| `route` | Profile, provider, target, or destination resolution. | +| `auth` | Authentication or authorization work. | +| `dispatch` | Outbound request or child launch prepared. | +| `upstream` | Provider request or child operation in flight. | +| `transform` | Request or response translation. | +| `respond` | Result dispatched to the caller. | +| `cleanup` | Failure, abort, or teardown path. | -Stages may be skipped or repeated. Streaming responses tag `upstream` only at start/end (NOT per chunk). +Stages can be skipped or repeated. Use `logger.info()`, `warn()`, or `error()` +for events that do not represent a lifecycle stage. High-volume details, +including streaming chunk metrics, belong at `debug`. -## RequestId Propagation (AsyncLocalStorage) - -`requestId` is propagated implicitly via Node `AsyncLocalStorage`. Entry edges wrap their handler in `withRequestContext`; every `createLogger`-emitted entry inside the context auto-merges `requestId` from the active store. +Example: ```ts -import { withRequestContext, createLogger } from './services/logging'; - -const logger = createLogger('proxy:my-edge'); - -http.createServer((req, res) => { - const requestId = req.headers['x-ccs-request-id'] ?? randomUUID(); - res.setHeader('x-ccs-request-id', requestId); - withRequestContext({ requestId }, async () => { - logger.stage('intake', 'request.received', 'inbound'); - // ... downstream work emits with the same requestId - }); +logger.stage('respond', 'request.completed', 'Request completed', { + statusCode: 200, +}, { + latencyMs: 42, }); ``` -### Cross-daemon header +See [`src/services/logging/logger.ts`](../src/services/logging/logger.ts) for the +compiler-checked signature. -`x-ccs-request-id` round-trips across the proxy edge: -- Inbound: if the header is present and matches the UUID-ish guard (`/^[A-Za-z0-9._-]{8,128}$/`), it is reused; otherwise a fresh UUID is minted. -- Outbound (response): the resolved id is echoed back via `res.setHeader('x-ccs-request-id', ...)`. -- When CCS calls another daemon (copilot, cursor, glmt), forward the active id in the same header so that daemon can correlate. +## Request Correlation -### Ordering guarantee +[`src/services/logging/log-context.ts`](../src/services/logging/log-context.ts) +uses Node async-local storage within a process. Entry edges establish a +context; loggers created downstream read its `requestId` automatically. -Emit-time ordering of entries within a single `requestId` is monotonic — the active context is single-threaded relative to the request, so `timestamp` ordering reflects emit order. The UI layer (#1142) consumes this guarantee. +Async-local context does not cross child processes or worker threads. A child +process can inherit the active id through `CCS_REQUEST_ID` and establish a new +local context. HTTP edges may use `x-ccs-request-id`; each edge owns whether it +accepts an incoming id or mints a new one. -### What NOT to put in the context +Only the characters and length accepted by `REQUEST_ID_PATTERN` are valid for +forwarded ids. Do not treat request ids as authentication or authorization +material. -The ALS context object is mixed into every downstream entry. Never store: -- Raw tokens, API keys, refresh tokens, OAuth codes -- Raw request/response bodies -- User-supplied secrets +There is no global ordering guarantee across concurrent async work or processes. +Use `timestamp`, stage, event, and process/run identifiers together when +reconstructing a request. -Only benign correlation metadata: `requestId`, `method`, `path`, `command`, `profile`. +## Redaction and Data Minimization -### Worker threads / spawned children +[`src/services/logging/log-redaction.ts`](../src/services/logging/log-redaction.ts) +is the implementation source of truth. With the default `logging.redact: true`, +the logger: -ALS context is **not** inherited by worker threads or `child_process.spawn` stdio pipes. At those boundaries, mint a fresh `requestId` at the child entry and pass the parent id explicitly via env var or header for correlation. +- replaces values under known credential-bearing keys; +- masks common authorization schemes and credential token shapes in strings; +- redacts sensitive CLI flag values, including inline assignments; +- limits string length and nested object depth; +- strips an `Error` object to safe structured fields. -## Redaction +The matcher evolves as credential surfaces change. Link to the implementation +instead of copying its complete key or token-pattern list into other docs. -`src/services/logging/log-redaction.ts` is the single source of truth. +Redaction is defense in depth, not permission to log sensitive data. Never log: -### Sensitive key matcher +- tokens, passwords, cookies, OAuth codes, or authorization values; +- raw request or response bodies; +- raw prompts or prompt-bearing CLI arguments; +- personal identifiers when a non-identifying account or profile label works. -`SENSITIVE_KEY_PATTERN` matches (case-insensitive, with `_` / `-` / camelCase variants): -`authorization`, `proxy-authorization`, `cookie`, `set-cookie`, `password`, `password_hash`, `secret`, `client_secret`, `token`, `auth_token`, `access_token`, `refresh_token`, `id_token`, `bearer`, `assertion`, `api_key`, `x-api-key`, `x-goog-api-key`, `management_key`, `copilot_token`, `cursor_session_key`, `oauth_code`, `auth_code`. +When a new secret-bearing field or flag is introduced, update the redactor and +its tests under +[`tests/unit/services/logging/`](../tests/unit/services/logging/). -String/object values for matching keys are replaced with `[redacted]`. Numeric/boolean values pass through (e.g., `expires_at` epoch numbers stay readable). +## Error Codes and Process Exit Codes -### Auth-scheme value masking +These are separate contracts: -Raw string values whose prefix matches `^(Bearer|Basic|Token)\s+\S+` are rewritten to ` [redacted]` even when nested under non-sensitive keys. +- `LogEntry.error.code` is an optional **string** supplied as part of structured + error metadata. It can carry a runtime, system, or provider error identifier. +- `CCSError.code` is a numeric `ExitCode`. The centralized CLI error handler + writes it as `context.exitCode` and passes it to `process.exit`. -### Argv redaction +Do not put a numeric process exit code in `LogEntry.error.code`, and do not make +log consumers derive process status from that string field. -`redactArgv(argv)` redacts the value following any sensitive flag (`--token`, `--api-key`, `--auth`, `--bearer`, `--secret`, `--client-secret`, `--access-token`, `--refresh-token`, `--id-token`, `--password`). +The numeric mapping lives in +[`src/errors/exit-codes.ts`](../src/errors/exit-codes.ts). Typed error +assignments live in +[`src/errors/error-types.ts`](../src/errors/error-types.ts), and propagation is +implemented by +[`src/errors/error-handler.ts`](../src/errors/error-handler.ts). -### Adding new sensitive keys +## Configuration -1. Extend `SENSITIVE_KEY_PATTERN` in `src/services/logging/log-redaction.ts`. -2. Add a unit test in `tests/unit/services/logging/log-redaction-extended.test.ts`. -3. Verify regex stays O(1) per key (no catastrophic backtracking). +CCS-owned logging uses the `logging` section in the unified config. Its defaults +enable logging and redaction at `info` level with bounded rotation, retention, +and live-buffer settings. This is distinct from `cliproxy.logging`, which +controls CLIProxy runtime logging. -## Contributor Guide - -### When to use `logger.stage()` vs `logger.info()` - -Use `stage()` whenever the entry corresponds to one of the canonical lifecycle stages — this is what observability tooling and the dashboard rely on. Use `info()` / `warn()` / `error()` for one-off events that don't fit a stage. - -### What NOT to log - -- Token values (use metadata: `expires_at`, `scopes`, account display name). -- Request/response bodies (sample lengths only). -- Authorization headers (log header *names* present, not values). - -### Level guidance - -| Level | Use for | -|-------|---------| -| `error` | Failures requiring action (cleanup stage). | -| `warn` | Recoverable issues (auth rejected, route fallback). | -| `info` | Lifecycle stage entries by default. | -| `debug` | High-volume detail (per-chunk stream metrics, lock acquire/release). | - -### Level config - -Default level is `info`. Configure via `logging.level` in `~/.ccs/config.yaml`. Streaming providers MUST gate per-chunk metrics behind `debug`. - -## `error.code` values (exit codes) - -Typed errors (`src/errors/error-types.ts`) carry an `ExitCode` that `handleError` propagates to `process.exit`. Log readers can branch on `error.code` for differentiated handling. The full mapping lives in `src/errors/exit-codes.ts`; the per-class assignment: - -| Typed class | ExitCode | Value | -|---|---|---:| -| `ConfigError` | `CONFIG_ERROR` | 2 | -| `NetworkError` | `NETWORK_ERROR` | 3 (recoverable) | -| `AuthError` | `AUTH_ERROR` | 4 | -| `BinaryError` | `BINARY_ERROR` | 5 | -| `ProviderError` | `PROVIDER_ERROR` | 6 (recoverable) | -| `ProfileError` | `PROFILE_ERROR` | 7 | -| `ProxyError` | `PROXY_ERROR` | 8 | -| `MigrationError` | `MIGRATION_ERROR` | 9 | -| `UserAbortError` | `USER_ABORT` | 130 | -| `ValidationError`, `RetryableError` | `GENERAL_ERROR` | 1 | - -New throws must use a typed class (enforced by `ccs/no-new-throw-error`, see `docs/code-standards.md`). Redaction scrubs credential token shapes in both context values and message strings, so routing errors into the logger is safe — but keep messages clean prose and put sensitive data in context under a sensitive key (auto-redacted). - -## Backward Compatibility - -- All new `LogEntry` fields (`requestId`, `stage`, `latencyMs`, `error`) are optional. Old readers ignore them. -- Existing `console.*` UX prints in `src/commands/`, `src/utils/ui.ts`, and similar user-facing paths are intentionally **not** converted to logger. -- `/api/logs` reader unchanged in this PR; UI surfacing of new fields tracked under #1142. - -## Future Work - -- UI surfacing of `requestId` / `stage` / `latencyMs` in the dashboard (#1142). -- `ccs logs` CLI improvements (filter by `requestId` / `stage`). -- Per-stage performance budgets (see #1071). +When adding a logging setting, update the schema/defaults, configuration loader, +dashboard surface if applicable, and focused logging tests. Do not document a +default that is not present in `DEFAULT_LOGGING_CONFIG`. diff --git a/docs/websearch.md b/docs/websearch.md index f83ede4c..a254f8ef 100644 --- a/docs/websearch.md +++ b/docs/websearch.md @@ -1,96 +1,84 @@ # WebSearch Configuration Guide -Last Updated: 2026-04-11 +CCS provides a local `WebSearch` MCP tool for managed Claude launches that use a +third-party provider. Native Claude sessions keep Anthropic's native search +behavior. -CCS provides automatic web search for third-party profiles that cannot access Anthropic's native WebSearch API. +## Launch Contract -## How WebSearch Works +For a third-party Claude launch, CCS: -### Native Claude Accounts +1. suppresses the native `WebSearch` tool because the third-party backend cannot + execute it; +2. adds a short steering prompt that prefers the CCS MCP `WebSearch` tool; +3. when WebSearch is enabled, installs the managed MCP server and adds the + `ccs-websearch` entry to the applicable Claude configuration; +4. searches enabled and ready providers in deterministic order. -Native Claude subscription accounts still use Anthropic's server-side WebSearch directly. +Enabled launches fail closed if the MCP runtime or configuration cannot be +prepared. This prevents a launch where native search is suppressed but the +managed replacement is missing. When `websearch.enabled` is `false`, CCS skips +MCP provisioning but still suppresses native WebSearch for third-party +profiles; the model can use ordinary network or shell tools when allowed. -### Third-Party Profiles +Claude subcommands that reject session flags are passed through without +WebSearch argument injection. -Third-party profiles cannot execute Anthropic's server-side WebSearch because the tool never reaches their backend. CCS now handles that by provisioning a first-class local MCP tool when the managed runtime is available, suppressing native `WebSearch` for those launches, appending a short launch-time steering hint, and running real local search providers directly. +Implementation sources: -## Architecture +- [`src/utils/websearch/mcp-installer.ts`](../src/utils/websearch/mcp-installer.ts) +- [`src/utils/websearch/claude-tool-args.ts`](../src/utils/websearch/claude-tool-args.ts) +- [`src/dispatcher/flows/settings-flow.ts`](../src/dispatcher/flows/settings-flow.ts) +- [`lib/mcp/ccs-websearch-server.cjs`](../lib/mcp/ccs-websearch-server.cjs) -``` -┌──────────────────────────────────────────────────────────────────┐ -│ Claude Code CLI │ -│ │ -│ Search Request │ -│ │ │ -│ ├── Native Claude Account? → Anthropic WebSearch API │ -│ │ │ -│ └── Third-party Profile? → native WebSearch disabled │ -│ │ │ -│ ├── CCS MCP tool when ready │ -│ │ ccs-websearch.WebSearch │ -│ │ │ │ -│ │ ├── 1. Exa │ -│ │ ├── 2. Tavily │ -│ │ ├── 3. Brave │ -│ │ ├── 4. SearXNG │ -│ │ ├── 5. DuckDuckGo│ -│ │ └── 6. Legacy CLI│ -│ │ fallback │ -│ │ (Gemini/ │ -│ │ OpenCode/│ -│ │ Grok) │ -│ └── Bash/network fallback │ -└──────────────────────────────────────────────────────────────────┘ -``` +## Provider Order, Runtime Eligibility, and Dashboard Status -## Why This Changed +The managed runtime tries eligible providers sequentially: -The previous design asked another model CLI to perform web search and summarize the answer. A later compatibility path also depended on a denied native-tool hook. Both were brittle: +1. Exa +2. Tavily +3. Brave Search +4. SearXNG +5. DuckDuckGo +6. Antigravity CLI +7. Gemini CLI compatibility fallback +8. OpenCode +9. Grok CLI -- CLI syntax changed upstream -- auth state varied per tool -- prompt/tool behavior drifted across releases -- hook-shaped denial output produced awkward host UX +The first successful provider wins. Temporarily failing providers can enter a +bounded cooldown and be skipped on later calls. Runtime attempt eligibility is +defined in +[`lib/hooks/websearch-transformer.cjs`](../lib/hooks/websearch-transformer.cjs); +do not duplicate that list in other architecture docs. -The new flow matches the `goclaw` model more closely: web search is treated as a first-class deterministic capability, not an LLM-to-LLM workaround or a denied native tool call. +The transformer and dashboard answer related but different questions: -When provisioned, the managed MCP tool is exposed as `ccs-websearch.WebSearch`, not a generic `search` helper. That naming is deliberate: it gives Claude a tool that matches the native `WebSearch` concept more directly, which should reduce cases where the model reaches for ad hoc Bash or `curl` fetches instead. +| Provider | Transformer attempts when | Dashboard reports available when | +| --- | --- | --- | +| Exa | Enabled and `EXA_API_KEY` is present. | Enabled and the key is available through the active process or enabled Global Env. | +| Tavily | Enabled and `TAVILY_API_KEY` is present. | Enabled and the key is available through the active process or enabled Global Env. | +| Brave Search | Enabled and `BRAVE_API_KEY` is present. | Enabled and the key is available through the active process or enabled Global Env. | +| SearXNG | Enabled and a base URL is present. | Enabled with a valid normalized base URL. | +| DuckDuckGo | Enabled. | Enabled. | +| Antigravity | Enabled and the `agy` executable is available. | Enabled and the CLI is installed. | +| Gemini compatibility | Enabled and the `gemini` executable is available. | Enabled, installed, and authenticated. | +| OpenCode | Enabled and the `opencode` executable is available. | Enabled and the CLI is installed. | +| Grok | Enabled and the `grok` executable is available. | Enabled, installed, and `GROK_API_KEY` is available. | -CCS also appends a third-party-only `--append-system-prompt` hint telling Claude to prefer that managed `WebSearch` tool for web lookups and current-information requests. This is soft steering only: if the user explicitly asks for shell commands, or the tool is unavailable, Claude can still fall back to Bash/network tools. -That shared launch helper applies to normal third-party settings profiles, CLIProxy/Copilot-backed Claude launches, and CCS headless/delegation runs that execute through a settings profile. - -`websearch.enabled: false` disables the managed local runtime, but CCS still suppresses Anthropic's native `WebSearch` on third-party profiles. That native tool cannot be satisfied by Exa, Tavily, Brave, DuckDuckGo, or other non-Anthropic backends, so CCS avoids sending a broken native-tool request and lets Claude fall back to normal shell/network tools instead. - -## Providers - -| Provider | Type | Setup | Default | Notes | -|----------|------|-------|---------|-------| -| Exa | HTTP API | `EXA_API_KEY` | No | High-quality API search with extracted content | -| Tavily | HTTP API | `TAVILY_API_KEY` | No | Agent-oriented search API | -| Brave Search | HTTP API | `BRAVE_API_KEY` | No | Cleaner snippets and metadata | -| SearXNG | JSON API | `providers.searxng.url` | No | Self-hosted/public SearXNG backend via `/search?format=json` | -| DuckDuckGo | HTML fetch | None | Yes | Built-in zero-setup fallback | -| Antigravity (agy) | LLM CLI | `curl -fsSL https://antigravity.google/cli/install.sh \| bash` | No | Recommended LLM CLI fallback (Gemini CLI successor) | -| Gemini CLI | LLM CLI | Deprecated, use Antigravity (agy) | No | Deprecated. Google retired the gemini CLI on 2026-06-18 | -| OpenCode | LLM CLI | `curl -fsSL https://opencode.ai/install \| bash` | No | Optional compatibility fallback | -| Grok CLI | LLM CLI | `npm i -g @vibe-kit/grok-cli` + `GROK_API_KEY` | No | Optional compatibility fallback | +DuckDuckGo is the default zero-setup provider. API-backed and CLI fallback +providers are disabled by default. Dashboard availability and setup guidance +are computed separately by +[`src/utils/websearch/status.ts`](../src/utils/websearch/status.ts); they do not +change the transformer's attempt predicate. ## Configuration -### Via Dashboard +Use `ccs config` and open `Settings` → `WebSearch`, or edit the `websearch` +section in the unified CCS configuration: -Open `ccs config` → `Settings` → `WebSearch`. - -- Enable Exa, Tavily, Brave, SearXNG, or DuckDuckGo in the backend chain -- Configure the SearXNG base URL (for example `https://search.example.com`) when SearXNG is enabled - Do not include `/search`, embedded credentials, query parameters, or URL fragments. CCS appends `/search?format=json`. -- Set or rotate Exa, Tavily, and Brave API keys directly inside each provider card -- Saved keys are persisted in `global_env` and injected at runtime, so readiness updates from the same screen -- Review whether any legacy fallback CLIs are still enabled in config - -### Via Config File - -Edit `~/.ccs/config.yaml`: +The runtime schema supports Antigravity through `providers.agy`; the current +dashboard editor does not expose that provider, so configure it in +`config.yaml`. ```yaml websearch: @@ -129,80 +117,84 @@ websearch: timeout: 55 ``` -Note: `enabled: false` stops provisioning the managed local `ccs-websearch.WebSearch` runtime. It does not re-enable Anthropic's native `WebSearch` for third-party backends. +The schema is +[`src/config/schemas/websearch.ts`](../src/config/schemas/websearch.ts), and +defaults are in +[`src/config/schemas/unified-config.ts`](../src/config/schemas/unified-config.ts). +Deprecated top-level WebSearch fields remain load-compatible but must not be +used for new configuration. -## Environment Variables +### SearXNG URL -| Variable | Description | -|----------|-------------| -| `EXA_API_KEY` | Enables Exa when `providers.exa.enabled: true` | -| `TAVILY_API_KEY` | Enables Tavily when `providers.tavily.enabled: true` | -| `BRAVE_API_KEY` | Enables Brave Search when `providers.brave.enabled: true` | -| `CCS_WEBSEARCH_SEARXNG_URL` | Runtime URL used when `providers.searxng.enabled: true` | -| `CCS_WEBSEARCH_SEARXNG_MAX_RESULTS` | Optional runtime override for SearXNG result count (clamped 1..10) | -| `GROK_API_KEY` | Required only for legacy Grok CLI fallback | -| `CCS_WEBSEARCH_SKIP` | Disable the CCS local WebSearch runtime for the current process; third-party launches still keep native Anthropic `WebSearch` disabled | -| `CCS_DEBUG` | Verbose WebSearch runtime logging | -| `CCS_WEBSEARCH_TRACE` | Write opt-in JSONL trace records under `~/.ccs/logs/websearch-trace.jsonl` | -| `CCS_WEBSEARCH_TRACE_FILE` | Override the trace file path (must stay inside `~/.ccs/`, your system temp directory, or `/var/log`) | +Configure the instance base URL, for example +`https://search.example.invalid`. Do not include `/search`, credentials, query +parameters, or a URL fragment. CCS normalizes the base and calls the JSON search +endpoint. -## Managed Runtime Files +### Dashboard-managed API keys -- `~/.claude.json` → CCS manages `mcpServers.ccs-websearch` -- `~/.ccs/mcp/ccs-websearch-server.cjs` → local MCP server binary -- `~/.ccs/hooks/websearch-transformer.cjs` → shared provider runtime plus legacy compatibility fallback +The dashboard stores supported provider keys in `global_env`. They are +available to WebSearch only when global environment injection is enabled. +Shell-provided environment values remain supported. Never place real keys in +documentation, tests, or committed configuration. -## Troubleshooting +## Managed Files -### WebSearch says "Ready (DuckDuckGo)" +In the default user layout, CCS manages: -That is expected. DuckDuckGo is the default zero-setup backend. +- `~/.claude.json` → `mcpServers.ccs-websearch` +- `~/.ccs/mcp/ccs-websearch-server.cjs` +- `~/.ccs/hooks/websearch-transformer.cjs` -### Exa, Tavily, or Brave is enabled but not ready +CCS installation and test paths can differ because configuration helpers honor +the active CCS and Claude home locations. Provisioning uses a lock and +preserves unrelated MCP server entries. Malformed Claude configuration is not +overwritten. -Set the matching API key in the WebSearch dashboard card, or export it in the environment that launches CCS, then refresh status: +## Runtime Environment -```bash -export EXA_API_KEY="your-api-key" -# or: export TAVILY_API_KEY="your-api-key" -# or: export BRAVE_API_KEY="your-api-key" -ccs config -``` +[`src/utils/websearch/hook-env.ts`](../src/utils/websearch/hook-env.ts) converts +the resolved configuration into runtime environment values. Provider API keys +remain environment inputs; boolean and limit variables describe provider +selection. -If the dashboard says the key is stored but still not ready, check whether `Settings -> Global Env` is disabled. WebSearch reuses that injection path for dashboard-managed keys. +Operational overrides: -### SearXNG is enabled but not ready +| Variable | Effect | +| --- | --- | +| `CCS_DEBUG` | Enables verbose diagnostics and WebSearch trace collection. | +| `CCS_WEBSEARCH_TRACE` | Enables WebSearch JSONL trace collection. | +| `CCS_WEBSEARCH_TRACE_FILE` | Requests a trace path within an allowed CCS log, system temporary, or `/var/log` boundary. | -1. Confirm the configured base URL is valid (for example `https://search.example.com`) -2. Confirm the instance exposes `GET /search?q=&format=json` -3. If the hook reports `SearXNG returned 403: format=json is disabled on this instance`, enable JSON format on that SearXNG deployment or switch to another backend +An unsafe trace-file override is ignored. Trace writes are best effort and do +not change launch or search results. -### I still want Gemini/OpenCode/Grok fallback +## Diagnostics -Those providers remain supported, but they are no longer the primary path. Enable them explicitly in `config.yaml` if you want them as last-resort fallback. +By default, trace records are written under +`~/.ccs/logs/websearch-trace.jsonl`. They correlate launch preparation, MCP +exposure, tool calls, provider attempts, provider success/failure, and session +summaries. -### I need to see whether CCS exposed WebSearch or the model bypassed it +Normal trace metadata uses a query fingerprint and length instead of the raw +query. Provider-failure records can also include `error` detail returned by a +provider implementation, including HTTP response excerpts or CLI stderr. +That provider-generated detail is not guaranteed to exclude query text or +other sensitive content unless the implementation redacts it. Treat the trace +file as sensitive operational data. -Run the launch with `CCS_WEBSEARCH_TRACE=1` (or `CCS_DEBUG=1`). CCS writes a JSONL trace to `~/.ccs/logs/websearch-trace.jsonl` with: +For delegated/headless sessions, a likely-bypass summary means the tool was +exposed but no WebSearch call occurred and another allowed tool path was used. -1. source-side launch records from CCS (`ccs_websearch_launch`) -2. MCP exposure and call records (`mcp_initialize`, `mcp_tools_list`, `mcp_tool_call_*`) -3. provider attempt and winner records (`websearch_provider_attempt`, `websearch_provider_success`) -4. session summaries (`mcp_session_summary`, and headless `headless_websearch_summary` when applicable) +When search is unavailable: -Queries are fingerprinted (`queryHash`, `queryLength`) instead of logged raw by default. For headless/delegation runs, `headless_websearch_summary.likelyBypassed=true` means the MCP tool was exposed, no WebSearch call occurred, and Claude fell back to `Bash` or `WebFetch`. +1. confirm `websearch.enabled` and at least one provider are enabled; +2. check readiness in the dashboard; +3. verify API keys or the SearXNG base URL without printing secrets; +4. enable `CCS_WEBSEARCH_TRACE=1` for one launch; +5. inspect provider attempt and cooldown events. -### WebSearch returns no results - -1. Check `websearch.enabled: true` -2. Keep DuckDuckGo enabled unless you have a strong reason to disable it -3. If using Exa, Tavily, or Brave, verify the matching API key -4. Run with `CCS_DEBUG=1` for runtime logs, or `CCS_WEBSEARCH_TRACE=1` for correlated launch/MCP/provider traces -5. If DuckDuckGo returns a non-result HTML error, retry later or enable another provider. CCS now treats that as a provider failure instead of a false empty result. - -## Security Considerations - -- API keys entered from the dashboard are stored in `~/.ccs/config.yaml` under `global_env` and injected as environment variables at runtime -- Shell-exported keys still work and are detected as external environment input -- Never commit API keys to version control -- Use the dashboard only on trusted machines, and protect `~/.ccs/config.yaml` with normal user-level filesystem permissions +Focused coverage lives under +[`tests/unit/utils/websearch/`](../tests/unit/utils/websearch/), +[`tests/unit/hooks/`](../tests/unit/hooks/), and +[`tests/unit/targets/settings-profile-websearch-launch.test.ts`](../tests/unit/targets/settings-profile-websearch-launch.test.ts).