Commit Graph
891 Commits
Author SHA1 Message Date
Kai (Tam Nhu) TranandGitHub 100308a3cf fix(logging): redact CLI argv in lifecycle start logs
Squash merge PR #1297 into dev.
2026-05-19 07:57:43 -04:00
Kai (Tam Nhu) TranandGitHub 857dac8751 fix(docker): harden integrated service exposure
Squash merge PR #1291 into dev.
2026-05-19 07:45:20 -04:00
Kai (Tam Nhu) TranandGitHub 3b2016462a fix(persist): block codex claude settings bridge 2026-05-19 07:32:04 -04:00
Kai (Tam Nhu) TranandGitHub 8a37578702 fix(codex): pass native ccsx subcommands through (#1290) 2026-05-18 10:48:44 -04:00
Kai (Tam Nhu) TranandGitHub 5a54c1b536 fix(codex): pass ccsx resume through to native codex
Closes #1287
2026-05-18 09:45:20 -04:00
Kai (Tam Nhu) TranandGitHub 9aa854bc59 feat(docker): zero-install Docker UX — ccs:latest, ccs:full, canonical compose, ccs-net contract (#1251) (#1261)
* feat(docker): publish ccs:latest + ccs:full integrated images (P1)

* feat(docker): parameterize Dockerfile.integrated with ARG FLAVOR=minimal|full

Add FLAVOR build arg that gates the AI CLI install layer (claude-code,
gemini-cli, grok-cli, opencode) so one Dockerfile produces both the
minimal (< 350 MB) and full (< 600 MB) integrated images.

Use BuildKit cache mount for /root/.npm to speed up repeated builds.
Part (P1 — publish integrated images).

* feat(docker): emit startup deprecation warning in legacy ccs-dashboard entrypoint

Prepend a [WARN] line to stderr on every container start so operators
running ghcr.io/kaitranntt/ccs-dashboard:latest are notified to migrate
to ghcr.io/kaitranntt/ccs:latest. Sunset window: 2 releases. See .

* test(docker): add image-size.sh budget assertion + unit test suite

image-size.sh: asserts docker image inspect .Size against a byte budget.
 Usage: image-size.sh <image:tag> <max-bytes>
 Budgets: minimal=350 MB (367001600), full=600 MB (629145600)

image-size-logic.test.sh: 6 mock-docker unit tests covering pass/fail
boundaries, bad arg count, and non-integer input. All 6 pass locally.
Part (P1).

* ci(docker): extend docker-release.yml to publish ccs:latest and ccs:full

- Add publish-integrated job with matrix flavor: [minimal, full]
 - minimal -> ghcr.io/kaitranntt/ccs:<ver> + :latest (when promoted)
 - full -> ghcr.io/kaitranntt/ccs:full-<ver> + :full (when promoted)
 - Multi-arch: linux/amd64 + linux/arm64 via buildx
 - Scoped GHA cache per flavor to avoid cross-contamination

- Add promote_to_latest workflow_dispatch input (default false)
 - rc.1 soak: first publish only pushes immutable version tag
 - Mutable :latest/:full promoted only on release event OR explicit opt-in

- Add smoke-test job (post-publish) for each flavor
 - Pulls the just-published version tag
 - Asserts image size via tests/docker/image-size.sh
 - Boots container, waits for healthcheck, probes :3000 and :8317

- Keep publish-dashboard job unchanged (legacy 2-release sunset)
 - Updated labels to note deprecation status

All jobs run on self-hosted cliproxy runners. Part (P1).

* docs(docker): document new image tags, add ccs-dashboard deprecation notices

docker/README.md:
- Add "Choosing an image" table (ccs:latest / ccs:full / ccs-dashboard deprecated)
- Update Quick Start section to use ccs:latest as primary example
- Add legacy image note with sunset timeline

CHANGELOG.md:
- Add Unreleased > ### Deprecated entry for ccs-dashboard:latest
 pointing to migration path and

README.md:
- Add one-line deprecation banner near top routing users to ccs:latest

Part (P1).

* refactor(docker): drop Bun from runtime stage; generate npm lockfile in build stage

- Build stage keeps Bun for fast installs; appends `npm install --package-lock-only`
 to generate an ephemeral package-lock.json immediately after `bun run build:all`.
- Runtime stage removes BUN_VERSION ARG/ENV, the bun.sh curl install, and
 `bun install --frozen-lockfile --production`. Replaces with:
 COPY --from=build /app/package.json /app/package-lock.json ./
 RUN npm ci --omit=dev --ignore-scripts
- --ignore-scripts rationale: postinstall (scripts/postinstall.js) writes ~/.ccs/
 config — not needed in Docker context. bcrypt v6+ is pure-JS, no native compile.
- package-lock.json already in .gitignore (line 33); never committed.
- docker/Dockerfile.integrated unchanged — no Bun present there (alpine + npm).
- Targets: image size reduction >= 300 MB by eliminating ~130 MB Bun binary + installer.

* feat(docker): canonical compose.yaml + smoke-test (P2)

* feat(docker): add canonical compose.yaml for zero-install flow

* ci(docker): smoke-test ccs.kaitran.ca/docker-compose.yaml on release + nightly

* docs(docker): document ccs-net contract for sibling containers

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.

* docs(docker): P3 — hoist two-command quickstart, restructure docker/README, add parity CI

* docs: hoist Docker zero-install quickstart above npm install path

- Create docs/quickstart-snippet.md as canonical source for the
 two-command flow (curl + docker compose up -d), wrapped in
 <!-- quickstart-snippet-start/end --> markers
- Hoist the snippet into README.md immediately below the deprecation
 banner, above all other install paths
- Rename old npm-only "## Quick Start" to "## Install on Host (npm)"
 and move it below the Docker quickstart

* docs(docker): restructure README with zero-install first and migration section

- Reorder top-level sections: zero-install (canonical snippet with
 markers), choosing an image, power-user ccs docker, prebuilt image,
 connect your app to CLIProxy, migration, env vars, troubleshooting
- Add deprecation banner at the top pointing at the migration section
- Add ## Migration from ccs-dashboard:latest section with step-by-step
 instructions covering compose down, data preservation, named volume
 vs bind-mount path, and compose up with the new image
- Keep P1's Choosing an image table and P5's Connect Your App to
 CLIProxy section intact, just repositioned

* test(docs): parity check for quickstart snippet across README files

Assert README.md and docker/README.md both contain the canonical
quickstart block verbatim, anchored by marker comments. Exits non-zero
and prints a diff on any drift.

* ci(docs): wire quickstart-parity test on push and PR

Runs tests/docs/quickstart-parity.sh on self-hosted runner whenever
docs/quickstart-snippet.md, README.md, docker/README.md, or the
test/workflow files themselves change. Fails fast on snippet drift.

* fix(docker): apply red-team findings — drop :full, rc.1 soak, healthcheck, signing

* docs(quickstart): fix raw URL for corporate-proxy fallback (H1)

* feat(docker)!: drop :full image variant — use sibling containers on ccs-net (Q3)

No AI CLIs (claude-code/gemini-cli/grok-cli/opencode) are bundled in the image.
Use sibling containers attached to ccs-net instead.
See docker/README.md#connect-your-app-to-cliproxy.

Also removes bash from apk deps (entrypoint uses #!/bin/sh — L2).

ci(docker): publish only immutable :<ver> tag pre-smoke; promote-mutable-tags
job adds :latest/:MAJOR/:MINOR aliases only after smoke tests pass (H3)

ci(docker): smoke-test-compose-url runs network-contract.sh against the
downloaded /tmp/ccs-compose.yaml instead of re-cloning the repo (H4)

ci(docker): sign published images with cosign keyless OIDC + attach
provenance/SBOM via build-push-action (M8)

test(docker): network-contract.sh now accepts compose-file and image-ref
positional args; replaces python3 healthcheck parser with jq (L4)

* chore(release): cut every main release as rc.N prerelease, manual promote flow (H2)

- .releaserc.cjs: main branch now uses prerelease 'rc' channel — every
 semantic-release cut becomes vX.Y.Z-rc.N
- add promote-release.yml: workflow_dispatch flips rc → stable via
 'gh release edit --prerelease=false'; triggers docker promote-mutable-tags
- add docs/release-process.md: full soak + promote procedure, rollback steps,
 cosign verification command
- releaseNotesGenerator: add revert section, document chore hidden behaviour (L8)

* fix(docker): healthcheck probes both dashboard and cliproxy ports (M1)

compose.yaml healthcheck now checks :3000 and :8317 concurrently with
a 4.5s internal timeout, within Docker's 5s timeout budget.

Also:
- docs(docker): document npm lockfile ephemeral tradeoff above install
 layer; note size-budget regression test as the practical safeguard (M3)
- docs(docker): drop :full row from Choosing an image table; add sibling
 container note pointing to connect-your-app-to-cliproxy (Q3/docs)
- docs(docker): remove :full docker run block; fix release-tag sentence (Q3)
- docs(docker): fix raw URL in migration section (H1 parity)
- docs(docker): add Volume warning — 'down -v' deletes named volumes (L13)
- docs(docker): update What changes table — remove :full reference (Q3)
- docs(docker): add Image Signatures and SBOM section with cosign verify
 and sbom download commands (M8/docs)
- changelog: add Unreleased entries for rc soak, cosign signing, :full
 removal with migration guidance

* ci(docker): assert image-size budget per platform; add compose parity + breaking-change guard (M6/L9/L12)

- image-size.sh: add --platform flag; uses 'docker buildx imagetools
 inspect' to sum compressed layer sizes from registry manifest for
 linux/amd64 and linux/arm64 separately (M6)
- docker-release.yml smoke-test: runs size check for both platforms
- compose-parity.sh: diffs docker/compose.yaml vs
 docker/docker-compose.integrated.yml for image name, ports 3000/8317,
 volume mounts /root/.ccs and /var/log/ccs, ccs-net definition (L12)
- ci.yml: add compose-parity job wired to cliproxy runner (L12)
- breaking-change-guard.yml: fails PR if compose.yaml changes image name,
 network name, or container_name without a feat!/fix! commit (L9)

* chore(ci): fix cosign shell substitution — use tr instead of bash @L expansion (L6/nit)

* test(docker): fix compose-parity port regex for variable-interpolated host ports

* fix: address upstream reviewer findings + failing CI checks (loop 1)

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

* fix: address reviewer findings round 2 (loop 2)

* fix(docker): parameterize image with CCS_IMAGE env so smoke-test exercises new digest

docker/compose.yaml hardcoded image: ghcr.io/kaitranntt/ccs:latest, causing
network-contract.sh to always use the old published image regardless of what
IMAGE_OVERRIDE was passed. Switching to \${CCS_IMAGE:-ghcr.io/kaitranntt/ccs:latest}
lets CI pass CCS_IMAGE=<digest> so the smoke-test actually exercises the
just-built image. Default end-user behaviour is unchanged.

Resolves REV4 (reviewer loop 2, PR).

* fix(test): image-size.sh fails loudly on manifest inspection failure

The --platform branch previously exited 0 when imagetools inspect returned
empty or zero bytes, silently passing the budget check. A broken image
could reach production undetected if buildx had any inspection issue.

Changed to exit 1 with a clear diagnostic message explaining possible causes
(old buildx, manifest format mismatch, image not yet pushed). No --allow-inspect-failure
escape hatch is provided; CI must fix the root cause.

Resolves REV5 (reviewer loop 2, PR).

* test(docker): cover --platform branch in image-size-logic tests

Prior tests only exercised the local docker image inspect path. The
--platform branch (imagetools inspect) had zero coverage, meaning the
REV5 silent-pass regression would not have been caught by CI.

Added 5 mock-based test cases for the --platform branch:
- pass when platform-scoped compressed size < budget
- fail when platform-scoped compressed size > budget
- fail (exit 1) when imagetools inspect errors (REV5 guard)
- fail (exit 1) when reported size is "0" (REV5 guard)
- fail (exit 1) when size output is empty (REV5 guard)

Mocks follow the existing pattern (override docker in PATH with a temp
wrapper), extended to handle buildx imagetools inspect subcommand.

Resolves REV6 (reviewer loop 2, PR).

* fix(test): compose-parity image_name() handles \${VAR:-default} syntax

After parameterizing compose.yaml's image field with \${CCS_IMAGE:-...},
the image_name() sed pipeline cut at the first colon in the shell variable
syntax (:-) instead of the tag separator, returning '\${CCS_IMAGE' and
failing the kaitranntt/ccs grep.

Added a sed step to unwrap \${VAR:-default} by extracting just the default
value before stripping the tag. Existing plain image: name:tag references
are unaffected.

* fix: tighten parity check + catch service-key rename (loop 3)

* fix(test): tighten compose-parity image-name match to exact expected values (REV7)

Replace loose substring grep (grep -q "ccs") with exact equality checks
against declared EXPECTED_CANONICAL_IMAGE and EXPECTED_INTEGRATED_IMAGE
constants. The integrated compose builds locally as ccs-cliproxy:latest
(not ghcr.io/kaitranntt/ccs), so both expected names are explicitly
documented at the top of the assertion block.

Fixes: a drift in integrated compose to a wrong owner/registry could
slip through the old "grep -q ccs" check; now any name other than the
declared constant is a hard failure.

* feat(ci): breaking-change-guard catches services.ccs rename — public DNS contract (REV8)

Docker's service-name DNS uses the compose service KEY as the hostname.
Sibling containers on ccs-net reach CCS via http://ccs:8317; renaming
services.ccs: to anything else silently breaks that contract even when
image name, network name, and container_name are unchanged.

Add check 4 to the guard:
- Extract top-level service keys from both base and HEAD versions of
 docker/compose.yaml using awk (no external YAML parser required).
- Fail if the "ccs" key is absent from HEAD.
- Fail if the sorted set of service keys differs from base.

Wraps inside the existing git cat-file guard so new-file PRs skip it.

* fix(ci): smoke-test failure check + relax service-key guard (REV9, REV10)

REV9 — docker-release.yml smoke-test: add HEALTHY flag to the boot-and-wait
loop. Previously a timeout (status stays 'starting'/'missing' for all 12
iterations) exited the loop silently, letting port probes be the only net.
Now if the loop exits without HEALTHY=1 the step fails immediately with
container logs and state dump. network-contract.sh already has the HEALTHY
pattern — no change needed there.

REV10 — breaking-change-guard.yml: drop the OLD_KEYS != NEW_KEYS comparison.
That check treated ANY change to the service-key set as breaking — including
adding a harmless sidecar. The DNS contract only requires services.ccs to
exist; other services are irrelevant to the 'ccs' hostname on ccs-net. Keep
only the "ccs key must exist" check; remove the unused OLD_KEYS extraction.

* fix(release)!: decouple npm @latest from Docker rc.1 soak (REV11)

Reverts the loop-1 prerelease channel that was publishing npm as
vX.Y.Z-rc.N to the `rc` dist-tag instead of `latest`.

`main` is now a stable semantic-release channel again: every merge
publishes vX.Y.Z immediately to npm @latest. The rc.1 soak window
that guards Docker mutable tags is moved entirely into the Docker
publish workflow:

- `.releaserc.cjs`: remove `prerelease: 'rc'` from productionConfig,
 restore `branches: ['main']` (stable). Restore full successComment
 and `released` label. Keep loop-1 releaseNotesGenerator additions
 (revert section, breaking change comment).

- `docker-release.yml`: every `release: published` event publishes
 only the immutable `:<ver>` Docker tag. `promote-mutable-tags` job
 now gates exclusively on `workflow_dispatch` with
 `promote_to_latest=true` — no longer triggered automatically by
 non-prerelease release events.

- `promote-release.yml`: rewritten as a dispatch wrapper that validates
 the stable tag exists and is not a prerelease, verifies the immutable
 Docker image is in the registry, then dispatches docker-release.yml
 with `promote_to_latest=true`. Removes the `gh release edit
 --prerelease=false` approach that required the rc soak to be wired
 through GitHub release state.

- `docs/release-process.md`: updated to reflect the decoupled model —
 npm @latest is immediate; Docker :latest requires manual promote after
 soak. Documents the `why` split between npm and Docker soak windows.

* fix(ci): reviewer loop 6 — REV12 compose image parsing, REV13 fork bypass, REV14 :full audit

* fix(ci): breaking-change-guard handles \${VAR:-default} compose image syntax (REV12)

The sed 's/:.*//' pattern truncated at the first colon in the
\${CCS_IMAGE:-ghcr.io/...} expression, yielding "\${CCS_IMAGE" as the
image name instead of the actual registry path. Any change to the default
image namespace was therefore undetectable.

Introduce extract_image_name() that first strips the \${VAR:-default}
wrapper with a sed -E expression, then strips only the trailing :tag
suffix using a pattern that preserves internal colons (e.g. registry:5000/
owner/repo). Applied to both OLD_RAW and NEW_RAW extraction paths.

* fix(ci): breaking-change-guard runs on ubuntu-latest to cover forked PRs (REV13)

The trusted-author gate (COLLABORATOR|MEMBER|OWNER) caused forked-PR
contributors to bypass the breaking-change check entirely. A forked
contributor could rename services.ccs or change the image namespace
without a feat!/fix! marker and the guard would never run.

This workflow is a documented exception to the self-hosted-first policy:
it performs ONLY pure YAML diff parsing (git show / awk / sed). No build,
install, or arbitrary PR-branch scripts are executed. The checkout uses
persist-credentials: false. There is no untrusted code execution, so
ubuntu-latest is safe and necessary for universal fork coverage.

Update self-hosted-runner-policy.test.ts to:
- Introduce GITHUB_HOSTED_RUNNER_EXCEPTIONS registry with required
 justification comments for each entry
- Skip exception workflows in the "keeps active workflows on local runners"
 and "gates pull-request workflows" assertions
- Add a new "documented exceptions use github-hosted runners" test that
 verifies each exception entry actually uses a GitHub-hosted runner
 (prevents stale entries accumulating without cleanup)

* docs(ci): clarify rc.1 soak and :full removal as intentional design

Reviewer kept flagging :full omission and stale :latest as bugs. Both are intentional per Q3 maintainer decision and the rc.1 soak design (loop-5). Strengthen inline comments to make this unmistakable to future readers.

* fix: address final-review criticals — healthcheck/digest/contract guards (loop 8)

* fix(docker): healthcheck fails on 4xx not only 5xx (H1)

Change threshold from >= 500 to >= 400 so a misrouted 404 on / is
treated as unhealthy. A container returning 4xx on the root path is
not serving traffic correctly and must not be reported healthy.

* chore(docker): pin eceasy/cli-proxy-api base image by digest (H2)

FROM eceasy/cli-proxy-api:latest@sha256:4fa722b... locks the base image
to a specific manifest-list digest for reproducible builds. The human-
readable :latest tag is preserved for readability. Refresh cadence:
quarterly or on security advisory from upstream. apk packages (nodejs,
npm) remain unpinned — noted as a known limitation until a CI workflow
for apk-digest pinning exists.

* fix(ci): breaking-change-guard catches network rename and hostname override (M1/M2)

Replace the old diff-based ccs-net check (which missed the case where
both old and new lack the name, producing identical empty strings) with
a positive assertion: the canonical compose MUST contain 'name: ccs-net'.
This catches renames, removals, and whitespace variations.

Also add a hostname: override guard (M2): setting services.ccs.hostname
changes the Docker DNS name on ccs-net even when the service key stays
"ccs", silently breaking http://ccs:8317 for sibling containers.

* fix(test): compose-parity image-name regex preserves registry:port (M5)

The old sed 's/:.*//' truncated at the first colon, incorrectly stripping
registry:port prefixes (e.g. registry.local:5000/owner/repo:tag became
registry.local). Replace with sed 's|:[^:/]*$||' which strips only the
trailing :tag suffix, preserving internal colons.

Move tr -d ' ' before the sed -E wrapper-strip so the '^' anchor matches
correctly after whitespace removal. Mirrors extract_image_name() in
breaking-change-guard.yml.

Add 3 image_name() regression tests to image-size-logic.test.sh covering
plain tag, env-var-with-default syntax, and registry:port/owner/repo:tag.

* ci(pr-agent): disable ticket analysis to avoid sub-issue crash

PR-Agent's fetch_sub_issues crashes when the PR body references PR numbers as shortcodes (etc) — GraphQL repository.issue(number) returns null for PR numbers and pr-agent dereferences without a guard (github_provider.py:1048). Disable ticket_analysis_review until upstream fixes the None guard. Other reviewer sections (security, score, tests, etc.) remain enabled.

* ci(pr-agent): re-enable ticket analysis after history+body rewrite

PR shortcodes removed from all commit messages and PR body, so fetch_sub_issues no longer hits the upstream null-deref bug. Ticket compliance review is re-enabled in full.
2026-05-17 19:54:48 -04:00
Tam Nhu Tran e461c7a67e fix(codex-auth): address persistent review focus 2026-05-17 18:33:20 -04:00
Tam Nhu Tran 81c4acc73a fix(codex-auth): harden registry fail-closed paths 2026-05-17 18:20:56 -04:00
Tam Nhu Tran 54738e88f7 fix(codex-auth): surface registry corruption 2026-05-17 18:02:13 -04:00
Tam Nhu Tran c90b3cb9da fix(codex-auth): reject stray profile args 2026-05-17 17:51:32 -04:00
Tam Nhu Tran 85521018bf fix(codex-auth): close local review gaps 2026-05-17 17:41:23 -04:00
Tam Nhu Tran 2cd2d43186 fix(codex-auth): fail closed on registry corruption 2026-05-17 17:14:54 -04:00
Tam Nhu Tran 211e51b949 fix(codex-auth): address review focus areas 2026-05-17 17:02:59 -04:00
Tam Nhu Tran 5c79df4311 fix(codex-auth): close remaining review gaps 2026-05-17 16:51:22 -04:00
Tam Nhu Tran 08fe63c626 fix(codex-auth): fail fast on missing active profile 2026-05-17 16:06:32 -04:00
Tam Nhu Tran ea421c4a20 chore: merge origin/dev into codex auth profile branch 2026-05-17 15:33:06 -04:00
Tam Nhu Tran a3fe2c63d8 fix(codex-auth): harden profile review findings 2026-05-17 15:32:29 -04:00
Tam Nhu Tran 631c799322 feat(codex-auth): add import-default migration + integration tests + docs
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.
2026-05-17 14:46:16 -04:00
Tam Nhu Tran e99c4612a8 feat(codex-auth): add dashboard Auth Profiles tab + GET /api/codex/profiles
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
2026-05-17 14:45:35 -04:00
Tam Nhu Tran 8c604a040f feat(codex-auth): wire ccsx bin router + ccsxp scope notice
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
2026-05-17 14:45:13 -04:00
Tam Nhu Tran bf92645b35 feat(codex-auth): add ccsx auth CLI subcommands (create/login/switch/use/show/remove)
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).
2026-05-17 14:44:44 -04:00
Tam Nhu Tran 358b703d98 feat(codex-auth): add profile registry + storage foundation
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.
2026-05-17 14:44:07 -04:00
Kai (Tam Nhu) TranandGitHub d68ee37590 fix(codex): preserve custom ccsxp cliproxy base URLs
Preserve valid custom model_providers.cliproxy.base_url values during ccsxp Codex provider repair while keeping local fallback repair for missing or invalid URLs.\n\nCloses #1281
2026-05-17 06:59:09 -04:00
Kai (Tam Nhu) TranandGitHub e7ce699dc2 fix(ci): reviewer loop 6 — REV12 compose image parsing, REV13 fork bypass, REV14 :full audit (#1278)
* fix(ci): breaking-change-guard handles \${VAR:-default} compose image syntax (REV12)

The sed 's/:.*//' pattern truncated at the first colon in the
\${CCS_IMAGE:-ghcr.io/...} expression, yielding "\${CCS_IMAGE" as the
image name instead of the actual registry path. Any change to the default
image namespace was therefore undetectable.

Introduce extract_image_name() that first strips the \${VAR:-default}
wrapper with a sed -E expression, then strips only the trailing :tag
suffix using a pattern that preserves internal colons (e.g. registry:5000/
owner/repo). Applied to both OLD_RAW and NEW_RAW extraction paths.

* fix(ci): breaking-change-guard runs on ubuntu-latest to cover forked PRs (REV13)

The trusted-author gate (COLLABORATOR|MEMBER|OWNER) caused forked-PR
contributors to bypass the breaking-change check entirely. A forked
contributor could rename services.ccs or change the image namespace
without a feat!/fix! marker and the guard would never run.

This workflow is a documented exception to the self-hosted-first policy:
it performs ONLY pure YAML diff parsing (git show / awk / sed). No build,
install, or arbitrary PR-branch scripts are executed. The checkout uses
persist-credentials: false. There is no untrusted code execution, so
ubuntu-latest is safe and necessary for universal fork coverage.

Update self-hosted-runner-policy.test.ts to:
- Introduce GITHUB_HOSTED_RUNNER_EXCEPTIONS registry with required
  justification comments for each entry
- Skip exception workflows in the "keeps active workflows on local runners"
  and "gates pull-request workflows" assertions
- Add a new "documented exceptions use github-hosted runners" test that
  verifies each exception entry actually uses a GitHub-hosted runner
  (prevents stale entries accumulating without cleanup)
2026-05-17 06:01:25 -04:00
Kai (Tam Nhu) TranandGitHub db175b6807 fix(security): require localhost for Claude extension /setup when dashboard auth is disabled (#1270)
* fix(security): restrict Claude extension setup endpoint

* style: apply prettier formatting
2026-05-16 13:58:57 -04:00
Kai (Tam Nhu) TranandGitHub 2115742caf fix(websearch): avoid shell for legacy CLI fallbacks (#1267)
* fix(websearch): avoid shell for legacy CLI fallbacks

* style: apply prettier formatting
2026-05-16 13:58:52 -04:00
Kai (Tam Nhu) TranandGitHub 78004746be fix: address upstream reviewer findings + failing CI checks (#1261 loop 1) (#1271)
* 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.
2026-05-16 13:57:50 -04:00
Kai (Tam Nhu) TranandGitHub 14cbe8fb67 fix(security): reject 127-prefixed websocket origins (#1263)
* fix(security): reject 127-prefixed websocket origins

* style: apply prettier formatting
2026-05-16 13:56:10 -04:00
Kai (Tam Nhu) TranandGitHub 609bed0c39 fix(codex): normalize native cliproxy tuning aliases (#1254) 2026-05-15 09:51:42 -04:00
Tam Nhu Tran 5b664d2105 hotfix(codex): tolerate BOM in Codex TOML config 2026-05-14 13:13:55 -04:00
Kai (Tam Nhu) TranandGitHub 4748c452bd fix(codex): self-heal ccsxp cliproxy provider (#1243) 2026-05-14 12:35:31 -04:00
walker1211andGitHub 35b6210cd2 fix: support Claude subcommands and background sessions
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
2026-05-14 10:18:23 -04:00
Kai (Tam Nhu) TranandGitHub 667b1d1e1f fix: ignore internal instance directories in health scans
Closes #1236
2026-05-12 22:17:16 -04:00
Kai (Tam Nhu) TranandGitHub 349db830df fix(settings): confine dashboard settings paths (#1231) 2026-05-12 18:13:26 -04:00
Kai (Tam Nhu) TranandGitHub a840793a9b fix(proxy): avoid exposing local auth token in daemon argv (#1230) 2026-05-12 18:13:20 -04:00
Kai (Tam Nhu) TranandGitHub 9c5d4976c5 fix(windows): pin cmd shell for wrapper launches (#1229) 2026-05-12 18:13:13 -04:00
Tam Nhu Tran c2b00b7ad6 feat(dispatcher): pass through Claude subcommands without interactive-session args
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.
2026-05-12 16:35:37 -04:00
Tam Nhu Tran 4c0aa83c82 chore(sync): merge main into dev after release
# Conflicts:
#	.github/workflows/ci.yml
#	package.json
2026-05-12 12:43:02 -04:00
Kai (Tam Nhu) TranandGitHub 2273757c83 fix(ci): harden self-hosted workflow trust boundaries
Keep active CCS workflows on self-hosted runners, gate self-hosted PR execution to trusted authors, and scope privileged release credentials to the exact git operations that need them.
2026-05-12 12:34:15 -04:00
Kai (Tam Nhu) TranandGitHub d61469edcb fix(config): bind dashboard to loopback by default 2026-05-12 10:59:49 -04:00
Kai (Tam Nhu) TranandGitHub f09cdfcf2b fix(security): protect dashboard WebSocket upgrades 2026-05-12 10:42:05 -04:00
Kai (Tam Nhu) TranandGitHub e7174d63bb fix(ci): run pull request validation on hosted runners 2026-05-12 10:33:47 -04:00
Kai (Tam Nhu) TranandGitHub bd588a271d fix(codex): require local access for dashboard config routes 2026-05-12 10:24:25 -04:00
Kai (Tam Nhu) TranandGitHub eb4bbfddf3 security(browser-mcp): restrict model-callable local file uploads/downloads to safe roots and deny sensitive paths (#1220)
* fix(browser): restrict file transfer paths

* fix(browser): normalize safe transfer paths

* fix(browser): reject sensitive transfer roots
2026-05-12 08:57:33 -04:00
Kai (Tam Nhu) TranandGitHub 29da75c0d4 fix(cliproxy): explain headless Codex OAuth recovery
Closes #1213
2026-05-11 14:25:34 -04:00
Tam Nhu Tran a983332016 refactor(web-server): modularize shared-routes and Windows-aware symlink status 2026-05-10 22:15:33 -04:00
Kai (Tam Nhu) TranandGitHub 248b6b735d Merge pull request #1200 from kaitranntt/kai/feat/1199-shared-resource-controls
feat(auth): add shared resource controls
2026-05-10 22:01:57 -04:00
Tam Nhu Tran 9d0690e1a8 test(ci): make routing mock complete and websearch trace path env-robust
Two pre-existing test bugs surfaced under CCS CI on the new claw self-
hosted runners but stayed hidden locally:

1. tests/unit/commands/cliproxy-routing-subcommand.test.ts mocks the
   routing-subcommand module with only 3 exports, but
   src/commands/cliproxy/index.ts statically imports 6. Bun resolves
   the static import graph against the mock and reports
     SyntaxError: Export named 'handleRoutingAffinitySet' not found
   for every test in the file. Add the missing 3 exports
   (handleRoutingAffinityStatus, handleRoutingAffinityHelp,
   handleRoutingAffinitySet) to the mock and document why the mock
   must mirror every named export of the target module.

2. tests/unit/hooks/websearch-transformer.test.ts builds the
   "disallowed" trace path from process.cwd() and from os.homedir().
   Both fall under the os.tmpdir() safe-prefix in two real
   environments: CI runners with cwd == /tmp/runner/work/... and Bun
   test isolation that re-roots HOME under tmpdir. The hook treats
   them as safe, writes the trace, and the assertion that the file
   does NOT exist fails. Anchor disallowedTracePath under /etc/...
   instead so it cannot satisfy the tmpdir, /var/log, or
   <CCS_HOME>/.ccs/logs prefixes in any host environment.

Both fixes are independent of the OAuth callback traceability change
that this branch otherwise carries, but ship together so the PR
clears CI on the new runner stack.
2026-05-10 20:23:02 -04:00
Tam Nhu Tran 43ece40c21 Revert "ci(ai-review): move PR-Agent jobs to ubuntu-latest"
This reverts the runs-on change for ai-review.yml. All CI/CD must
stay on home infra (claw, docker LXC, etc); GitHub-hosted runners
are forbidden. The qodo-ai/pr-agent docker action mount issue will
be addressed by either configuring the self-hosted runner image to
use path-identical bind mounts so nested-docker volume sources
resolve correctly, or by switching to native CLI invocation
(pip install pr-agent) instead of the docker action.

Tracked separately; not blocking PR #1209.
2026-05-10 18:30:05 -04:00
Tam Nhu Tran c02c66dcd1 ci(ai-review): move PR-Agent jobs to ubuntu-latest
The qodo-ai/pr-agent Docker action requires the GitHub event payload
at /github/workflow/event.json inside the action container. On
self-hosted runners that themselves run inside a Docker container
(e.g. myoung34/github-runner), the host docker daemon resolves the
volume mount /tmp/runner/work/_temp/_github_workflow:/github/workflow
against the host filesystem, where the runner's /tmp path does not
exist. The action container starts with an empty /github/workflow
mount and fails with FileNotFoundError on event.json.

AI review jobs do not need self-hosted runner access (no bun cache,
no internal infra). Switch both dispatch-review and pr-agent jobs
to ubuntu-latest so the volume mount resolves on the same host where
the action expects it.
2026-05-10 18:26:17 -04:00