Skip to content

Fix all critical & high deep-audit findings - #167

Merged
gnanam1990 merged 17 commits into
mainfrom
fix/audit-critical-high
Jun 11, 2026
Merged

Fix all critical & high deep-audit findings#167
gnanam1990 merged 17 commits into
mainfrom
fix/audit-critical-high

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Resolves every critical and high finding from the deep audit. Each commit is scoped to a single finding and ships with a regression test; the full suite, go vet, and gofmt are clean.

  • MCP framing — cap framed messages at 64 MiB and switch the stdio transport to newline-delimited JSON (Content-Length kept for back-compat). Closes an unbounded make([]byte, contentLength) allocation.
  • Provider connectivity probe — SSRF-hardened: re-validate every redirect hop (capped at 5) and validate + pin the resolved IP at dial time, closing the DNS-rebinding TOCTOU.
  • bash tool — kill the shell as a process group on timeout (Setpgid + group SIGKILL + WaitDelay) so backgrounded children can't outlive the command or hang Run().
  • sandbox-exec profile — allow the standard device nodes and temp trees for parity with the bubblewrap backend; workspace stays the only writable project location.
  • Reasoning effort — thread CompletionRequest.ReasoningEffort through the agent loop and map it per provider: OpenAI reasoning_effort, Anthropic extended thinking (with thinking-block replay), Gemini thinking budget.
  • sessions — tolerate a torn tail only when the final line is unterminated; a fully-flushed corrupt line still fails loudly.
  • streamjson — anchor secret patterns so prose (task-list, bearer of bad news) is no longer over-redacted.
  • tools — fix grep glob scope (ripgrep semantics) and apply_patch hunk-body misparse.
  • cli/cron — honor an explicit expr over a recipe; preserve an external pause mid-run.
  • specialist — keep the name in the session title so description-less runs resume.
  • agent — surface response truncation (finish_reason) to the user.

Partial (tracked in docs/audit/2026-06-10-deep-audit-status.md)

  • Hooks — a Dispatcher that runs and audits configured hooks is added and tested, but it is not yet wired into the beforeTool/afterTool lifecycle and has no CLI dispatch/edit surface (behavioral change deferred to a design pass).
  • Cron — the pause-clobber during a run is fixed; the concurrent-scheduler race (no flock/CAS in internal/cron/store.go) remains, with single-scheduler as the supported model.

Test Plan

  • go build ./...
  • go test ./... (full suite green)
  • go vet ./...
  • gofmt -l clean

Summary by CodeRabbit

  • New Features

    • Extended “thinking”/reasoning support with configurable effort levels and preserved replayable reasoning across turns
    • Hook dispatch system for configurable event hooks
  • Bug Fixes

    • Fixed MCP message framing/parsing
    • Improved truncation detection/reporting (CLI/TUI and run outputs now show truncation notices)
    • Better recovery from partial session logs
    • More robust patch parsing to avoid misinterpreting hunk content
  • Improvements

    • Expanded sandbox write allowances for common temp/dev paths
    • Stricter secret-redaction patterns
    • Hardened process lifetime handling for executed commands
    • CLI cron argument parsing and run handling made more resilient
    • Safer provider connectivity checks and redirect handling

- protocol.go read() rejected no upper bound on Content-Length, so a hostile
  peer could drive make([]byte, n) with n up to ~9.2e18. Cap every framed
  message at 64 MiB (both the Content-Length and newline-delimited paths).
- The MCP stdio transport is newline-delimited JSON, not LSP Content-Length
  framing. write() now emits one JSON message per line; read() accepts
  newline-delimited frames and still tolerates Content-Length for back-compat,
  so Zero interoperates with spec-compliant MCP servers and clients.
- grep matched the glob against the workspace-relative path, so "*.go" with
  path="subdir" found nothing while "**/*.go" matched subdir/a.go. Match the
  glob relative to the search directory (ripgrep semantics); a single explicit
  file is matched by its base name.
- apply_patch parsed file paths per-line, so a hunk-body line removing content
  that starts with "-- " (appearing as "--- ...") was mistaken for a file
  header and a valid patch was rejected. Parse headers with hunk line-counting,
  matching how git apply delimits hunks.
The sk- and bearer patterns were unanchored, so "sk-" inside "task-list" and
the word after a bare "bearer" were replaced with [REDACTED], corrupting
legitimate output. Anchor sk- on a word boundary and require a credential-length
body for both sk- and bearer tokens; real keys/tokens are still redacted.
ReadEvents returned an error on any malformed JSONL line, so a single
interrupted append (truncated final line) made resume hard-fail and lose the
whole session. Drop only a malformed final line (a torn tail); malformed lines
earlier in the file are real corruption and still error.
- `cron add "<expr>" --recipe X` silently dropped the explicit schedule because
  the recipe set expr before the positional arg was consumed. Consume the
  positional expr first so it overrides the recipe default.
- cron_run persisted the job copy read at tick start, so a pause or removal that
  happened during the run was clobbered back to active. Re-read before persisting
  and preserve an external pause/removal (best-effort; full atomicity needs a
  lock).
A specialist run with no description set --session-title to just the
description (empty), leaving AgentName empty on resume and making the
session unresumable. Always prefix the title with the specialist name and
fall back to the bare name when no description is given.
…children

