fix(export): block synthetic data from reaching production GCS bucket - #580
Merged
Conversation
Measured 2026-08-21: `test/multi-agent-proxy.e2e.test.js` inherited the developer's globally-exported `CCXRAY_EXPORT_GCS_BUCKET` and uploaded one synthetic daily row (3 turns, fake providers) to the company bucket (GCS objects 136 → 137). `CCXRAY_HOME` isolates storage paths but NOT the bucket. `isExportSuppressed()` in `flushExport` now blocks uploads on two layers: 1. `CCXRAY_EXPORT_DISABLE=1` — explicit flag, set by every synthetic launcher (boot-smoke.sh, measure.js, test/*.sh, CLAUDE.md smoke, generate-screenshot-fixtures.js, shoot-spiral.mjs). An enumeration test catches a missing flag so new launchers fail the suite instead of leaking silently. 2. `NODE_TEST_CONTEXT` — automatic net for `node --test` (what `npm test` runs). NOT liftable by any env flag, because the measured leak happened inside this context. A non-null `_uploader` (test seam) bypasses it so aggregation tests keep working. `startExportSync()` now prints a positive startup signal naming the bucket, and stays silent when suppressed — so it cannot announce an exporter that `flushExport` would refuse to run. Verification (docs/verification-principles.md): - old code + new tests: 0/5 pass (all red) - new code + new tests: 5/5 pass - old code + old tests: 28/28 pass (clean baseline) - full suite: 2231/2231 pass Codex review: 5 rounds, 0 rejected findings. Round 4 blocker (a third layer that inferred "synthetic" from redirected CCXRAY_HOME) was built and then deleted after round 5 found it would disable production exports (ccxray-ops/deploy-stable.sh sets CCXRAY_HOME="$HOME/.ccxray"). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaJ4EeuCA6BmvvNdkZteuo
…ness, grok smoke
M1: Remove redundant test-end cleanup() calls — afterEach already does it.
The double-call deleted a restored ambient CCXRAY_EXPORT_DISABLE under
--test-isolation=none.
M3: setsFlag() for .md files now requires the match inside a code fence,
so CLAUDE.md's prose description of the flag doesn't satisfy the check.
M4: docs/grok-testing.md live smoke command now carries CCXRAY_EXPORT_DISABLE=1.
Added to the enumeration test's launcher list.
M2: Rejected — the .test.js gap is documented as an accepted known limitation
in the PR body.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TaJ4EeuCA6BmvvNdkZteuo
…eanup, partial_day
M1b: Save ambient CCXRAY_EXPORT_DISABLE at module load so the first test's
afterEach(cleanup) can restore it. The partial_day test (which bypasses
setup()) also clears it explicitly.
Minor: Removed vestigial CCXRAY_EXPORT_FORCE references from the guard test
— FORCE was deleted from production in an earlier round.
B1: This commit is signed (git commit -s).
Rejected:
M4b: grok-testing.md has two code-fenced commands, both correctly carry the
flag. The per-file check intentionally passes if ANY fenced command sets
it — per-command-line granularity is diminishing returns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TaJ4EeuCA6BmvvNdkZteuo
Signed-off-by: Justin Lee <justinlee@91app.com>
…Y_EXPORT_DISABLE The test bypasses setup() and manages its own env, but afterEach(cleanup) still runs. Without saving _savedFlags.disable first, cleanup deletes the ambient value and later files under --test-isolation=none lose the safety flag. Rejected (not in PR scope): - ADR 0017 duplicate heading - internal-headers.js wire-protocol-reference update - usage.test.js temp dir cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaJ4EeuCA6BmvvNdkZteuo Signed-off-by: Justin Lee <justinlee@91app.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaJ4EeuCA6BmvvNdkZteuo Signed-off-by: Justin Lee <justinlee@91app.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
摘要
全域 export 的
CCXRAY_EXPORT_GCS_BUCKET會被npm test的 e2e 繼承,實測上傳了一筆合成 daily row 到公司 GCS(136→137)。CCXRAY_HOME只隔離路徑不隔離 bucket。加了兩層抑制 + 正面啟動訊號 + enumeration test 確保新 launcher 不會靜默洩漏。
Detail
The measured leak (2026-08-21)
test/multi-agent-proxy.e2e.test.jsboots a full server, inheritsCCXRAY_EXPORT_GCS_BUCKETfrom the developer's~/.zshrc, and uploaded one synthetic daily summary (agent_ida91e9944, 3 turns, fake providersclaude-sonnet-4/gpt-5.5/grok-4.5) to the company bucket. GCS objects went 136 → 137. The synthetic row was deleted after confirmation.Two-layer suppression in
isExportSuppressed()npm test/node --test;!_uploaderlets the aggregation suite's injected seam still runtest/export-sync-test-guard.test.js) asserts each known launcher carries the flag — removing it from any one of them turns the suite red (verified per-launcher)Positive startup signal
startExportSync()now prints[ccxray export] exporter active — bucket <name>, flush every 60minon successful activation. Before this, exporter startup was completely silent — only failures printed. The message shares the same suppression predicate so it cannot announce an exporter thatflushExportwould refuse to run.uploaderAtEntrysnapshot_uploaderis snapshotted once atflushExportentry. Without this, a test's_setUploader(null)cleanup landing during async index traversal could switch an already-permitted flush to the real GCS uploader.Codex review
5 rounds, 0 rejected findings. Key moments:
CCXRAY_EXPORT_FORCE(an escape hatch) was checked before the test-runner net, so an inherited ambient FORCE disabled both automatic safeguards → FORCE removed entirelyCCXRAY_HOMEbeing set, butccxray-ops/deploy-stable.shlaunches the real hub withCCXRAY_HOME="$HOME/.ccxray"→ third layer deletedFiles changed
server/export-sync.jsisExportSuppressed(),uploaderAtEntry, startup messagetest/export-sync-test-guard.test.jstest/export-sync.test.jsCCXRAY_EXPORT_DISABLEin setup/cleanupCLAUDE.mdCCXRAY_EXPORT_DISABLE=1scripts/boot-smoke.shCCXRAY_EXPORT_DISABLE=1scripts/perf/measure.jsCCXRAY_EXPORT_DISABLE: '1'in serverEnvscripts/generate-screenshot-fixtures.jstest/rebuild-index.browser-harness.e2e.shexport CCXRAY_EXPORT_DISABLE=1docs/grok-testing.mddocs/site/slides/coscup2026/README.mddocs/site/slides/coscup2026/tools/shoot-spiral.mjsCCXRAY_EXPORT_DISABLE: '1'in spawn envCompanion change (ccxray-ops, not in this PR)
bq/flush-once.jsneeds updating to useisExportSuppressed— it currently uses atypeofcompatibility check that fails closed (exit 1) on an old checkout. This is the safe direction: the tool refuses rather than uploading. Will commit to ccxray-ops after this merges.Known gap (accepted)
node test/foo.test.js(without--test) leavesNODE_TEST_CONTEXTunset, and the 22.test.jsfiles that spawn a listening server do NOT set layer 1 individually. Tagging all 22 was rejected: high maintenance and a new test would silently miss it. Normal paths (npm test, CI,node --test <file>) are covered by layer 2.Verification
Per
docs/verification-principles.md(fail-on-old in a throwaway worktree):Test plan
node --test test/export-sync-test-guard.test.js— 5/5node --test test/export-sync.test.js— 28/28CCXRAY_EXPORT_DISABLE=1— guard suite 5/5, aggregation 28/28CCXRAY_EXPORT_FORCE=1— guard suite 5/5 (FORCE was removed; env is inert)🤖 Generated with Claude Code
https://claude.ai/code/session_01TaJ4EeuCA6BmvvNdkZteuo