Adds opt-in `ccsx auth import-default <name>` to migrate the existing
~/.codex/auth.json into a new profile, plus the cross-system integration
tests and user-facing documentation.
- import-default-command (C3 torn-write protection):
- readFileSync + JSON.parse with 3x retry / 100ms backoff to survive
Codex's truncate-then-write auth.json refresh race
- decode-id-token sanity-check on JWT shape (catches mid-write JWT
corruption that JSON.parse alone wouldn't notice)
- pgrep -f codex best-effort detection; warns + refuses without
--force-while-running flag if a live codex process is found
- rejects cliproxy-format auth files ({type: "codex", ...} wrapper)
with a clear "use ccs cliproxy ..." pointer
- atomic write to <dest>.tmp.<pid>.<rand> + rename
- --with-history defaults to false per D8 (auth-only is the safer
default; opt in for bulkier data)
- --force backs up existing auth.json to .bak-<ts> before overwrite
- non-destructive — never modifies ~/.codex/; legacy mode keeps
working without ever running this command
- integration tests:
- two-terminal-isolation: two profiles with separate CODEX_HOMEs
write to their own auth.json/history.jsonl with no crosstalk
- ccsxp-independence: codex-auth profile set; ccsxp still uses its
own CCSXP_CODEX_HOME / ~/.codex pool (H5 stderr notice present)
- legacy-fallback: no profiles registered → codex-runtime-router
leaves CODEX_HOME unset → codex falls back to ~/.codex
- import-default.integration: real fs copy + decode + register
- docs/codex-auth.md: user guide covering quick start, two-terminal
example, migration, dashboard, and caveats (cmd.exe, Windows
symlinks, ccsx vs ccsxp distinction)
155 codex-auth-scope tests green (45 Phase 1 + 57 Phase 2 + 19 Phase 3
+ 15 Phase 4 + 19 Phase 5). Full suite 3051/3082 — the 1 failure is a
pre-existing test-pollution issue between ccsxp-runtime.test.ts and
codex-runtime-integration.test.ts that exists on dev today; the test
passes in isolation.
Adds a read-only dashboard surface for the codex-auth profile registry.
Power users can see which profile is active and its decoded email/plan
without leaving the browser; mutations stay CLI-only.
- codex-auth-dashboard-service: builds the response shape (active +
default + profiles[]), reads each profile's auth.json, decodes id_token
via Phase 1 decoder (nested URI claims per C1), 5s in-memory single-key
cache plus exported invalidateCodexAuthProfilesCache() hook for
in-process Phase 2 callers (D7); strict field whitelist — id_token /
access_token / refresh_token NEVER appear in response body or logs
- GET /api/codex/profiles registered INSIDE the
requireLocalAccessWhenAuthDisabled middleware (H6) — emails are PII
and must not leak when dashboard is exposed remotely; integration
test asserts 403 from non-localhost origin
- NEW Auth Profiles tab (D5) in ui/src/pages/codex.tsx — distinct from
the existing codex-profiles-card (which edits config.toml [profiles],
a different concept); active profile + email + plan tier highlighted,
table of all profiles below, disabled Switch/Remove buttons redirect
to terminal commands
- accountId returned by API for power users (curl) but hidden from
the default UI (D6)
- 11 service unit tests + 4 endpoint integration tests, all green
Upgrades the previously-stub ccsx binary entry (src/bin/codex-runtime.ts)
into an argv router: `ccsx auth <cmd>` dispatches to the Phase 2 router;
any other argv resolves the active codex-auth profile and spawns codex
with CODEX_HOME pointed at the profile dir.
- resolve-active-profile: sync, hot-path-safe (<5ms), reads YAML
registry via Phase 1 helpers; precedence is CODEX_HOME (explicit)
> CCS_CODEX_PROFILE (env) > registry default > null (legacy
~/.codex fallback); fails open on any error (silent for missing
registry, stderr warn for corrupt/missing-profile)
- codex-runtime-router: extracted main() for testability; entry
script is a thin 3-line wrapper; returns -1 sentinel for the
CCS branch so the spawn lifecycle isn't terminated
- ccsxp-runtime: H5 defensive stderr notice when CCS_CODEX_PROFILE
is set, surfacing the boundary between codex-auth (native codex)
and ccsxp (cliproxy pool) without changing functional behavior;
CLIProxyAPI does not read CODEX_HOME so no pool contamination
possible
- 14 unit tests (8 resolver + 6 router); ccsxp regression suite
(5 tests) untouched and still green
Implements the user-facing surface for ccsx auth profile management.
After `ccsx auth create work` (auto-spawns codex login with CODEX_HOME
pinned per D11), users can `eval "$(ccsx auth use work)"` in any shell
to scope all subsequent codex invocations to that profile — letting
two terminals run two different Codex accounts concurrently.
- codex-auth-router: dispatches argv to subcommand handlers
- create: idempotent, --force re-links config.toml preserving auth.json
(D9), then auto-spawns codex login with CODEX_HOME pinned (D11);
filesystem ops happen before registry write to avoid registry orphans
on EACCES/ENOSPC
- login: standalone re-auth for an existing profile
- switch: persistent default in YAML registry
- use: STDOUT-DISCIPLINED — emits only shell-evalable exports;
bash/zsh/fish/PowerShell/cmd syntaxes via shell-detect; sets
CCS_NO_PRE_DISPATCH=1 at module load to suppress recovery/migration
banners that would otherwise contaminate eval (C2)
- show: list (active(missing) row at top per D14) + detail views
- remove: default-profile guard, active-shell warn, --yes / --force
- ASCII-only output, NO_COLOR honored, all errors to stderr via
exitWithError
- pre-dispatch.ts: early-return when CCS_NO_PRE_DISPATCH=1, placed
before autoMigrate which is itself a stdout writer
57 unit tests, all green. Help text cross-references the ccsxp/ccsx
distinction since the binaries differ by one character (H5).
Adds the storage substrate for ccsx auth profile isolation.
Each Codex profile gets its own CODEX_HOME dir under
~/.ccs/codex-instances/<name>/ with isolated auth.json and
history.jsonl; config.toml is shared via symlink to ~/.codex/config.toml
so two terminals can run two Codex accounts simultaneously without
duplicating user config.
- CodexProfileRegistry (YAML, atomic write tmp.<pid>.<rand> + rename,
orphan cleanup, full CRUD + default pointer)
- decode-id-token: pure base64 JWT decoder for OpenAI id_token,
reads nested https://api.openai.com/auth claims (chatgpt_plan_type,
chatgpt_account_id) and dual-path email
- ensureSharedConfigSymlink: self-healing, idempotent, overwrites
stale entries with stderr warning
- 45 unit tests, all green
Foundation only — no CLI, no runtime injection, no dashboard.
Subsequent commits wire those in.
* fix(ci): breaking-change-guard skips missing base files (CI-1)
Guard each git-show call with git cat-file -e existence check before
attempting to read docker/compose.yaml from the base branch. When the
file doesn't exist on the base (new file in this PR), the script would
crash with "fatal: path exists on disk but not in origin/dev". New
files can't cause a contract regression, so we exit early with
breaking=0.
* style: prettier reformat unrelated drift (CI-2)
Two files had minor formatting drift from earlier umbrella PRs:
- src/cliproxy/quota/quota-manager.ts
- src/management/checks/image-analysis-check.ts
No logic changes — formatter-only pass to unblock CI format:check.
* fix(test): update trusted-author gate count to 4 in ci-workflow test (CI-3)
PR #1260 added a compose-parity job to ci.yml. That job runs on
self-hosted runners using PR-provided checkout, so it legitimately
requires the trusted-author guard (same as validate, build, test).
The test expected 3 occurrences; the correct count is now 4:
validate (matrix), build, test, compose-parity.
* fix(ci): smoke-test passes compose path + image ref to network-contract (REV-1)
network-contract.sh signature is: <compose-file> [image-ref]
The smoke-test job was calling it as:
bash tests/docker/network-contract.sh "${{ steps.image.outputs.ref }}"
which placed the image ref in the compose-file position ($1), causing
the script to try docker compose -f <image-ref> which fails.
Corrected to:
bash tests/docker/network-contract.sh docker/compose.yaml "${{ steps.image.outputs.ref }}"
Also removes publish-dashboard from smoke-test.needs (REV-2): when
publish-dashboard is SKIPPED on prerelease events, GitHub Actions
propagates the skip to downstream jobs, so smoke-test and
promote-mutable-tags were silently skipped on every rc.N publish.
smoke-test only verifies the integrated image; it has no dependency
on the legacy dashboard image job.
* docs(docker): annotate /root/.ccs path in compose volume (REV-3 clarification)
The reviewer raised a concern that the compose volume mounts /root/.ccs
but the entrypoint might default to /home/node/.ccs. This is a false
positive: the integrated image uses entrypoint-integrated.sh (not
entrypoint.sh), which runs under supervisord with user=root and
explicitly mkdir -p /root/.ccs. HOME is /root inside the container.
The volume mount at /root/.ccs is correct.
Added an inline comment documenting the reasoning so future reviewers
do not confuse entrypoint.sh (legacy dashboard image) with
entrypoint-integrated.sh (integrated image).
* fix(ci): gate docs-parity pull_request job to trusted authors
docs-parity.yml runs on a self-hosted runner and checks out PR code.
The self-hosted-runner-policy test requires any such workflow to include
the trusted-author guard. The workflow was missing the guard, causing
bun test:fast to fail with 1 failure.
Allow push events (no author check needed — push is to own branch)
and trusted-contributor PRs only.
Add "Connect your app to CLIProxy" section to docker/README.md with:
- Public contract table (network=ccs-net, service DNS=ccs, ports 8317/3000)
- Pattern A: same compose file with external network reference
- Pattern B: docker run --network ccs-net
- Troubleshooting subsection (DNS, missing network, conflict, Podman, MTU)
Add one-line link in README.md pointing to the new section.
Add CHANGELOG entry under Unreleased noting the contract as SemVer-major stable.
Add CONTRIBUTING.md note that changing services.ccs or networks.ccs-net requires major bump.
Squash merge PR #1240.\n\nValidated locally from the PR head because trusted-author GitHub CI was skipped for this fork PR:\n- bun run validate\n- bun run build:all\n- bun run test:all\n- CCS_E2E_SKIP_BUILD=1 bun run test:e2e
Closes#1218.
When the user invokes `ccs <profile> agents` (and other Claude subcommands
like `mcp`, `doctor`, `plugin`, ...), CCS was unconditionally injecting
session-only Claude flags (`--append-system-prompt`, `--disallowedTools`,
`--settings`, official-channels plugin specs) and forwarding the
`DISABLE_TELEMETRY=1` env var from profile settings. Each of those
either errored out the subcommand (`error: unknown option
'--append-system-prompt'`) or silently flipped Claude into
non-interactive list mode, so the new `claude agents` agent view never
opened under CCS.
Add a small `claude-subcommand-detector` that recognizes the documented
Claude subcommand set after skipping known value-taking flags, then have
the three steering-prompt injectors (websearch, image-analysis, browser),
the cliproxy and settings launchers, and the official-channels plan
short-circuit when a subcommand invocation is detected. Also strip
`DISABLE_TELEMETRY` from the spawned env only for subcommand
invocations — upstream Claude Code uses that var as a kill switch for
the subcommand TUIs, and the user's telemetry preference still applies
to every normal interactive session.
Verified end-to-end against `ccs glm agents` and `ccs ck agents`: the
agent view opens correctly, no `unknown option` error, and all 1938
existing tests pass.
The dashboard Add Account flow hits POST /api/cliproxy/auth/:provider/start-url
which fetches the OAuth consent URL directly from the CLIProxy Plus
management API. With cliproxy.backend=plus and unset
CLIPROXY_{GEMINI,ANTIGRAVITY}_OAUTH_CLIENT_ID/SECRET, Plus returns a URL
with empty client_id= and Google rejects with 400 invalid_request. PR
#1131 only guarded the CLI /start path; /start-url was unguarded.
- Pre-fetch guard via getPlusOAuthCredentialError -> 400
plus_oauth_credentials_missing before contacting the Plus binary.
- Post-fetch guard via getPlusAuthUrlCredentialError -> 502
plus_oauth_url_missing_client_id if Plus still emits a URL without
client_id (logged with query string redacted).
- New integration test (cliproxy-auth-routes-oauth-guard.test.ts) covers
gemini/agy guard firing, non-table providers passing through, and both
error body contracts. 15 cases pass.
Closes#1208
Refactor getGeminiPlusOAuthCredentialError / getGeminiAuthUrlCredentialError
behind a provider-table-driven helper (PLUS_OAUTH_ENV_BY_PROVIDER) and add
getPlusOAuthCredentialError / getPlusAuthUrlCredentialError exports covering
both gemini and agy. Existing gemini-named exports preserved as aliases so
PR #1131 test surface remains unchanged. New AGY-parity test cases mirror
the original gemini diagnostics. CLI call sites in triggerOAuth /
handlePasteCallbackMode stay gemini-only; dashboard handler in the
follow-up commits will use the generalized exports for both providers.
Refs #1208
* fix(dispatcher): preserve anthropic auth env for settings profiles on non-proxy path
API profiles whose `ANTHROPIC_BASE_URL` is classified as `'anthropic'`
(anthropic.com, paths containing `/anthropic`, ollama.com) skip the
local OpenAI-compat proxy. The non-proxy launch path stripped
`ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_API_KEY`
from the subprocess env without re-injecting them, so Claude Code
launched with no routing/auth in `process.env` and failed with
`Not logged in - Please run /login`. The `--settings` env block does
not satisfy Claude Code's auth check.
Pre-existing for `anthropic.com` and `/anthropic` profiles. Newly
broken in v7.77.0 for Ollama Cloud profiles - PR #1175 reclassified
`ollama.com` from `generic-chat-completion-api` to `anthropic`,
moving it from the proxy path onto this broken non-proxy path.
Fix by extending `stripAnthropicRoutingEnv` with an optional
`preserveFrom` parameter. Routing keys present in `preserveFrom`
survive the strip (with values from `preserveFrom`). Settings-type
profiles pass their own `settings.env` as the preserve source so
routing they explicitly supplied is kept while routing leaked from
the parent shell or `global.env` is dropped.
Wired into both call sites:
- `headless-executor.ts` (the `-p` headless executor)
- `settings-flow.ts` (the interactive flow, which then calls
`execClaude` - whose own `stripAnthropicRoutingEnv` pass on the
merged env now also takes `envVars` as the preserve source so the
caller-supplied routing is not stripped a second time before spawn)
Native Anthropic / Bedrock / Vertex profiles are unaffected (they
don't put routing keys in `settings.env`). The OpenAI-compat proxy
path is unaffected because `buildOpenAICompatProxyEnv` overrides
`BASE_URL` / `AUTH_TOKEN` with localhost values and explicitly deletes
`API_KEY` after this strip.
Replaces the Apr 21 defensive double-strip test (which was the
mechanism causing this regression on the interactive path) with a
test asserting the new contract: caller-supplied routing in `envVars`
survives, parent-process routing is still stripped.
* test: harden anthropic settings env preservation coverage
* fix: preserve explicit blank anthropic routing env
---------
Co-authored-by: Tam Nhu Tran <kaitran.ntt@gmail.com>
Resolve the dispatcher and dashboard conflicts from origin/dev while preserving the configured local CLIProxy port behavior. Also harden lifecycle port fallback and wait for the actual stopped port during binary installs.
The normalizers extraction in #1168 introduced a circular module-load
chain that crashed the built CLI:
ccs.js -> errors -> services/logging -> log-config -> config-loader-facade
-> unified-config-loader -> loader/normalizers -> channels/official-channels-runtime
-> utils/claude-detector -> utils/shell-executor -> utils/websearch-manager
-> utils/websearch/hook-env -> utils/websearch/trace
-> services/logging (mid-load - createLogger undefined) -> CRASH
normalizers only needed three pure helpers from official-channels-runtime
(isOfficialChannelId, normalizeOfficialChannelIds, resolveLegacyDiscord
Selection) but pulled in the whole file's claude-detector / shell-
executor / websearch chain.
Fix: extract those three helpers + OFFICIAL_CHANNEL_IDS into a leaf
module src/channels/official-channels-ids.ts with no runtime deps.
Update normalizers.ts and config-getters.ts to import from the leaf.
official-channels-runtime.ts re-exports from the leaf for callers that
still want the bundled API.
Also revert the over-eager facade-import migration in
src/utils/config-manager.ts (it was importing from config-loader-facade
which re-exports from itself, creating a direct cycle).
Verified: dist/ccs.js boots cleanly (--version returns); test:all
1828/1828 pass; typecheck/lint/format clean.
Refs #1135