On timeout the context cancelled only the shell, leaving backgrounded
children (e.g. 'sleep 3 & wait') orphaned. Worse, those children inherited
the stdout/stderr pipes, so Run() blocked until they exited — far past the
timeout. Set Setpgid and a Cancel that SIGKILLs the whole process group on
POSIX, plus a WaitDelay backstop so a lingering child can't hang Wait. On
Windows, set WaitDelay only (no process groups).
The default probe used http.DefaultClient, which (1) followed redirects
without revalidating the target, so a 3xx to an internal address bypassed
the pre-flight endpoint check, and (2) re-resolved DNS at dial time,
leaving a rebinding TOCTOU window after validateEndpoint. Build a dedicated
client that revalidates every redirect hop (capped at 5) and validates +
pins the resolved IP at dial time, dialing the IP literal so the kernel
cannot re-resolve to an unsafe address.
The macOS sandbox-exec profile made only the workspace writable, so
ubiquitous operations failed with 'Operation not permitted' — `> /dev/null`
and mktemp among them. The bubblewrap backend already grants these via
--dev /dev and --tmpfs /tmp; bring the sandbox-exec profile to parity by
allowing the standard device nodes and the system temp trees. The workspace
remains the only writable project location.
Providers already normalized finish_reason into collected.FinishReason, but
the agent loop never read it, so a final answer cut off at the output token
cap (or by a content filter) was silently presented as complete. Carry the
turn's finish reason into agent.Result, add Truncated()/TruncationNotice(),
and emit a warning from the exec writer and a system row in the TUI when the
response was truncated.
…quest

