Conversation
A run cut short by SIGINT/SIGTERM or -timeout previously computed Passed from only the suites that happened to execute, so a truncated run could exit 0, append a passing entry to trend history, and be signed by the release-gate attestation with its security suites never having run. - Report records Interrupted and SkippedSuites; Passed is forced false when the run did not complete. - runBoundedByIndex returns the executed prefix so engines truncate pre-sized case slices instead of reporting phantom zero-value failures. - cleanr run exits 130 for interrupted runs, skips trend-history persistence, and refuses to build an attestation. - Text report shows INTERRUPTED status and the skipped suite names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wazero ignores context cancellation during guest execution unless the runtime is built with WithCloseOnContextDone, so a plugin with an infinite loop blew straight through the 5s entry timeout and hung the whole run — defeating the sandbox. The entry deadline now also applies to guest execution only: module compilation is host-side work whose cost scales with module size, not plugin behavior, and under the new instrumentation it could eat the entire execution budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bypass The generic sink POST and http/langsmith/openllmetry/provider_logs trend fetches built http.Clients directly from timeout_ms, so an unset value meant no timeout at all and one hung endpoint stalled a release-gate run until SIGINT. All integration HTTP calls now share newIntegrationHTTPClient with the same 10s default httpjson already used. PostHog and Langfuse send credentials outside applyAuth (request body / Basic auth), bypassing the CredentialEgressAllowed policy: a config with project_token_env: OPENAI_API_KEY and an attacker base_url could exfiltrate a provider secret. Both sinks now enforce the same egress policy and fail loudly when it is violated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cleanr_generate_dataset passed a typed-nil *http.Client into the real generation path, which nil-derefs on the first client.Do — and with no recover() anywhere in the stdio server, the panic killed the whole process and every session with it. The tool now gets a real client matching the CLI generate command's provider timeout, and tool dispatch is wrapped in recover() so a panicking handler returns a JSON-RPC error while the server keeps serving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… replay artifacts Extract the two copy-pasted writeFileAtomic helpers (snapshots, trends) into cleanr/fsatomic, add fsync before the rename plus a best-effort directory sync, and adopt it in the writers that were plain os.WriteFile: an interrupted write could previously destroy the user's hand-edited cleanr.yaml, corrupt profile.json and lose every stored provider credential, or leave a torn attestation whose signature can never verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zero-ambiguous fields were silently rewritten by applyDefaults: require_review: false was inverted to true, max_normalized_drift: 0 (zero tolerance) became 0.3, min_score: 0 became 0.6, and the trend-gate presets overrode an explicit enabled: value in both directions. Following the existing MaxFailedCasesDelta precedent, these fields are now pointer-typed with *Value() accessors that supply the defaults at read time: DriftConfig's six thresholds, LLMJudgeConfig.MinScore, ScenarioGenerationConfig.RequireReview, and TrendGateConfig.Enabled. Presets fill Enabled only when unset, so exploratory + enabled: true keeps relaxed thresholds with gating active (docs updated). Because the accessors default at read time, SDK-built configs that bypass applyDefaults now get correct thresholds too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The race detector was never run anywhere, hiding a real data race: tests/cli/snapshot_test.go swapped the process-global http.DefaultTransport while marked t.Parallel(), racing every parallel test that makes an HTTP call. The swap test is now sequential (parallel tests never overlap sequential ones, which is what makes the pattern safe), and the CLI-adapter helper-process timeout is raised to 10s since race instrumentation makes the re-exec'd binary slow to start. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The initialize handshake ran under a sync.Once that stored the first error forever: if the first scenario's deadline expired mid-handshake or the server was briefly restarting, every remaining scenario in the run failed instantly with the stale error. A failed attempt is no longer cached — the next Invoke retries, with a mutex preserving Once's serialization of concurrent first invokes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two fixes to the shared HTTP retry loop (used by the HTTP, OpenAI, and Anthropic adapters): - When a 429/503's Retry-After exceeded the remaining context budget, the response body was closed before being returned, so callers got 'read on closed response body' instead of the actual rate-limit response. The body is now closed only after committing to another attempt — and drained first so the keep-alive connection is reused instead of forcing a new TCP+TLS handshake against an already-degraded server. - Transport errors were retried for every method, but a connection can die after a request was fully delivered — replaying a POST there risks duplicate writes or double charges. Transport-error retries are now limited to idempotent methods (GET/HEAD/OPTIONS); 429/503 responses still retry for all methods since the server explicitly rejected the request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tool results Three protocol-compliance fixes for the stdio MCP server: - Request ids are kept as raw JSON so id: 0 and id: "" round-trip exactly (omitempty previously dropped them, breaking client correlation), and parse errors now carry the spec-required id: null. - The final request of a session without a trailing newline (common for one-shot piped clients) is processed at EOF instead of silently dropped. - Ordinary tool execution failures return isError results the calling model can read and self-correct from; unknown tools are -32602 and contained panics remain -32603, instead of every failure surfacing as a blanket internal error that some clients treat as a broken server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merged
2 tasks
Follow-up hardening: MCP init retry, safe HTTP retry semantics, MCP server protocol compliance
The catch-all tests/cli/cli_test.go had grown to 2914 lines, tripping codeguard's max-file-lines gate once this branch touched it. Split by topic — integrations, native sinks, dataset commands, dataset policy, sync, trend history, trends/plugins commands — with shared helpers kept in cli_test.go. Test code is unchanged apart from imports. Also restructure TestExplicitRequireReviewFalseSurvivesLoad's assertions into explicit got/want checks to satisfy codeguard's conditional-assertion heuristic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.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.
Summary
A reliability audit of the codebase (runner/engines, network adapters, integrations, persistence, CLI, and MCP server) surfaced seven high-severity defects. This PR fixes the top six items from the audit, each verified with new regression tests. The full suite now passes
go test -race -shuffle=on ./...(423 tests) viacleanr-dev check.Fixes
1. Interrupted runs reported
Passed: true(fix(runner))A run cut short by SIGINT/SIGTERM or
-timeoutcomputedPassedfrom only the suites that happened to execute — a CI job killed early could exit 0, append a passing entry to trend history, and get a signed release-gate attestation with its security suites never having run.Reportnow recordsInterruptedandSkippedSuites;Passedis forced false on truncation.runBoundedByIndexreturns the executed prefix).cleanr runexits 130 for interrupted runs (documented indocs/ci.md), skips trend-history persistence, and refuses to sign an attestation. The text report showsStatus INTERRUPTEDwith the skipped suites.2. Data race + no race detector anywhere (
test)tests/cli/snapshot_test.goswapped the globalhttp.DefaultTransportundert.Parallel(), racing every parallel test doing HTTP — invisible because neither CI,make test, norcleanr-dev testever ran-race. The swap test is now sequential, the CLI-adapter helper timeout is raised for race-instrumented startup, and both CI andcleanr-dev testrun-race -shuffle=on.3. WASM plugins could hang the run forever (
fix(plugins))The wazero runtime lacked
WithCloseOnContextDone(true), so guest code ignored context deadlines — a plugin with an infinite loop wedgedRunner.Runpermanently, defeating the sandbox. The entry timeout now interrupts guest execution and is scoped to execution only (module compilation is host-side work). A regression test proves afor {}guest is killed.4. MCP server crash + missing HTTP timeouts (
fix(mcpserver),fix(integrations))cleanr_generate_datasetpassed a typed-nil*http.Clientinto the real generation path → nil-deref panic that killed the whole stdio server (norecover()anywhere). It now gets a real client, and tool dispatch is wrapped inrecover()so a panicking handler returns a JSON-RPC error while the server keeps answering.Timeout: 0whentimeout_mswas unset — one hung endpoint stalled a release-gate run indefinitely. All integration HTTP calls now share one constructor with a 10s default.5. Credential-egress bypass in PostHog/Langfuse sinks (
fix(integrations))Both sinks send credentials outside
applyAuth(request body / Basic auth), skipping theCredentialEgressAllowedpolicy — a config withproject_token_env: OPENAI_API_KEYand an attackerbase_urlcould exfiltrate a provider secret. Both now enforce the same egress policy and fail loudly.6. Persistence and config-defaults hardening (
fix(persistence),fix(config))cleanr/fsatomicpackage (temp file + fsync + rename + dir sync) replaces two copy-pasted helpers and is adopted by the previously non-atomic writers:cleanr.yaml, the credentials-bearingprofile.json, attestations, and replay artifacts. An interrupted write can no longer destroy or corrupt any of them.*Value()accessors (following theMaxFailedCasesDeltaprecedent):require_review: falseis expressible (was silently inverted to true),max_normalized_drift: 0means zero tolerance (was replaced by 0.3),min_score: 0survives, and trend-gate presets no longer override an explicitenabled:—exploratory+enabled: truekeeps relaxed thresholds with gating active. Since accessors default at read time, SDK-built configs that bypassapplyDefaultsget correct thresholds too.Behavior changes to be aware of
cleanr runwhen interrupted (previously 0 or 1 depending on partial results). Pipelines treating non-zero as failure are unaffected; pipelines matching exit codes exactly should add 130.interruptedandskipped_suitesfields.TrendGateConfig.Enabled,ScenarioGenerationConfig.RequireReview,LLMJudgeConfig.MinScore, and sixDriftConfigthresholds are now pointer-typed in the Go SDK (read via*Value()accessors). YAML/JSON configs are unaffected.Test plan
cleanr-dev check(gofiles layout, fmt, vet,go test -race -shuffle=on ./...) — 423 passed, 0 failedgo vet ./...clean🤖 Generated with Claude Code