Merge pull request #1668 from kaitranntt/kai/docs/1665-architecture-contracts

docs: reconcile architecture and engineering contracts
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-07-26 22:09:17 -04:00
commit a8217bf18d
8 files changed
+1099 -2479

No files matched your search

+20 -9
View File
@@ -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
+102 -141
View File
@@ -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 `<scheme> [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`.
+194 -314
View File
@@ -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 <profile>` 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/<backend>/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 `<profile>.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 <account>`
- 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/<channel>/.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/<channel>/.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)
+90 -79
View File
@@ -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 `:<ver>` tag. Docker
mutable tags (`:latest`, `:<MAJOR>`, `:<MINOR>`) 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`, `:<MAJOR>`, `:<MINOR>`) 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 `:<ver>` 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
`<stable>-dev.<n>` 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`, `:<MAJOR>`, and
`:<MINOR>` 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 `:<ver>` 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` | `:<ver>` (immutable, immediate) | `:latest`, `:<MAJOR>`, `:<MINOR>` (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 |
+211 -430
View File
@@ -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-<profile>`
- Spawns: `droid -m custom:ccs-<profile> <args>`
- 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 <cli>] <profile> [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
|
+---> <profile>.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/<profile>/
|
+---> 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/<profile>.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
├── <profile>.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)
+191 -585
View File
@@ -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 <profile>.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 <host> Remote hostname/IP
--proxy-port <port> Port (default: 8317 HTTP, 443 HTTPS)
--proxy-protocol <proto> http or https
--proxy-auth-token <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<boolean> {
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 <account> --> Set isPaused=true in account-manager
ccs cliproxy resume <account> --> 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/<profile>.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/<profile>.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/<backend>/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)
+169 -791
View File
@@ -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<void>;
/** 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<void> {
// 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<void> {
// 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 <model> 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 <key=value>` 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<void> {
// 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<TargetType, TargetAdapter>();
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<void> {
// 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 <name>` or `--target=<name>`; 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.
+122 -130
View File
@@ -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=<query>&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).