zeroruntime.CompletionRequest had no effort field, so agent.Options.Reasoning
Effort never reached the provider (cli/exec.go even noted it was 'not yet
forwarded'). Add CompletionRequest.ReasoningEffort, thread it through every
request the agent loop builds, and map it to OpenAI's reasoning_effort. The
CLI gates the forwarded value against the resolved model: known non-reasoning
models send nothing (matching the existing 'ignoring' advisory), known
reasoning models send their effective level, and unknown custom endpoints
forward the requested value as-is. Anthropic/Gemini mapping is handled
separately.
Map a requested reasoning effort to an Anthropic thinking budget and enable
extended thinking, raising max_tokens so the budget plus a response both fit.
Crucially, capture the streamed thinking / redacted_thinking blocks (text +
signature) and preserve them on the assistant message so they are replayed
verbatim at the start of the next turn — without this, Anthropic rejects every
tool-using follow-up. Adds zeroruntime carriers (Message.Reasoning, ToolCall.
Signature, ReasoningBlock) shared with the upcoming Gemini work. No effort =
no thinking, so default runs are unchanged.
Map a requested reasoning effort to a Gemini thinkingBudget (capped at the
lowest per-model ceiling). Capture the thoughtSignature each functionCall part
carries and preserve it on the tool call (via a new StreamEvent/ToolCall
signature field), then replay it on the functionCall part in later turns so
multi-turn function calling with thinking is not rejected. Thought-summary
parts are skipped so reasoning never leaks into the answer. No effort = no
thinking config, so default runs are unchanged.
The torn-tail tolerance dropped any malformed final line, including a
complete-but-corrupt line ending in a newline — silently swallowing real
corruption. A genuine torn tail is an incomplete write with no trailing
newline; gate the tolerance on that so a fully-flushed corrupt line fails
loudly again, while a truncated final line is still recovered.
The package had all the primitives (Select, ConfigStore, AuditStore) but no
caller ever ran a hook. Add a Dispatcher that selects the hooks for a lifecycle
event, runs each command (no shell) with the event payload on stdin under a
timeout, and records started/completed audit events. A beforeTool hook that
exits non-zero vetoes the tool and stops the chain; other events are advisory.
A hook that cannot be launched is recorded as an error but never blocks. The
command runner is injectable for testing.
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 7dcbf4fd670d
Changed files (44): internal/agent/loop.go, internal/agent/loop_test.go, internal/agent/types.go, internal/cli/cron.go, internal/cli/cron_run.go, internal/cli/cron_test.go, internal/cli/exec.go, internal/cli/exec_test.go, internal/cron/store.go, internal/cron/store_test.go, internal/hooks/dispatch.go, internal/hooks/dispatch_test.go, and 32 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a26ef033-f67e-4cc5-826d-f97d280f3ea2

📥 Commits

Reviewing files that changed from the base of the PR and between f638ceb and 7dcbf4f.

📒 Files selected for processing (1)
  • internal/providerhealth/connectivity_client_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/providerhealth/connectivity_client_test.go

Walkthrough

This PR adds end-to-end reasoning/replay (ReasoningEffort, ReasoningBlocks, tool-call signatures, FinishReason) across runtime, providers, agent/CLI/TUI; introduces a hook dispatcher with blocking before-tool semantics and audits; hardens MCP framing and provider connectivity; and updates sandbox, bash process lifetime, apply_patch parsing, grep globing, cron/store behavior, session JSONL robustness, redaction regexes, and many tests.

Changes

Extended Thinking and Reasoning Support

Layer / File(s) Summary
Runtime model extensions
internal/zeroruntime/types.go, internal/zeroruntime/helpers.go
Adds ToolCall.Signature, new ReasoningBlock type, Message.Reasoning, StreamEvent fields, CompletionRequest.ReasoningEffort, and collection of reasoning blocks during streaming.
Agent loop integration
internal/agent/loop.go, internal/agent/loop_test.go, internal/agent/types.go
Propagates ReasoningEffort into all completion requests (initial and retries), records collected.FinishReason into Result.FinishReason, preserves ReasoningBlocks onto assistant messages, deep-copies message Reasoning, and adds Result.Truncated() / TruncationNotice() with tests.
Anthropic thinking
internal/providers/anthropic/*
Maps ReasoningEffort→thinking budget, buffers streamed thinking/redacted_thinking (including signatures), finalizes blocks at end-of-stream, emits ReasoningBlocks on done, and replays prior reasoning blocks in outgoing requests; tests added.
Gemini thinking and signatures
internal/providers/gemini/*
Maps ReasoningEffort→thinking budget, skips internal thought text from emitted answer, captures/propagates thought signatures with tool-call events, and replays signatures in outgoing requests; types and tests updated.
OpenAI reasoning effort
internal/providers/openai/*
Adds reasoning_effort request field and normalizer that whitelists minimal/low/medium/high, with tests validating inclusion/omission.
CLI reasoning forwarding and truncation warnings
internal/cli/exec.go, internal/cli/exec_test.go
Computes forwarded reasoning effort per model support, uses it for spec-draft and agent runs, and emits truncation notices after runs.
TUI truncation display
internal/tui/model.go
Appends system transcript row with truncation notice when applicable.

Hook Dispatch System

Layer / File(s) Summary
Hook dispatch and audit
internal/hooks/dispatch.go, internal/hooks/dispatch_test.go
New Dispatcher runs matching hooks sequentially with per-hook timeout, JSON stdin payloads, captures stdout/stderr/exit, records audit start/completed, and vetoes before-tool events on non-zero exit or timeout; many tests cover matching, blocking, non-blocking, timeout, and launch-failure semantics.

MCP Protocol and Provider Health Hardening

Layer / File(s) Summary
MCP newline-delimited JSON framing
internal/mcp/protocol.go, internal/mcp/protocol_test.go
Replaces writer to emit newline-delimited JSON, reader accepts newline-delimited JSON with fallback to Content-Length framing, and enforces per-message size cap; tests for newline, EOF-without-newline, content-length, oversized rejection, and round-trip added.
Provider health SSRF hardening
internal/providerhealth/providerhealth.go, internal/providerhealth/connectivity_client_test.go
connectivityCheck uses internally-constructed client with safeDialContext, re-resolves hosts at dial, rejects blocked IPs (private/link-local/metadata), validates redirects per-target, and caps redirect chains; tests validate dialing fallback, last-error behavior, and blocked redirect handling.

Tool and Process Hardening

Layer / File(s) Summary
Bash process lifetime
internal/tools/bash.go, internal/tools/bash_proc_unix.go, internal/tools/bash_proc_windows.go, internal/tools/bash_tool_test.go
Adds hardenProcessLifetime to configure process-group Setpgid, bounded Wait delays, and group-kill on cancel; platform-specific implementations and a regression test ensure background children are killed on timeout.
Hunk-aware patch parsing
internal/tools/apply_patch.go, internal/tools/write_tools_test.go
Replaces per-line path extraction with patchHeaderPaths unified-diff parser that tracks hunk body state via @@ counts to avoid misparsing header-like hunk body lines and prevents hunk-count-inflation path-escape attacks; tests added.
Sandbox and file-tool updates
internal/sandbox/runner.go, internal/sandbox/runner_test.go, internal/tools/grep.go, internal/tools/file_tools_test.go
Sandbox-exec profile now allows writable devices/subpaths (e.g., /dev/null, /private/tmp); tests include macOS sandbox runtime check. Grep glob matching now uses basename for single-file targets and search-dir-relative paths for directory walks; tests added.
Cron, store, sessions, specialist, redaction
internal/cli/cron.go, internal/cli/cron_run.go, internal/cli/cron_test.go, internal/cron/store.go, internal/cron/store_test.go, internal/sessions/store.go, internal/sessions/store_test.go, internal/specialist/exec.go, internal/specialist/exec_test.go, internal/streamjson/streamjson.go, internal/streamjson/streamjson_test.go
Cron add positional parsing adjusted; cron run rereads persisted job and handles mid-run removal/paused state; Store.Get introduces ErrJobNotFound sentinel; sessions tolerate a torn final JSONL line; specialist exec always sets session title; secret redaction regexes tightened to avoid prose over-match; tests added/updated.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Gitlawb/zero#48: Related apply_patch unified-diff header/path parsing work.
  • Gitlawb/zero#105: Related finish-reason/truncation plumbing between zeroruntime and agent Result.
  • Gitlawb/zero#128: Related --reasoning-effort wiring and model gating/forwarding.

Suggested reviewers

  • anandh8x
  • Vasanthdev2004
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audit-critical-high

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (3)
internal/mcp/protocol_test.go (1)

27-36: ⚡ Quick win

Add a regression test for EOF-terminated final JSON line.

messageReader.read has special logic for a final message without trailing \n, but that path is not explicitly tested. Add one focused test to prevent regressions in tail handling.

Proposed test addition
 func TestMessageReaderAcceptsNewlineDelimited(t *testing.T) {
 	in := strings.NewReader(`{"jsonrpc":"2.0","method":"tools/list"}` + "\n")
 	msg, err := newMessageReader(in).read()
 	if err != nil {
 		t.Fatalf("read: %v", err)
 	}
 	if msg.Method != "tools/list" {
 		t.Fatalf("method=%q want tools/list", msg.Method)
 	}
 }
+
+func TestMessageReaderAcceptsFinalLineWithoutTrailingNewline(t *testing.T) {
+	in := strings.NewReader(`{"jsonrpc":"2.0","method":"tools/list"}`)
+	msg, err := newMessageReader(in).read()
+	if err != nil {
+		t.Fatalf("read: %v", err)
+	}
+	if msg.Method != "tools/list" {
+		t.Fatalf("method=%q want tools/list", msg.Method)
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/mcp/protocol_test.go` around lines 27 - 36, Add a regression test
that verifies newMessageReader.read correctly handles a final JSON-RPC message
that ends at EOF without a trailing newline: create a
TestMessageReaderAcceptsEOFTerminated (or similar) in
internal/mcp/protocol_test.go that constructs an io.Reader from a JSON-RPC
message string without a trailing "\n", calls newMessageReader(...).read(),
asserts no error, and checks msg.Method == "tools/list" (matching the existing
newline test's assertions) to ensure the EOF-terminated path in read() is
exercised.
internal/tools/bash_tool_test.go (1)

254-288: ⚡ Quick win

Use the test hook for bashWaitDelay so this regression doesn’t add 3.5s to every run.

This test can stay deterministic with a much shorter child sleep by temporarily lowering bashWaitDelay inside the test. That keeps the regression coverage without baking a multi-second delay into the suite.

Proposed test tightening
 func TestBashToolTimeoutKillsBackgroundChildren(t *testing.T) {
 	if runtime.GOOS == "windows" {
 		t.Skip("process-group kill is POSIX-only")
 	}
+	oldWaitDelay := bashWaitDelay
+	bashWaitDelay = 100 * time.Millisecond
+	defer func() { bashWaitDelay = oldWaitDelay }()
+
 	root := t.TempDir()
 	sentinel := filepath.Join(root, "leaked")
@@
-	command := fmt.Sprintf("(sleep 3; touch %s) & wait", shellQuote(sentinel))
+	command := fmt.Sprintf("(sleep 1; touch %s) & wait", shellQuote(sentinel))
@@
-	time.Sleep(3500 * time.Millisecond)
+	time.Sleep(1200 * time.Millisecond)
 	if _, err := os.Stat(sentinel); err == nil {
 		t.Fatalf("background child survived the timeout and wrote %s", sentinel)
 	} else if !os.IsNotExist(err) {
 		t.Fatalf("unexpected error stat-ing sentinel: %v", err)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tools/bash_tool_test.go` around lines 254 - 288, The test should use
the test hook for bashWaitDelay to avoid multi-second sleeps: inside
TestBashToolTimeoutKillsBackgroundChildren save the original := bashWaitDelay,
set bashWaitDelay = 10 * time.Millisecond (or similar small duration) before
constructing command, defer restoring the original, and shorten the child sleep
and final verification sleep accordingly (e.g., change "sleep 3" to "sleep 30ms"
in command and the final time.Sleep(3500 * time.Millisecond) to ~100ms) so the
test remains deterministic without long delays; keep references to
TestBashToolTimeoutKillsBackgroundChildren and the package-level variable
bashWaitDelay when making the change.
internal/providers/openai/provider.go (1)

350-360: ⚡ Quick win

Remove redundant normalization in the return statement.

Line 356 calls strings.ToLower(strings.TrimSpace(requested)) again, but requested was already normalized at line 354. Return the pre-normalized value directly.

♻️ Proposed fix
 func openAIReasoningEffort(requested string) string {
-	switch strings.ToLower(strings.TrimSpace(requested)) {
+	normalized := strings.ToLower(strings.TrimSpace(requested))
+	switch normalized {
 	case "minimal", "low", "medium", "high":
-		return strings.ToLower(strings.TrimSpace(requested))
+		return normalized
 	default:
 		return ""
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/openai/provider.go` around lines 350 - 360, The function
openAIReasoningEffort re-normalizes requested twice; capture the normalized
value once (e.g., assign norm := strings.ToLower(strings.TrimSpace(requested)))
and use that for the switch and as the return value for the allowed cases
instead of calling strings.ToLower(strings.TrimSpace(requested)) again; update
the switch in openAIReasoningEffort to return the pre-normalized variable for
"minimal","low","medium","high" and keep default returning "".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/cron_run.go`:
- Around line 175-177: Introduce a sentinel not-found error in the cron store
(e.g. var ErrNotFound = errors.New("cron job not found")) and change Store.Get
to return errors.Is-compatible wrapping: return ErrNotFound (or fmt.Errorf("%w:
%v", ErrNotFound, err)) only when the underlying failure is os.IsNotExist; other
IO/parsing errors must be returned unchanged. In cron_run (the code path that
calls Store.Get), only take the “job removed during run” early-return when
errors.Is(err, store.ErrNotFound); for any other Get error, do not swallow it —
log or surface it and continue to record the run via store.AppendRun and
store.Update (or fail loudly per desired policy). Add unit tests for Store.Get
that assert ErrNotFound for missing metadata and a non-ErrNotFound for
corrupt/unreadable metadata, and tests for cron_run behavior for both cases
(removed vs other error).

In `@internal/cli/cron_test.go`:
- Around line 111-113: The current assertion only checks jobs[0].Prompt is
non-empty which can pass via a fallback; update the test to assert that the job
prompt equals (or contains) the recipe's explicit prompt value to ensure
propagation. Locate the test's recipe setup (the recipe instance or
expectedPrompt variable used to create the job) and replace the empty-check on
jobs[0].Prompt with a strict comparison to that expected prompt (e.g., compare
to recipe.Prompt or expectedPrompt) and include a helpful failure message
referencing jobs and the expected value.

In `@internal/hooks/dispatch.go`:
- Around line 159-178: classifyResult currently maps any result.Err to
AuditError which lets timed-out/cancelled beforeTool hooks (from
execCommandRunner) fail-open; change classifyResult to treat
context.DeadlineExceeded and context.Canceled (use errors.Is(result.Err,
context.DeadlineExceeded) / errors.Is(result.Err, context.Canceled)) as vetoes
by returning (AuditBlocked, true) and ensure the blocking reason is surfaced by
using blockReason(result) (or include result.Err.Error() if no output) so
timeouts show as a block reason; update tests by adding a regression next to the
launch-failure test to assert that a timed-out beforeTool hook results in
AuditBlocked and includes the timeout/cancel reason.

In `@internal/providerhealth/providerhealth.go`:
- Around line 455-467: safeDialContext validates all DNS answers but only tries
addrs[0], causing false failures; change the dial step so after
resolver.LookupNetIP and the blockedAddrReason checks you iterate over addrs and
attempt dialer.DialContext for each net.JoinHostPort(addr.String(), port) until
one succeeds, returning that connection immediately; if none succeed, return an
endpointSafetyError (or the last dial error) so callers get failure only after
every validated address was tried; reference resolver.LookupNetIP,
blockedAddrReason, dialer.DialContext, network, port, and addrs in your changes.

In `@internal/providers/anthropic/provider.go`:
- Around line 437-441: The replay logic in the switch handling block.Type inside
provider.go omits the signature for the "redacted_thinking" case, causing
replayed request shapes to diverge; update the "redacted_thinking" branch (the
switch that appends to blocks) to include the "signature" field from
block.Signature alongside "data" (mirroring the "thinking" case) so replayed
artifacts retain both data and signature when building the next Anthropic
request.
- Around line 336-340: emitDone currently calls state.closeOpen() but doesn't
finalize open entries in state.thinking before sending the StreamEventDone via
providerio.SendEvent, so any pending thinking deltas are dropped; fix by
ensuring you finalize/flush open thinking buffers (e.g., call a new or existing
method like state.finalizeThinking() or state.flushThinkingBuffers that
moves/merges entries from state.thinking into state.reasoningBlocks) immediately
after state.closeOpen() and before providerio.SendEvent, and if such a helper
doesn't exist implement it to convert partial thinking buffers into completed
reasoning block entries so the final StreamEventDone contains consistent
reasoningBlocks.

In `@internal/providers/gemini/types.go`:
- Around line 15-20: Update the comment for type thinkingConfig to reflect
actual behavior: note that mapped.GenerationConfig.ThinkingConfig is only
assigned when thinkingBudgetForEffort(request.ReasoningEffort) > 0 (so a zero
budget is not sent to Gemini), and remove the incorrect reference to an
IncludeThoughts request field; also mention that thought summaries are
suppressed by skipping streamed parts where part.Thought is true rather than via
an IncludeThoughts flag. Keep references to thinkingConfig,
mapped.GenerationConfig.ThinkingConfig, thinkingBudgetForEffort, and
part.Thought so reviewers can locate the relevant code.

In `@internal/sessions/store_test.go`:
- Line 106: The call to file.Close() in the test is ignoring its returned error;
update the test in internal/sessions/store_test.go to capture and assert the
close succeeded (e.g., err := file.Close(); require.NoError(t, err) or t.Fatalf
on error) so the file is flushed/closed before reading it back; reference the
existing file variable and its Close() call to locate where to add the check.
- Line 135: The call to file.Close() in the test ignores its returned error;
change it to check and fail the test on error (e.g. capture err := file.Close()
and call t.Fatalf or use require.NoError(t, err)) so any flush/close failure is
surfaced before the test reads the file back; update the invocation of
file.Close() accordingly in the test in store_test.go.

In `@internal/tools/apply_patch.go`:
- Around line 196-206: parseHunkCounts currently scans the whole line and picks
up any +/- tokens after the second "@@", allowing an attacker to append e.g.
"+1,999" to override counts; restrict parsing to only the hunk header section by
extracting the substring between the first "@@" and the second "@@" (or to the
end if second "@@" missing), then run strings.Fields on that segment and pass
the numeric parts to hunkCount (referencing parseHunkCounts and hunkCount); this
prevents tokens outside the hunk header from influencing old/next counts and
stops patchHeaderPaths / validatePatchPaths from being confused by injected
tokens.

---

Nitpick comments:
In `@internal/mcp/protocol_test.go`:
- Around line 27-36: Add a regression test that verifies newMessageReader.read
correctly handles a final JSON-RPC message that ends at EOF without a trailing
newline: create a TestMessageReaderAcceptsEOFTerminated (or similar) in
internal/mcp/protocol_test.go that constructs an io.Reader from a JSON-RPC
message string without a trailing "\n", calls newMessageReader(...).read(),
asserts no error, and checks msg.Method == "tools/list" (matching the existing
newline test's assertions) to ensure the EOF-terminated path in read() is
exercised.

In `@internal/providers/openai/provider.go`:
- Around line 350-360: The function openAIReasoningEffort re-normalizes
requested twice; capture the normalized value once (e.g., assign norm :=
strings.ToLower(strings.TrimSpace(requested))) and use that for the switch and
as the return value for the allowed cases instead of calling
strings.ToLower(strings.TrimSpace(requested)) again; update the switch in
openAIReasoningEffort to return the pre-normalized variable for
"minimal","low","medium","high" and keep default returning "".

In `@internal/tools/bash_tool_test.go`:
- Around line 254-288: The test should use the test hook for bashWaitDelay to
avoid multi-second sleeps: inside TestBashToolTimeoutKillsBackgroundChildren
save the original := bashWaitDelay, set bashWaitDelay = 10 * time.Millisecond
(or similar small duration) before constructing command, defer restoring the
original, and shorten the child sleep and final verification sleep accordingly
(e.g., change "sleep 3" to "sleep 30ms" in command and the final time.Sleep(3500
* time.Millisecond) to ~100ms) so the test remains deterministic without long
delays; keep references to TestBashToolTimeoutKillsBackgroundChildren and the
package-level variable bashWaitDelay when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 391b0400-df03-4455-9e5f-6c5f17eb27d6

📥 Commits

Reviewing files that changed from the base of the PR and between f32d07b and 17a91d9.

📒 Files selected for processing (42)
  • internal/agent/loop.go
  • internal/agent/loop_test.go
  • internal/agent/types.go
  • internal/cli/cron.go
  • internal/cli/cron_run.go
  • internal/cli/cron_test.go
  • internal/cli/exec.go
  • internal/cli/exec_test.go
  • internal/hooks/dispatch.go
  • internal/hooks/dispatch_test.go
  • internal/mcp/protocol.go
  • internal/mcp/protocol_test.go
  • internal/providerhealth/connectivity_client_test.go
  • internal/providerhealth/providerhealth.go
  • internal/providers/anthropic/provider.go
  • internal/providers/anthropic/provider_test.go
  • internal/providers/anthropic/types.go
  • internal/providers/gemini/provider.go
  • internal/providers/gemini/provider_test.go
  • internal/providers/gemini/types.go
  • internal/providers/openai/provider.go
  • internal/providers/openai/provider_test.go
  • internal/providers/openai/types.go
  • internal/sandbox/runner.go
  • internal/sandbox/runner_test.go
  • internal/sessions/store.go
  • internal/sessions/store_test.go
  • internal/specialist/exec.go
  • internal/specialist/exec_test.go
  • internal/streamjson/streamjson.go
  • internal/streamjson/streamjson_test.go
  • internal/tools/apply_patch.go
  • internal/tools/bash.go
  • internal/tools/bash_proc_unix.go
  • internal/tools/bash_proc_windows.go
  • internal/tools/bash_tool_test.go
  • internal/tools/file_tools_test.go
  • internal/tools/grep.go
  • internal/tools/write_tools_test.go
  • internal/tui/model.go
  • internal/zeroruntime/helpers.go
  • internal/zeroruntime/types.go

Comment thread internal/cli/cron_run.go Outdated
Comment thread internal/cli/cron_test.go Outdated
Comment thread internal/hooks/dispatch.go
Comment thread internal/providerhealth/providerhealth.go Outdated
Comment thread internal/providers/anthropic/provider.go
Comment thread internal/providers/anthropic/provider.go
Comment thread internal/providers/gemini/types.go Outdated
Comment thread internal/sessions/store_test.go Outdated
Comment thread internal/sessions/store_test.go Outdated
Comment thread internal/tools/apply_patch.go
Fixes from the code review on the critical/high audit branch:

- tools/apply_patch: parseHunkCounts now reads only the range section between
  the opening and closing "@@". A crafted section heading like
  "@@ -1,1 +1,1 @@ +1,999999" could otherwise overwrite the line count, wedge
  the parser in hunk mode, and swallow later file headers so they escaped
  validatePatchPaths / recheckPatchWriteTargets — a workspace-confinement bypass.
- providerhealth: the SSRF-hardened dial now tries every validated address in
  order instead of pinning addrs[0], so a dual-stack/multi-record provider isn't
  failed by a single dead record. Extracted dialValidatedAddrs for coverage.
- hooks/dispatch: a beforeTool hook killed by its deadline now fails CLOSED
  (vetoes the tool) instead of fail-open. A launch failure (missing binary)
  still fails open so a misconfigured hook can't wedge every tool call.
- cron: Store.Get returns a distinct ErrJobNotFound sentinel; cron_run only
  treats that as "removed during run" and still records/persists on a transient
  read error instead of silently dropping the run.
- providers/anthropic: closeOpen finalizes thinking buffers still open at stream
  end (SSE ended after thinking_delta/signature_delta but before
  content_block_stop), so they survive in the done event's ReasoningBlocks for
  replay. Finalized in index order.
- providers/gemini: corrected the stale thinkingConfig comment (no IncludeThoughts
  field; thought summaries are suppressed by skipping part.Thought parts).

Tests: regression coverage for each fix, plus the requested nitpicks —
EOF-terminated MCP line, recipe-prompt propagation assertion, checked
file.Close() in session store tests, and a shortened bash process-group timeout
test via the bashWaitDelay hook.

Note: declined the suggestion to replay a "signature" on redacted_thinking
blocks — Anthropic's redacted_thinking carries only an opaque "data" field; the
signature is for regular thinking blocks, and the stream never populates one for
redacted blocks.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Addressed the review findings (head f638ceb). Each fix ships with regression coverage; full go test ./..., go vet, and gofmt are clean.

Fixed

  • apply_patch hunk-count bypass (Major/security)parseHunkCounts now parses only the range section between the opening and closing @@. A crafted heading like @@ -1,1 +1,1 @@ +1,999999 could otherwise inflate the count, wedge the parser in hunk mode, and hide a later --- /+++ file header from validatePatchPaths/recheckPatchWriteTargets (workspace-confinement bypass). Test: TestApplyPatchToolRejectsHunkCountInflationHidingEscapePath.
  • providerhealth single-address pinning (Major) — the SSRF-hardened dial now tries every validated address in order instead of addrs[0], so a dual-stack / multi-record provider isn't failed by one dead record. Extracted dialValidatedAddrs for coverage.
  • hooks fail-open on timeout (Major) — a beforeTool hook killed by its deadline now fails closed (vetoes the tool); a true launch failure (missing binary) still fails open so a misconfigured hook can't wedge every tool call. Tests cover both.
  • cron not-found over-broad (Major)Store.Get returns a distinct ErrJobNotFound; cron_run only treats that as "removed during run" and still records + persists on a transient read error.
  • Anthropic open thinking buffers (Major)closeOpen finalizes thinking blocks still open at stream end (SSE ended after thinking_delta/signature_delta but before content_block_stop), so they survive in the done event's ReasoningBlocks for replay.
  • gemini stale comment, cron_test recipe-prompt assertion, store_test file.Close() checks, MCP EOF-terminated line test, bash timeout test sped up via the bashWaitDelay hook.

Declined (with reasoning)

  • Replay the redacted_thinking signature too — Anthropic's redacted_thinking blocks carry only an opaque data field; the signature field belongs to regular thinking blocks, and the stream never populates a signature for redacted blocks. Adding one would send a field the API doesn't expect for that block type, so the current {type: redacted_thinking, data} replay is correct.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

@gnanam1990 Thanks for the detailed breakdown. The fixes look well-targeted:

  • apply_patch hunk-count bypass — anchoring parseHunkCounts to the @@ ... @@ range section is the right fix; the inflation-hiding-escape-path test closes the loop cleanly.
  • providerhealth multi-address dialing — iterating all validated addresses rather than addrs[0] is the correct dual-stack behavior. Good call extracting dialValidatedAddrs for direct test coverage.
  • hooks fail-closed on deadline — the distinction between timeout-killed (veto) vs. launch failure (fail-open) is exactly the right security posture.
  • cron ErrJobNotFound — returning a typed sentinel instead of a generic error makes the "removed during run" path precise and avoids silently swallowing transient read errors.
  • Anthropic open thinking bufferscloseOpen ensuring in-flight thinking_delta/signature_delta blocks survive to the done event is necessary for correct replay.

On the declined item: the reasoning is sound. Anthropic's redacted_thinking block spec uses only data (opaque bytes the model needs echoed back); signature is specific to unredacted thinking blocks. Sending a signature field on a redacted_thinking replay would be malformed per the API contract.

Triggering a fresh review of the updated head.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/providerhealth/connectivity_client_test.go (1)

37-45: ⚡ Quick win

Assert the “last error” contract explicitly.

At Line 42, this test only checks that an error exists, so it would still pass if dialValidatedAddrs returned the first failure instead of the last. Use distinct per-attempt errors and assert errors.Is(err, lastErr) (and optionally assert attempt count) to lock in the behavior this test name promises.

Proposed test tightening
 func TestDialValidatedAddrsReturnsLastErrorWhenAllFail(t *testing.T) {
 	addrs := []netip.Addr{netip.MustParseAddr("203.0.113.1"), netip.MustParseAddr("203.0.113.2")}
+	firstErr := errors.New("first unreachable")
+	lastErr := errors.New("last unreachable")
+	attempt := 0
 	dial := func(_ context.Context, _ string) (net.Conn, error) {
-		return nil, errors.New("unreachable")
+		attempt++
+		if attempt == 1 {
+			return nil, firstErr
+		}
+		return nil, lastErr
 	}
-	if _, err := dialValidatedAddrs(context.Background(), addrs, "443", dial); err == nil {
+	_, err := dialValidatedAddrs(context.Background(), addrs, "443", dial)
+	if err == nil {
 		t.Fatal("expected an error when every address fails to dial")
 	}
+	if !errors.Is(err, lastErr) {
+		t.Fatalf("expected last error, got %v", err)
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providerhealth/connectivity_client_test.go` around lines 37 - 45,
TestDialValidatedAddrsReturnsLastErrorWhenAllFail currently only checks for any
error; update it to use distinct per-attempt errors and assert that the returned
error matches the last attempt's error (use errors.Is(err, lastErr)) to enforce
the “last error” contract for dialValidatedAddrs, and optionally verify the dial
function was invoked for each address (attempt count) to ensure all addresses
were tried.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/providerhealth/connectivity_client_test.go`:
- Around line 37-45: TestDialValidatedAddrsReturnsLastErrorWhenAllFail currently
only checks for any error; update it to use distinct per-attempt errors and
assert that the returned error matches the last attempt's error (use
errors.Is(err, lastErr)) to enforce the “last error” contract for
dialValidatedAddrs, and optionally verify the dial function was invoked for each
address (attempt count) to ensure all addresses were tried.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 34fbde2e-2646-4614-913d-d195dac25362

📥 Commits

Reviewing files that changed from the base of the PR and between 17a91d9 and f638ceb.

📒 Files selected for processing (16)
  • internal/cli/cron_run.go
  • internal/cli/cron_test.go
  • internal/cron/store.go
  • internal/cron/store_test.go
  • internal/hooks/dispatch.go
  • internal/hooks/dispatch_test.go
  • internal/mcp/protocol_test.go
  • internal/providerhealth/connectivity_client_test.go
  • internal/providerhealth/providerhealth.go
  • internal/providers/anthropic/provider.go
  • internal/providers/anthropic/provider_test.go
  • internal/providers/gemini/types.go
  • internal/sessions/store_test.go
  • internal/tools/apply_patch.go
  • internal/tools/bash_tool_test.go
  • internal/tools/write_tools_test.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • internal/cli/cron_test.go
  • internal/providers/gemini/types.go
  • internal/mcp/protocol_test.go
  • internal/tools/apply_patch.go
  • internal/tools/bash_tool_test.go
  • internal/providers/anthropic/provider_test.go
  • internal/providerhealth/providerhealth.go
  • internal/tools/write_tools_test.go
  • internal/sessions/store_test.go
  • internal/providers/anthropic/provider.go
  • internal/hooks/dispatch.go
  • internal/hooks/dispatch_test.go

…test

Use distinct per-attempt errors and assert errors.Is(err, lastErr) plus the
attempt count, so a regression returning the first failure (or short-circuiting)
is caught instead of slipping past a bare non-nil check.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Pushed 7dcbf4f.

  • dialValidatedAddrs last-error test (nitpick) — fixed: the test now uses distinct per-attempt errors and asserts errors.Is(err, lastErr) plus the attempt count, so a regression returning the first failure is caught.

The other two open inline threads are already resolved:

  • cron_run.go:177 — distinguish not-found from IO error — done in f638ceb: cron.Store.Get now returns a wrapped ErrJobNotFound sentinel, and cron_run only takes the "removed during run" path for that sentinel (transient read errors warn but still record + persist). Tests: TestStoreGetDistinguishesNotFoundFromReadError.
  • provider.go:442 — replay redacted_thinking signature — declining (with reasoning): Anthropic's redacted_thinking blocks carry only an opaque data field; the signature field belongs to regular thinking blocks, and the stream never populates a signature for redacted blocks. Adding one would send a field the API doesn't expect for that block type, so the current {type: redacted_thinking, data} replay matches the contract.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

@gnanam1990 The 7dcbf4f fix for the dialValidatedAddrs last-error test is the right approach — distinct per-attempt errors + errors.Is + attempt count covers the two independent regression vectors (wrong error returned, loop exited early). That closes the nitpick cleanly.

The two inline threads are confirmed resolved:

  • cron_run.go:177 — typed ErrJobNotFound sentinel with TestStoreGetDistinguishesNotFoundFromReadError is the correct fix; transient IO errors no longer silently discard run records.
  • provider.go:442 — the redacted_thinking decline stands; Anthropic's spec only carries data for that block type, no signature field to echo.

Triggering a fresh review of the updated head.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approvable ?

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List open review comments on PR `#167`
gh pr view 167 --repo Gitlawb/zero --json reviewDecision,reviews,state
echo "---"
# Get review threads
gh api repos/Gitlawb/zero/pulls/167/comments --jq '[.[] | {id: .id, path: .path, line: .line, body: .body[0:120], resolved: false}]' 2>/dev/null | head -100
echo "---"
# Check review thread resolution status via GraphQL
gh api graphql -f query='
{
  repository(owner: "Gitlawb", name: "zero") {
    pullRequest(number: 167) {
      reviewThreads(first: 20) {
        nodes {
          isResolved
          isOutdated
          comments(first: 1) {
            nodes {
              author { login }
              body
              path
              line
            }
          }
        }
      }
    }
  }
}' 2>/dev/null

Length of output: 58487


🏁 Script executed:

#!/bin/bash
# Resolve the cron_run.go outdated thread via GraphQL
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "PRRT_kwDOSqaaUs5Zv7s5"}) {
    thread { isResolved }
  }
}' 2>/dev/null || true

# List threads to get the node IDs for targeted resolution
gh api graphql -f query='
{
  repository(owner: "Gitlawb", name: "zero") {
    pullRequest(number: 167) {
      reviewThreads(first: 20) {
        nodes {
          id
          isResolved
          isOutdated
          comments(first: 1) {
            nodes {
              path
              line
              body
            }
          }
        }
      }
    }
  }
}' 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
threads = data['data']['repository']['pullRequest']['reviewThreads']['nodes']
for t in threads:
    comment = t['comments']['nodes'][0] if t['comments']['nodes'] else {}
    print(f\"ID={t['id']} resolved={t['isResolved']} outdated={t['isOutdated']} path={comment.get('path','')} line={comment.get('line','')}\")"

Length of output: 2325


🏁 Script executed:

#!/bin/bash
# Resolve the cron_run.go outdated thread
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "PRRT_kwDOSqaaUs6Ivr0W"}) {
    thread { id isResolved }
  }
}' 2>/dev/null

# Resolve the redacted_thinking thread (withdrawn — reasoning is correct per API contract)
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "PRRT_kwDOSqaaUs6Ivr1C"}) {
    thread { id isResolved }
  }
}' 2>/dev/null

Length of output: 680



@gnanam1990 Both remaining threads are now resolved:

  • cron_run.go (outdated) — marked resolved; the ErrJobNotFound sentinel fix in f638ceb fully addresses it.
  • provider.go:442 (redacted_thinking signature) — withdrawn; the decline reasoning is correct per the Anthropic API contract.

All critical and high findings have been addressed with regression coverage. Approving the PR.

[approve]

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approve

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE. I reviewed the critical/high audit fixes around provider reasoning/finish reasons, MCP message bounds/framing, provider health safe dialing, hook dispatch timeout behavior, bash process lifetime hardening, cron/session edge cases, and the tool parsing fixes. I did not find a blocking issue. Validation: go test ./... passed locally in D:\tmp\zero-review-167.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants