Commit Graph
994 Commits
Author SHA1 Message Date
Kai (Tam Nhu) TranandGitHub 0bd2e577b2 fix(codex-auth): restrict symlink fallback and harden copy path (#1365) 2026-05-23 21:43:51 -04:00
Kai (Tam Nhu) TranandGitHub ab379e9e13 fix: disable docker legacy API key auth by default (#1355) 2026-05-23 21:03:24 -04:00
Kai (Tam Nhu) TranandGitHub 8335f0c73c fix(dispatcher): also treat -p as non-subcommand short for --print (#1352)
Red-team follow-up to #1341: the original fix only guarded --print but
Claude accepts -p as the short form, and CCS itself passes -p in
headless-executor.ts. Without this check the security bypass survived
via the short flag.

Added regression tests: ['-p', 'agents'], ['-p', 'doctor'], ['-p']
alone, and injector short-circuit verification for -p form.
2026-05-23 17:33:38 -04:00
Kai (Tam Nhu) TranandGitHub 9a420988bc fix(analytics): support sqlite3 path resolution on Windows and NixOS (#1354)
- Add CCS_SQLITE_BIN env-var override; validated with fs.realpathSync so
  symlinks are fully resolved before prefix check
- Reject any override whose realpath does not start under a trusted system
  prefix (prevents PATH-hijack reintroduction from #1347)
- Add TRUSTED_PREFIX_UNIX covering /nix/store/, /opt/local/ (MacPorts),
  /snap/, /run/current-system/ in addition to existing /usr/* and
  /opt/homebrew/ entries
- Add TRUSTED_PREFIX_WINDOWS covering Program Files, System32, and the
  Chocolatey managed bin dir (no canonical winget/Scoop path exists)
- Keep TRUSTED_SQLITE_PATHS_WINDOWS empty — Windows users set CCS_SQLITE_BIN
- Pass env as optional third param to querySqliteJson (backward compatible)
- Add 16-test suite covering env-var acceptance, /tmp rejection, symlink
  traversal, NixOS paths, MacPorts, Windows fallback, and prefix safety
2026-05-23 17:29:52 -04:00
Kai (Tam Nhu) TranandGitHub 5d27f10367 refactor(cliproxy): remove orphaned bg keepalive wiring (#1353)
Red-team review of #1340 found that backgroundProbe / shouldKeepSessionProxiesAlive /
startKeepAliveWatcher and related types were never reachable: the sole caller
(claude-launcher.ts) passed no hasBackgroundWorkerUsingBaseUrl detector, so
backgroundProbe was always undefined and the keepalive branch never executed.

Remove the unreachable scaffold:
- Delete CLAUDE_BG_WORKER_ARGS, hasClaudeBackgroundWorkerUsingBaseUrl{,InProcessList},
  shouldKeepSessionProxiesAlive, ShouldKeepSessionProxiesAliveOptions, and the
  SessionProxyKeepAliveOptions interface from session-bridge.ts
- Remove the backgroundProbe/startKeepAliveWatcher block and keepalive exit branch
  from setupCleanupHandlers; the function now always calls stopSessionResources on
  Claude exit
- Remove backgroundKeepAliveBaseUrl wiring from claude-launcher.ts
- Remove the execFileSync import (was only used by the deleted ps-based detector)
- Update test file: remove tests for deleted exports, keep placeholder

A trusted bg-worker detector can be re-added in a future PR if the feature is
actually needed; the keepalive logic in shouldKeepSessionProxiesAlive can be
restored then.
2026-05-23 17:29:18 -04:00
Kai (Tam Nhu) TranandGitHub 9cd9b796ca fix(models-dev): sanitize models.dev registry entries and harden pricing resolver (#1345)
* fix(models-dev): sanitize cached model entries before pricing lookup

* style: apply prettier formatting
2026-05-23 17:03:49 -04:00
Kai (Tam Nhu) TranandGitHub 66966f3527 fix(pricing): guard malformed models.dev model entries (#1344)
* fix(pricing): guard malformed models.dev model entries

* style: apply prettier formatting
2026-05-23 16:55:05 -04:00
Kai (Tam Nhu) TranandGitHub fd561b59bc fix(dispatcher): treat --print as non-subcommand Claude session (#1341)
* fix(dispatcher): treat --print as non-subcommand Claude session

* style: apply prettier formatting

* fix(dispatcher): correct return type in subcommand detector

getClaudeSubcommandName returns string | null; return false (boolean)
was invalid after dev refactored isClaudeSubcommandInvocation to
delegate to it. Change to return null to preserve the --print safety
fix intent.
2026-05-23 16:54:30 -04:00
Tam Nhu Tran bbcf9e8b15 fix: mask Docker key rotation banner 2026-05-22 17:04:19 -04:00
Tam Nhu Tran 30971ebb28 fix: preserve Docker legacy API key during rotation 2026-05-22 16:49:16 -04:00
Tam Nhu Tran 37a14525cc fix: repair ccsx codex profile resources 2026-05-22 16:27:00 -04:00
Tam Nhu Tran f667628411 fix: resolve ccsx auth profiles before CCS profiles 2026-05-22 15:54:55 -04:00
Tam Nhu Tran db4b938c0e fix: treat remote-control as a Claude subcommand 2026-05-22 15:39:15 -04:00
Kai (Tam Nhu) TranandGitHub f8df18a99b Merge pull request #1327 from kaitranntt/kai/feat/1266-dashboard-sunset-guard
feat(ci): sunset legacy dashboard image publishing
2026-05-22 14:38:25 -04:00
Tam Nhu Tran 9f6e64fd39 feat(ci): sunset legacy dashboard image publishing 2026-05-22 14:16:42 -04:00
Tam Nhu Tran 4d7f30a260 chore(docker): skip ccs postinstall in legacy image 2026-05-22 14:10:29 -04:00
Kai (Tam Nhu) TranandGitHub 07cef20c76 Merge pull request #1324 from kaitranntt/kai/fix/dashboard-cliproxy-restart-button
fix: ensure dashboard cliproxy restart waits for recovery
2026-05-22 13:51:42 -04:00
Tam Nhu Tran fc5851e3a2 fix: ensure dashboard cliproxy restart waits for recovery 2026-05-22 13:37:21 -04:00
Tam Nhu Tran 6bc04dee97 fix: normalize ccsxp codex model flag aliases 2026-05-22 13:03:00 -04:00
Kai (Tam Nhu) TranandGitHub fa17ad5af1 Merge pull request #1318 from kaitranntt/kai/feat/1255-analytics-profile-filter
feat: filter analytics dashboard by profile
2026-05-22 12:38:50 -04:00
Tam Nhu Tran 46d73ae79f feat: share stale Codex translator path scanner 2026-05-22 12:10:54 -04:00
Tam Nhu Tran c81ea20e44 test: avoid fixed image analyzer e2e port 2026-05-22 11:44:43 -04:00
Kai (Tam Nhu) TranandGitHub 8e2a89ea56 Merge pull request #1316 from kaitranntt/kai/feat/1303-persist-receipt
feat: report permission mode persist receipt status
2026-05-22 11:34:24 -04:00
Kai (Tam Nhu) TranandGitHub 9aeb1c1e99 Merge pull request #1315 from kaitranntt/kai/fix/1275-codex-diagnostics-redaction
fix: redact Codex diagnostics metadata
2026-05-22 11:25:10 -04:00
Tam Nhu Tran f1d655e425 feat: filter analytics by profile 2026-05-22 11:24:21 -04:00
Tam Nhu Tran 3dc1810540 feat: report permission mode persist receipt status 2026-05-22 11:14:17 -04:00
Tam Nhu Tran e2f2c7dc2e fix: redact Codex diagnostics metadata 2026-05-22 11:12:20 -04:00
Tam Nhu Tran e0f43b88c2 test: cover agents permission-mode passthrough 2026-05-22 11:11:08 -04:00
SoungRyoul Kim ef4b746876 fix(dispatcher): preserve --permission-mode for claude agents subcommand
`stripClaudeSubcommandSessionArgs` was unconditionally stripping
`--permission-mode`, `--dangerously-skip-permissions`, and
`--allow-dangerously-skip-permissions` from every Claude subcommand
invocation. That's correct for `auth`/`doctor`/`mcp`/etc. — their
parsers reject those flags with `error: unknown option '...'`.

But `claude agents` does accept all three (see `claude agents --help`):
they configure the default permission mode for sessions dispatched from
the agent view. As a result, `ccs <profile> agents --permission-mode
bypassPermissions` silently dropped the flag before forwarding to Claude
and the agent view kept its previous default.

Add a small per-subcommand allowlist (`SUBCOMMAND_ALLOWED_SESSION_FLAGS`)
and a `getClaudeSubcommandName(args)` helper, then branch the strip
logic on the detected subcommand. `agents` keeps the three permission
flags; every other subcommand (including unknown future ones) falls
through to the existing strip behavior. `--teammate-mode` is still
stripped for every subcommand — no upstream subcommand accepts it.

Tests cover both before- and after-position arguments, the
`--flag=value` form, the `--teammate-mode` exclusion for `agents`, and
regression coverage for non-`agents` subcommands.
2026-05-21 20:53:57 +09:00
Tam Nhu Tran 6569eed15b feat: support minimal codex effort aliases 2026-05-20 11:11:46 -04:00
Kai (Tam Nhu) TranandGitHub 67fe6d9c7f fix(persist): print recovery receipt after settings write (#1302) 2026-05-19 15:23:37 -04:00
Kai (Tam Nhu) TranandGitHub 8051f1614b fix(browser): stop recorder leaking sensitive input
Squash merge PR #1293 into dev.
2026-05-19 09:04:05 -04:00
Kai (Tam Nhu) TranandGitHub 9a2a1dde31 fix(browser): redact observed event URLs
Squash merge PR #1296 into dev.
2026-05-19 08:54:57 -04:00
Kai (Tam Nhu) TranandGitHub 538b6444ff fix(browser): gate response fulfillment opt-in
Squash merge PR #1295 into dev.
2026-05-19 08:41:00 -04:00
Kai (Tam Nhu) TranandGitHub a86486497b fix(browser): block sensitive header intercept matchers
Squash merge PR #1294 into dev.
2026-05-19 08:31:17 -04:00
Kai (Tam Nhu) TranandGitHub 24478135dc fix(proxy): scope insecure TLS to routed profile
Squash merge PR #1299 into dev.
2026-05-19 08:13:40 -04:00
Kai (Tam Nhu) TranandGitHub fa31bea672 fix(proxy): require owned daemon PID before reusing legacy/profile proxy session
Squash merge PR #1298 into dev.
2026-05-19 08:05:45 -04:00
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