From 51e806299564da03ac8ce98cad35cc962ec01078 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sun, 26 Jul 2026 09:35:10 -0400 Subject: [PATCH] docs(operations): refresh runtime contracts --- docs/i18n-dashboard.md | 29 +++-- docs/logging-contract.md | 243 ++++++++++++++++--------------------- docs/websearch.md | 252 +++++++++++++++++++-------------------- 3 files changed, 244 insertions(+), 280 deletions(-) diff --git a/docs/i18n-dashboard.md b/docs/i18n-dashboard.md index 618e18ba..7caa416a 100644 --- a/docs/i18n-dashboard.md +++ b/docs/i18n-dashboard.md @@ -1,7 +1,5 @@ # Dashboard i18n Guide -Last Updated: 2026-04-14 - This document describes the internationalization (i18n) architecture used by the CCS Dashboard (`ui/`), how locale selection works, and how to add new languages safely. --- @@ -15,6 +13,7 @@ Dashboard i18n currently covers UI text rendered by React components. - `zh-CN` (Simplified Chinese) - `vi` (Vietnamese) - `ja` (Japanese) + - `ko` (Korean) - Locale state is persisted in browser localStorage using `ccs-ui-locale`. - Fallback language is `en`. @@ -40,6 +39,7 @@ Out of scope: - `zh-CN.translation` - `vi.translation` - `ja.translation` + - `ko.translation` - Uses `initReactI18next` for React integration. ### Locale utilities @@ -59,8 +59,11 @@ Out of scope: ### Test bootstrap - File: `ui/tests/setup/vitest-setup.ts` -- Test setup must import `ui/src/lib/i18n.ts` so direct `useTranslation()` consumers resolve the same singleton instance as the app. -- If a test mocks `useTranslation()`, keep the mocked key surface aligned with the component output or the assertions will drift. +- Test setup must import `ui/src/lib/i18n.ts` so components using the + react-i18next translation hook resolve the same singleton instance as the + app. +- If a test mocks the translation hook, keep the mocked key surface aligned + with the component output or the assertions will drift. --- @@ -104,11 +107,8 @@ When adding a locale such as Thai (`th`): 5. Run UI validation and i18n tests: - `cd ui && bun run validate` - `cd ui && bun run test:run tests/unit/ui/i18n/language-switcher.test.tsx` -6. Add or update key-parity tests to catch locale drift. - -Current issue driving the Vietnamese rollout: - -- https://github.com/kaitranntt/ccs/issues/659 +6. Keep the key-parity test passing. It compares every non-English resource + against English and verifies interpolation placeholders match. --- @@ -121,3 +121,14 @@ Before opening a PR that touches i18n: - [ ] No unsafe HTML injection path introduced for translated content. - [ ] `ui` validate/test commands pass. - [ ] This document is updated if architecture or conventions changed. + +## References + +- [`ui/src/lib/locales.ts`](../ui/src/lib/locales.ts) - supported locale ids, + normalization, persistence, and formatting locale +- [`ui/src/lib/i18n.ts`](../ui/src/lib/i18n.ts) - translation resources and + i18next initialization +- [`ui/src/components/layout/language-switcher.tsx`](../ui/src/components/layout/language-switcher.tsx) + - locale selection UI +- [`ui/tests/unit/ui/i18n/language-switcher.test.tsx`](../ui/tests/unit/ui/i18n/language-switcher.test.tsx) + - selection, persistence, key-parity, and placeholder coverage diff --git a/docs/logging-contract.md b/docs/logging-contract.md index b51c68a9..99c05a1b 100644 --- a/docs/logging-contract.md +++ b/docs/logging-contract.md @@ -1,178 +1,139 @@ # Logging Contract -Single source of truth for structured backend logging in CCS CLI. Companion to GitHub issues #1138 (umbrella) and #1141 (backend instrumentation). +CCS structured logs are a machine-readable JSONL channel for backend and +runtime events. They are separate from terminal UX output and must never be +used as a substitute for user-facing recovery messages. -## Overview +The canonical schema is +[`src/services/logging/log-types.ts`](../src/services/logging/log-types.ts). +Defaults are defined in +[`src/config/schemas/logging.ts`](../src/config/schemas/logging.ts). -CCS emits structured JSONL log entries for backend behavior (proxy daemons, OAuth flows, target spawn lifecycle, executor errors, etc.). This document defines the canonical schema, request-correlation pattern, lifecycle stages, and redaction policy. +## Entry Schema -> CLI text output (`ok / info / warn / fail` from `src/utils/ui.ts`) is **NOT** affected by this contract. Logs are a separate channel — never printed to stdout/stderr. +| Field | Type | Required | Contract | +| --- | --- | --- | --- | +| `id` | `string` | yes | Unique entry id. | +| `timestamp` | `string` | yes | ISO 8601 emission time. | +| `level` | `error`, `warn`, `info`, or `debug` | yes | Severity. | +| `source` | `string` | yes | Stable module-scoped producer id. | +| `event` | `string` | yes | Stable machine-readable event name. | +| `message` | `string` | yes | Short human-readable summary. | +| `processId` | `number` | yes | Emitting process id. | +| `runId` | `string` | yes | Stable for the current process. | +| `context` | object | no | Event fields after configured redaction. | +| `requestId` | `string` | no | Cross-stage correlation id. | +| `stage` | `LogStage` | no | Canonical lifecycle stage. | +| `latencyMs` | `number` | no | Elapsed milliseconds, normally at completion. | +| `error` | `LogErrorInfo` | no | Structured error metadata. | -## Schema (`LogEntry`) - -Defined in `src/services/logging/log-types.ts`. - -| Field | Type | Required | Notes | -|-------|------|----------|-------| -| `id` | `string` | yes | UUID per entry. | -| `timestamp` | `string` | yes | ISO 8601. | -| `level` | `'error'\|'warn'\|'info'\|'debug'` | yes | | -| `source` | `string` | yes | Module-scoped identifier (e.g. `proxy:openai-compat:messages`). | -| `event` | `string` | yes | Dotted machine-readable event name (e.g. `request.received`). | -| `message` | `string` | yes | Human-readable summary. | -| `processId` | `number` | yes | `process.pid`. | -| `runId` | `string` | yes | Stable per-process id. | -| `context` | `object` | no | Free-form structured fields (redacted). | -| `requestId` | `string` | no | Correlates entries belonging to one inbound request across stages. | -| `stage` | `LogStage` | no | Lifecycle stage tag. | -| `latencyMs` | `number` | no | Elapsed ms (typically on `respond` / `cleanup`). | -| `error` | `{name, message, code?, stack?}` | no | Structured error metadata; never raw token strings. | - -Old free-form entries (no `requestId` / `stage`) are still valid; new fields are additive. - -### Example - -```jsonl -{"id":"...","timestamp":"2026-04-30T12:34:56.000Z","level":"info","source":"proxy:openai-compat:messages","event":"request.received","message":"Proxy /v1/messages request received","processId":42,"runId":"r1","requestId":"a1b2...","stage":"intake","context":{"method":"POST"}} -``` +Additive optional fields preserve compatibility with older readers. Consumers +must not assume every event has a stage, request id, latency, or structured +error. ## Lifecycle Stages -`LogStage` is one of: +Use `logger.stage()` for events that map to the canonical lifecycle: -| Stage | When to emit | -|-------|--------------| -| `intake` | Inbound request received at an entry edge (HTTP handler, CLI dispatch). | -| `route` | Destination/profile/target resolution. | -| `auth` | Authentication / authorization (token exchange, profile auth). | -| `dispatch` | Outbound request prepared / child process spawned. | -| `upstream` | Upstream call in flight (provider HTTP / spawned child running). | -| `transform` | Payload translation (request/response shape conversion). | -| `respond` | Response written / dispatched (`latencyMs` typically populated). | -| `cleanup` | Error path, abort, teardown. | +| Stage | Meaning | +| --- | --- | +| `intake` | Request or command entered a CCS boundary. | +| `route` | Profile, provider, target, or destination resolution. | +| `auth` | Authentication or authorization work. | +| `dispatch` | Outbound request or child launch prepared. | +| `upstream` | Provider request or child operation in flight. | +| `transform` | Request or response translation. | +| `respond` | Result dispatched to the caller. | +| `cleanup` | Failure, abort, or teardown path. | -Stages may be skipped or repeated. Streaming responses tag `upstream` only at start/end (NOT per chunk). +Stages can be skipped or repeated. Use `logger.info()`, `warn()`, or `error()` +for events that do not represent a lifecycle stage. High-volume details, +including streaming chunk metrics, belong at `debug`. -## RequestId Propagation (AsyncLocalStorage) - -`requestId` is propagated implicitly via Node `AsyncLocalStorage`. Entry edges wrap their handler in `withRequestContext`; every `createLogger`-emitted entry inside the context auto-merges `requestId` from the active store. +Example: ```ts -import { withRequestContext, createLogger } from './services/logging'; - -const logger = createLogger('proxy:my-edge'); - -http.createServer((req, res) => { - const requestId = req.headers['x-ccs-request-id'] ?? randomUUID(); - res.setHeader('x-ccs-request-id', requestId); - withRequestContext({ requestId }, async () => { - logger.stage('intake', 'request.received', 'inbound'); - // ... downstream work emits with the same requestId - }); +logger.stage('respond', 'request.completed', 'Request completed', { + statusCode: 200, +}, { + latencyMs: 42, }); ``` -### Cross-daemon header +See [`src/services/logging/logger.ts`](../src/services/logging/logger.ts) for the +compiler-checked signature. -`x-ccs-request-id` round-trips across the proxy edge: -- Inbound: if the header is present and matches the UUID-ish guard (`/^[A-Za-z0-9._-]{8,128}$/`), it is reused; otherwise a fresh UUID is minted. -- Outbound (response): the resolved id is echoed back via `res.setHeader('x-ccs-request-id', ...)`. -- When CCS calls another daemon (copilot, cursor, glmt), forward the active id in the same header so that daemon can correlate. +## Request Correlation -### Ordering guarantee +[`src/services/logging/log-context.ts`](../src/services/logging/log-context.ts) +uses Node async-local storage within a process. Entry edges establish a +context; loggers created downstream read its `requestId` automatically. -Emit-time ordering of entries within a single `requestId` is monotonic — the active context is single-threaded relative to the request, so `timestamp` ordering reflects emit order. The UI layer (#1142) consumes this guarantee. +Async-local context does not cross child processes or worker threads. A child +process can inherit the active id through `CCS_REQUEST_ID` and establish a new +local context. HTTP edges may use `x-ccs-request-id`; each edge owns whether it +accepts an incoming id or mints a new one. -### What NOT to put in the context +Only the characters and length accepted by `REQUEST_ID_PATTERN` are valid for +forwarded ids. Do not treat request ids as authentication or authorization +material. -The ALS context object is mixed into every downstream entry. Never store: -- Raw tokens, API keys, refresh tokens, OAuth codes -- Raw request/response bodies -- User-supplied secrets +There is no global ordering guarantee across concurrent async work or processes. +Use `timestamp`, stage, event, and process/run identifiers together when +reconstructing a request. -Only benign correlation metadata: `requestId`, `method`, `path`, `command`, `profile`. +## Redaction and Data Minimization -### Worker threads / spawned children +[`src/services/logging/log-redaction.ts`](../src/services/logging/log-redaction.ts) +is the implementation source of truth. With the default `logging.redact: true`, +the logger: -ALS context is **not** inherited by worker threads or `child_process.spawn` stdio pipes. At those boundaries, mint a fresh `requestId` at the child entry and pass the parent id explicitly via env var or header for correlation. +- replaces values under known credential-bearing keys; +- masks common authorization schemes and credential token shapes in strings; +- redacts sensitive CLI flag values, including inline assignments; +- limits string length and nested object depth; +- strips an `Error` object to safe structured fields. -## Redaction +The matcher evolves as credential surfaces change. Link to the implementation +instead of copying its complete key or token-pattern list into other docs. -`src/services/logging/log-redaction.ts` is the single source of truth. +Redaction is defense in depth, not permission to log sensitive data. Never log: -### Sensitive key matcher +- tokens, passwords, cookies, OAuth codes, or authorization values; +- raw request or response bodies; +- raw prompts or prompt-bearing CLI arguments; +- personal identifiers when a non-identifying account or profile label works. -`SENSITIVE_KEY_PATTERN` matches (case-insensitive, with `_` / `-` / camelCase variants): -`authorization`, `proxy-authorization`, `cookie`, `set-cookie`, `password`, `password_hash`, `secret`, `client_secret`, `token`, `auth_token`, `access_token`, `refresh_token`, `id_token`, `bearer`, `assertion`, `api_key`, `x-api-key`, `x-goog-api-key`, `management_key`, `copilot_token`, `cursor_session_key`, `oauth_code`, `auth_code`. +When a new secret-bearing field or flag is introduced, update the redactor and +its tests under +[`tests/unit/services/logging/`](../tests/unit/services/logging/). -String/object values for matching keys are replaced with `[redacted]`. Numeric/boolean values pass through (e.g., `expires_at` epoch numbers stay readable). +## Error Codes and Process Exit Codes -### Auth-scheme value masking +These are separate contracts: -Raw string values whose prefix matches `^(Bearer|Basic|Token)\s+\S+` are rewritten to ` [redacted]` even when nested under non-sensitive keys. +- `LogEntry.error.code` is an optional **string** supplied as part of structured + error metadata. It can carry a runtime, system, or provider error identifier. +- `CCSError.code` is a numeric `ExitCode`. The centralized CLI error handler + writes it as `context.exitCode` and passes it to `process.exit`. -### Argv redaction +Do not put a numeric process exit code in `LogEntry.error.code`, and do not make +log consumers derive process status from that string field. -`redactArgv(argv)` redacts the value following any sensitive flag (`--token`, `--api-key`, `--auth`, `--bearer`, `--secret`, `--client-secret`, `--access-token`, `--refresh-token`, `--id-token`, `--password`). +The numeric mapping lives in +[`src/errors/exit-codes.ts`](../src/errors/exit-codes.ts). Typed error +assignments live in +[`src/errors/error-types.ts`](../src/errors/error-types.ts), and propagation is +implemented by +[`src/errors/error-handler.ts`](../src/errors/error-handler.ts). -### Adding new sensitive keys +## Configuration -1. Extend `SENSITIVE_KEY_PATTERN` in `src/services/logging/log-redaction.ts`. -2. Add a unit test in `tests/unit/services/logging/log-redaction-extended.test.ts`. -3. Verify regex stays O(1) per key (no catastrophic backtracking). +CCS-owned logging uses the `logging` section in the unified config. Its defaults +enable logging and redaction at `info` level with bounded rotation, retention, +and live-buffer settings. This is distinct from `cliproxy.logging`, which +controls CLIProxy runtime logging. -## Contributor Guide - -### When to use `logger.stage()` vs `logger.info()` - -Use `stage()` whenever the entry corresponds to one of the canonical lifecycle stages — this is what observability tooling and the dashboard rely on. Use `info()` / `warn()` / `error()` for one-off events that don't fit a stage. - -### What NOT to log - -- Token values (use metadata: `expires_at`, `scopes`, account display name). -- Request/response bodies (sample lengths only). -- Authorization headers (log header *names* present, not values). - -### Level guidance - -| Level | Use for | -|-------|---------| -| `error` | Failures requiring action (cleanup stage). | -| `warn` | Recoverable issues (auth rejected, route fallback). | -| `info` | Lifecycle stage entries by default. | -| `debug` | High-volume detail (per-chunk stream metrics, lock acquire/release). | - -### Level config - -Default level is `info`. Configure via `logging.level` in `~/.ccs/config.yaml`. Streaming providers MUST gate per-chunk metrics behind `debug`. - -## `error.code` values (exit codes) - -Typed errors (`src/errors/error-types.ts`) carry an `ExitCode` that `handleError` propagates to `process.exit`. Log readers can branch on `error.code` for differentiated handling. The full mapping lives in `src/errors/exit-codes.ts`; the per-class assignment: - -| Typed class | ExitCode | Value | -|---|---|---:| -| `ConfigError` | `CONFIG_ERROR` | 2 | -| `NetworkError` | `NETWORK_ERROR` | 3 (recoverable) | -| `AuthError` | `AUTH_ERROR` | 4 | -| `BinaryError` | `BINARY_ERROR` | 5 | -| `ProviderError` | `PROVIDER_ERROR` | 6 (recoverable) | -| `ProfileError` | `PROFILE_ERROR` | 7 | -| `ProxyError` | `PROXY_ERROR` | 8 | -| `MigrationError` | `MIGRATION_ERROR` | 9 | -| `UserAbortError` | `USER_ABORT` | 130 | -| `ValidationError`, `RetryableError` | `GENERAL_ERROR` | 1 | - -New throws must use a typed class (enforced by `ccs/no-new-throw-error`, see `docs/code-standards.md`). Redaction scrubs credential token shapes in both context values and message strings, so routing errors into the logger is safe — but keep messages clean prose and put sensitive data in context under a sensitive key (auto-redacted). - -## Backward Compatibility - -- All new `LogEntry` fields (`requestId`, `stage`, `latencyMs`, `error`) are optional. Old readers ignore them. -- Existing `console.*` UX prints in `src/commands/`, `src/utils/ui.ts`, and similar user-facing paths are intentionally **not** converted to logger. -- `/api/logs` reader unchanged in this PR; UI surfacing of new fields tracked under #1142. - -## Future Work - -- UI surfacing of `requestId` / `stage` / `latencyMs` in the dashboard (#1142). -- `ccs logs` CLI improvements (filter by `requestId` / `stage`). -- Per-stage performance budgets (see #1071). +When adding a logging setting, update the schema/defaults, configuration loader, +dashboard surface if applicable, and focused logging tests. Do not document a +default that is not present in `DEFAULT_LOGGING_CONFIG`. diff --git a/docs/websearch.md b/docs/websearch.md index f83ede4c..a254f8ef 100644 --- a/docs/websearch.md +++ b/docs/websearch.md @@ -1,96 +1,84 @@ # WebSearch Configuration Guide -Last Updated: 2026-04-11 +CCS provides a local `WebSearch` MCP tool for managed Claude launches that use a +third-party provider. Native Claude sessions keep Anthropic's native search +behavior. -CCS provides automatic web search for third-party profiles that cannot access Anthropic's native WebSearch API. +## Launch Contract -## How WebSearch Works +For a third-party Claude launch, CCS: -### Native Claude Accounts +1. suppresses the native `WebSearch` tool because the third-party backend cannot + execute it; +2. adds a short steering prompt that prefers the CCS MCP `WebSearch` tool; +3. when WebSearch is enabled, installs the managed MCP server and adds the + `ccs-websearch` entry to the applicable Claude configuration; +4. searches enabled and ready providers in deterministic order. -Native Claude subscription accounts still use Anthropic's server-side WebSearch directly. +Enabled launches fail closed if the MCP runtime or configuration cannot be +prepared. This prevents a launch where native search is suppressed but the +managed replacement is missing. When `websearch.enabled` is `false`, CCS skips +MCP provisioning but still suppresses native WebSearch for third-party +profiles; the model can use ordinary network or shell tools when allowed. -### Third-Party Profiles +Claude subcommands that reject session flags are passed through without +WebSearch argument injection. -Third-party profiles cannot execute Anthropic's server-side WebSearch because the tool never reaches their backend. CCS now handles that by provisioning a first-class local MCP tool when the managed runtime is available, suppressing native `WebSearch` for those launches, appending a short launch-time steering hint, and running real local search providers directly. +Implementation sources: -## Architecture +- [`src/utils/websearch/mcp-installer.ts`](../src/utils/websearch/mcp-installer.ts) +- [`src/utils/websearch/claude-tool-args.ts`](../src/utils/websearch/claude-tool-args.ts) +- [`src/dispatcher/flows/settings-flow.ts`](../src/dispatcher/flows/settings-flow.ts) +- [`lib/mcp/ccs-websearch-server.cjs`](../lib/mcp/ccs-websearch-server.cjs) -``` -┌──────────────────────────────────────────────────────────────────┐ -│ Claude Code CLI │ -│ │ -│ Search Request │ -│ │ │ -│ ├── Native Claude Account? → Anthropic WebSearch API │ -│ │ │ -│ └── Third-party Profile? → native WebSearch disabled │ -│ │ │ -│ ├── CCS MCP tool when ready │ -│ │ ccs-websearch.WebSearch │ -│ │ │ │ -│ │ ├── 1. Exa │ -│ │ ├── 2. Tavily │ -│ │ ├── 3. Brave │ -│ │ ├── 4. SearXNG │ -│ │ ├── 5. DuckDuckGo│ -│ │ └── 6. Legacy CLI│ -│ │ fallback │ -│ │ (Gemini/ │ -│ │ OpenCode/│ -│ │ Grok) │ -│ └── Bash/network fallback │ -└──────────────────────────────────────────────────────────────────┘ -``` +## Provider Order, Runtime Eligibility, and Dashboard Status -## Why This Changed +The managed runtime tries eligible providers sequentially: -The previous design asked another model CLI to perform web search and summarize the answer. A later compatibility path also depended on a denied native-tool hook. Both were brittle: +1. Exa +2. Tavily +3. Brave Search +4. SearXNG +5. DuckDuckGo +6. Antigravity CLI +7. Gemini CLI compatibility fallback +8. OpenCode +9. Grok CLI -- CLI syntax changed upstream -- auth state varied per tool -- prompt/tool behavior drifted across releases -- hook-shaped denial output produced awkward host UX +The first successful provider wins. Temporarily failing providers can enter a +bounded cooldown and be skipped on later calls. Runtime attempt eligibility is +defined in +[`lib/hooks/websearch-transformer.cjs`](../lib/hooks/websearch-transformer.cjs); +do not duplicate that list in other architecture docs. -The new flow matches the `goclaw` model more closely: web search is treated as a first-class deterministic capability, not an LLM-to-LLM workaround or a denied native tool call. +The transformer and dashboard answer related but different questions: -When provisioned, the managed MCP tool is exposed as `ccs-websearch.WebSearch`, not a generic `search` helper. That naming is deliberate: it gives Claude a tool that matches the native `WebSearch` concept more directly, which should reduce cases where the model reaches for ad hoc Bash or `curl` fetches instead. +| Provider | Transformer attempts when | Dashboard reports available when | +| --- | --- | --- | +| Exa | Enabled and `EXA_API_KEY` is present. | Enabled and the key is available through the active process or enabled Global Env. | +| Tavily | Enabled and `TAVILY_API_KEY` is present. | Enabled and the key is available through the active process or enabled Global Env. | +| Brave Search | Enabled and `BRAVE_API_KEY` is present. | Enabled and the key is available through the active process or enabled Global Env. | +| SearXNG | Enabled and a base URL is present. | Enabled with a valid normalized base URL. | +| DuckDuckGo | Enabled. | Enabled. | +| Antigravity | Enabled and the `agy` executable is available. | Enabled and the CLI is installed. | +| Gemini compatibility | Enabled and the `gemini` executable is available. | Enabled, installed, and authenticated. | +| OpenCode | Enabled and the `opencode` executable is available. | Enabled and the CLI is installed. | +| Grok | Enabled and the `grok` executable is available. | Enabled, installed, and `GROK_API_KEY` is available. | -CCS also appends a third-party-only `--append-system-prompt` hint telling Claude to prefer that managed `WebSearch` tool for web lookups and current-information requests. This is soft steering only: if the user explicitly asks for shell commands, or the tool is unavailable, Claude can still fall back to Bash/network tools. -That shared launch helper applies to normal third-party settings profiles, CLIProxy/Copilot-backed Claude launches, and CCS headless/delegation runs that execute through a settings profile. - -`websearch.enabled: false` disables the managed local runtime, but CCS still suppresses Anthropic's native `WebSearch` on third-party profiles. That native tool cannot be satisfied by Exa, Tavily, Brave, DuckDuckGo, or other non-Anthropic backends, so CCS avoids sending a broken native-tool request and lets Claude fall back to normal shell/network tools instead. - -## Providers - -| Provider | Type | Setup | Default | Notes | -|----------|------|-------|---------|-------| -| Exa | HTTP API | `EXA_API_KEY` | No | High-quality API search with extracted content | -| Tavily | HTTP API | `TAVILY_API_KEY` | No | Agent-oriented search API | -| Brave Search | HTTP API | `BRAVE_API_KEY` | No | Cleaner snippets and metadata | -| SearXNG | JSON API | `providers.searxng.url` | No | Self-hosted/public SearXNG backend via `/search?format=json` | -| DuckDuckGo | HTML fetch | None | Yes | Built-in zero-setup fallback | -| Antigravity (agy) | LLM CLI | `curl -fsSL https://antigravity.google/cli/install.sh \| bash` | No | Recommended LLM CLI fallback (Gemini CLI successor) | -| Gemini CLI | LLM CLI | Deprecated, use Antigravity (agy) | No | Deprecated. Google retired the gemini CLI on 2026-06-18 | -| OpenCode | LLM CLI | `curl -fsSL https://opencode.ai/install \| bash` | No | Optional compatibility fallback | -| Grok CLI | LLM CLI | `npm i -g @vibe-kit/grok-cli` + `GROK_API_KEY` | No | Optional compatibility fallback | +DuckDuckGo is the default zero-setup provider. API-backed and CLI fallback +providers are disabled by default. Dashboard availability and setup guidance +are computed separately by +[`src/utils/websearch/status.ts`](../src/utils/websearch/status.ts); they do not +change the transformer's attempt predicate. ## Configuration -### Via Dashboard +Use `ccs config` and open `Settings` → `WebSearch`, or edit the `websearch` +section in the unified CCS configuration: -Open `ccs config` → `Settings` → `WebSearch`. - -- Enable Exa, Tavily, Brave, SearXNG, or DuckDuckGo in the backend chain -- Configure the SearXNG base URL (for example `https://search.example.com`) when SearXNG is enabled - Do not include `/search`, embedded credentials, query parameters, or URL fragments. CCS appends `/search?format=json`. -- Set or rotate Exa, Tavily, and Brave API keys directly inside each provider card -- Saved keys are persisted in `global_env` and injected at runtime, so readiness updates from the same screen -- Review whether any legacy fallback CLIs are still enabled in config - -### Via Config File - -Edit `~/.ccs/config.yaml`: +The runtime schema supports Antigravity through `providers.agy`; the current +dashboard editor does not expose that provider, so configure it in +`config.yaml`. ```yaml websearch: @@ -129,80 +117,84 @@ websearch: timeout: 55 ``` -Note: `enabled: false` stops provisioning the managed local `ccs-websearch.WebSearch` runtime. It does not re-enable Anthropic's native `WebSearch` for third-party backends. +The schema is +[`src/config/schemas/websearch.ts`](../src/config/schemas/websearch.ts), and +defaults are in +[`src/config/schemas/unified-config.ts`](../src/config/schemas/unified-config.ts). +Deprecated top-level WebSearch fields remain load-compatible but must not be +used for new configuration. -## Environment Variables +### SearXNG URL -| Variable | Description | -|----------|-------------| -| `EXA_API_KEY` | Enables Exa when `providers.exa.enabled: true` | -| `TAVILY_API_KEY` | Enables Tavily when `providers.tavily.enabled: true` | -| `BRAVE_API_KEY` | Enables Brave Search when `providers.brave.enabled: true` | -| `CCS_WEBSEARCH_SEARXNG_URL` | Runtime URL used when `providers.searxng.enabled: true` | -| `CCS_WEBSEARCH_SEARXNG_MAX_RESULTS` | Optional runtime override for SearXNG result count (clamped 1..10) | -| `GROK_API_KEY` | Required only for legacy Grok CLI fallback | -| `CCS_WEBSEARCH_SKIP` | Disable the CCS local WebSearch runtime for the current process; third-party launches still keep native Anthropic `WebSearch` disabled | -| `CCS_DEBUG` | Verbose WebSearch runtime logging | -| `CCS_WEBSEARCH_TRACE` | Write opt-in JSONL trace records under `~/.ccs/logs/websearch-trace.jsonl` | -| `CCS_WEBSEARCH_TRACE_FILE` | Override the trace file path (must stay inside `~/.ccs/`, your system temp directory, or `/var/log`) | +Configure the instance base URL, for example +`https://search.example.invalid`. Do not include `/search`, credentials, query +parameters, or a URL fragment. CCS normalizes the base and calls the JSON search +endpoint. -## Managed Runtime Files +### Dashboard-managed API keys -- `~/.claude.json` → CCS manages `mcpServers.ccs-websearch` -- `~/.ccs/mcp/ccs-websearch-server.cjs` → local MCP server binary -- `~/.ccs/hooks/websearch-transformer.cjs` → shared provider runtime plus legacy compatibility fallback +The dashboard stores supported provider keys in `global_env`. They are +available to WebSearch only when global environment injection is enabled. +Shell-provided environment values remain supported. Never place real keys in +documentation, tests, or committed configuration. -## Troubleshooting +## Managed Files -### WebSearch says "Ready (DuckDuckGo)" +In the default user layout, CCS manages: -That is expected. DuckDuckGo is the default zero-setup backend. +- `~/.claude.json` → `mcpServers.ccs-websearch` +- `~/.ccs/mcp/ccs-websearch-server.cjs` +- `~/.ccs/hooks/websearch-transformer.cjs` -### Exa, Tavily, or Brave is enabled but not ready +CCS installation and test paths can differ because configuration helpers honor +the active CCS and Claude home locations. Provisioning uses a lock and +preserves unrelated MCP server entries. Malformed Claude configuration is not +overwritten. -Set the matching API key in the WebSearch dashboard card, or export it in the environment that launches CCS, then refresh status: +## Runtime Environment -```bash -export EXA_API_KEY="your-api-key" -# or: export TAVILY_API_KEY="your-api-key" -# or: export BRAVE_API_KEY="your-api-key" -ccs config -``` +[`src/utils/websearch/hook-env.ts`](../src/utils/websearch/hook-env.ts) converts +the resolved configuration into runtime environment values. Provider API keys +remain environment inputs; boolean and limit variables describe provider +selection. -If the dashboard says the key is stored but still not ready, check whether `Settings -> Global Env` is disabled. WebSearch reuses that injection path for dashboard-managed keys. +Operational overrides: -### SearXNG is enabled but not ready +| Variable | Effect | +| --- | --- | +| `CCS_DEBUG` | Enables verbose diagnostics and WebSearch trace collection. | +| `CCS_WEBSEARCH_TRACE` | Enables WebSearch JSONL trace collection. | +| `CCS_WEBSEARCH_TRACE_FILE` | Requests a trace path within an allowed CCS log, system temporary, or `/var/log` boundary. | -1. Confirm the configured base URL is valid (for example `https://search.example.com`) -2. Confirm the instance exposes `GET /search?q=&format=json` -3. If the hook reports `SearXNG returned 403: format=json is disabled on this instance`, enable JSON format on that SearXNG deployment or switch to another backend +An unsafe trace-file override is ignored. Trace writes are best effort and do +not change launch or search results. -### I still want Gemini/OpenCode/Grok fallback +## Diagnostics -Those providers remain supported, but they are no longer the primary path. Enable them explicitly in `config.yaml` if you want them as last-resort fallback. +By default, trace records are written under +`~/.ccs/logs/websearch-trace.jsonl`. They correlate launch preparation, MCP +exposure, tool calls, provider attempts, provider success/failure, and session +summaries. -### I need to see whether CCS exposed WebSearch or the model bypassed it +Normal trace metadata uses a query fingerprint and length instead of the raw +query. Provider-failure records can also include `error` detail returned by a +provider implementation, including HTTP response excerpts or CLI stderr. +That provider-generated detail is not guaranteed to exclude query text or +other sensitive content unless the implementation redacts it. Treat the trace +file as sensitive operational data. -Run the launch with `CCS_WEBSEARCH_TRACE=1` (or `CCS_DEBUG=1`). CCS writes a JSONL trace to `~/.ccs/logs/websearch-trace.jsonl` with: +For delegated/headless sessions, a likely-bypass summary means the tool was +exposed but no WebSearch call occurred and another allowed tool path was used. -1. source-side launch records from CCS (`ccs_websearch_launch`) -2. MCP exposure and call records (`mcp_initialize`, `mcp_tools_list`, `mcp_tool_call_*`) -3. provider attempt and winner records (`websearch_provider_attempt`, `websearch_provider_success`) -4. session summaries (`mcp_session_summary`, and headless `headless_websearch_summary` when applicable) +When search is unavailable: -Queries are fingerprinted (`queryHash`, `queryLength`) instead of logged raw by default. For headless/delegation runs, `headless_websearch_summary.likelyBypassed=true` means the MCP tool was exposed, no WebSearch call occurred, and Claude fell back to `Bash` or `WebFetch`. +1. confirm `websearch.enabled` and at least one provider are enabled; +2. check readiness in the dashboard; +3. verify API keys or the SearXNG base URL without printing secrets; +4. enable `CCS_WEBSEARCH_TRACE=1` for one launch; +5. inspect provider attempt and cooldown events. -### WebSearch returns no results - -1. Check `websearch.enabled: true` -2. Keep DuckDuckGo enabled unless you have a strong reason to disable it -3. If using Exa, Tavily, or Brave, verify the matching API key -4. Run with `CCS_DEBUG=1` for runtime logs, or `CCS_WEBSEARCH_TRACE=1` for correlated launch/MCP/provider traces -5. If DuckDuckGo returns a non-result HTML error, retry later or enable another provider. CCS now treats that as a provider failure instead of a false empty result. - -## Security Considerations - -- API keys entered from the dashboard are stored in `~/.ccs/config.yaml` under `global_env` and injected as environment variables at runtime -- Shell-exported keys still work and are detected as external environment input -- Never commit API keys to version control -- Use the dashboard only on trusted machines, and protect `~/.ccs/config.yaml` with normal user-level filesystem permissions +Focused coverage lives under +[`tests/unit/utils/websearch/`](../tests/unit/utils/websearch/), +[`tests/unit/hooks/`](../tests/unit/hooks/), and +[`tests/unit/targets/settings-profile-websearch-launch.test.ts`](../tests/unit/targets/settings-profile-websearch-launch.test.ts).