docs(architecture): reconcile provider and target contracts

This commit is contained in:
Tam Nhu Tran committed 2026-07-26 09:35:00 -04:00
1 parent b918783293
commit ebe1746459
3 files changed
+571 -1806

No files matched your search

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