Skip to content

fix(review): surface reviewer admission diagnostics - #2082

Closed
Maicololiveras wants to merge 1 commit into
Gentleman-Programming:mainfrom
Maicololiveras:fix/reviewer-result-recovery
Closed

fix(review): surface reviewer admission diagnostics#2082
Maicololiveras wants to merge 1 commit into
Gentleman-Programming:mainfrom
Maicololiveras:fix/reviewer-result-recovery

Conversation

@Maicololiveras

@Maicololiveras Maicololiveras commented Jul 31, 2026

Copy link
Copy Markdown

Linked Issue

Closes #2028

PR Type

  • type:bug - Bug fix
  • type:feature - New feature
  • type:docs - Documentation only
  • type:refactor - Code refactoring
  • type:chore - Build, CI, or tooling changes
  • type:breaking-change - Breaking change

Maintainer action required: apply exactly the type:bug label. GitHub does not grant fork authors permission to label upstream pull requests.

Summary

  • Validate reviewer finding locations with typed errors instead of silently parsing malformed line suffixes as line zero.
  • Return bounded structured admission diagnostics containing actionable finding and location details.
  • Surface only allowlisted recovery diagnostics through the OpenCode adapter while preserving opaque path filtering and fail-closed candidate causality.

Changes

File / Area What Changed
internal/reviewtransaction Added typed location validation, structured admission diagnostics, and fail-closed tests.
internal/cli Propagated typed location failures through reviewer result capture.
internal/assets/opencode Exposed allowlisted recovery details and rejected unsafe diagnostic content.

Test Plan

Focused tests

Broad validation

  • go test ./... did not complete on Windows. e2e/organicruntime and a later internal/cli test timed out. The candidate-caused refusal-policy failure found before the timeout was fixed, and its exact test plus affected focused packages pass.
  • cd bench && go build ./...
  • cd bench && go vet ./...
  • cd bench && go test ./... fails three Windows-only shell fixture tests that create extensionless #!/bin/sh executables.
  • Docker E2E was not run from this Windows checkout.

Automated Checks

Check Status Description
Check PR Cognitive Load Pending 290 changed lines
Check Issue Reference Pending Closes #2028
Check Issue Has status:approved Pending Approved before implementation
Check PR Has type:* Label Pending Maintainer must apply exactly type:bug
Unit Tests Pending Awaiting Linux CI
Go Format Pending Local check passed
E2E Tests Pending Awaiting CI

Contributor Checklist

  • PR is linked to an issue with status:approved
  • PR stays within 400 changed lines
  • Exactly one type:* label is applied, maintainer permission is required
  • Full unit suite passes locally, see Windows limitations above
  • Go format passes
  • E2E tests pass locally, awaiting CI
  • Benchmark tests pass locally, see Windows shell limitation above
  • Documentation is not required for this internal recovery behavior
  • Commit follows Conventional Commits format
  • Commit does not include Co-Authored-By trailers

Notes for Reviewers

Please challenge the admission guard with real input populations: canonical repository-relative single-line locations remain accepted, malformed ranges, text, zero, and negative lines are rejected with typed reasons, and candidate-causal findings on unchanged lines remain out_of_scope.

  • Reviewed the integrity and admission guard against legitimate and invalid populations.
  • No guard:population direction or baseline change was required; the existing fail-closed direction remains unchanged.
  • .guard-population-baseline.txt is unchanged.

Receipt-driven development was disabled globally, so the native lifecycle did not start and delivery is recorded as disabled/unmanaged. No review PASS is claimed.

Summary by CodeRabbit

  • Bug Fixes
    • Improved review artifact rejection messages with clear, structured diagnostics for invalid finding locations.
    • Added actionable details such as the affected finding, location, and validation reason.
    • Invalid or unsafe diagnostic content is now filtered to prevent exposing sensitive native error details.
    • Malformed locations—including absolute paths, non-canonical paths, and invalid line numbers—are rejected consistently.
    • Existing fallback behavior remains available when diagnostic details cannot be safely parsed.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds typed finding-location validation and structured artifact admission diagnostics. CLI and OpenCode recovery handling now preserve validated finding details while filtering unsafe or unparseable native error content.

Changes

Admission diagnostics

Layer / File(s) Summary
Finding-location validation
internal/reviewtransaction/compact.go, internal/reviewtransaction/snapshot.go, internal/reviewtransaction/compact_store_test.go
Finding locations are parsed into canonical repository-relative paths and positive line numbers. Invalid formats return typed reasons.
Typed admission error propagation
internal/reviewtransaction/artifact_admission.go, internal/reviewtransaction/artifact_admission_test.go, internal/cli/review_artifact.go, internal/cli/review_artifact_test.go
Admission errors preserve causes and expose structured diagnostics for invalid locations and unproven candidate causality.
Recovery diagnostic sanitization
internal/assets/opencode/plugins/review-result-artifacts.ts, internal/assets/review_plugin_recovery_test.go
The OpenCode plugin validates bounded diagnostic fields and displays sanitized finding details with existing relaunch guidance.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels: type:bug

Suggested reviewers: alan-thegentleman, matam15

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds typed diagnostics and relaunch guidance but does not show a bounded recovery or terminal lifecycle transition required by issue #2028. Implement and expose a bounded provider-owned recovery or terminal transition after admission rejection, without weakening causal validation.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: exposing reviewer admission diagnostics.
Out of Scope Changes check ✅ Passed The changes remain focused on finding-location validation, structured admission diagnostics, recovery handling, and related tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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: 2

🤖 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/assets/opencode/plugins/review-result-artifacts.ts`:
- Around line 289-322: Update the safeLocation validation in admissionRejection
to scan the entire parsed.location for ".." path segments across both slash and
colon separators, rather than only the substring before the first colon.
Preserve the existing location safety checks and add a regression test alongside
TestReviewPluginSurfacesStructuredLocationRecoveryDiagnostic covering a
colon-embedded traversal location such as "good:../../secret:1".

In `@internal/reviewtransaction/artifact_admission.go`:
- Around line 282-289: In the location error handling of the artifact admission
flow, check the boolean result from errors.As before accessing
typedLocationErr.Reason. Update the parseFindingLocation error branch to safely
handle a failed type match, following the established pattern in
NewArtifactLocationAdmissionError, while preserving the existing diagnostic and
return behavior for *FindingLocationError values.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b7ab24f9-7800-46a3-bc3c-5b16690dbfd1

📥 Commits

Reviewing files that changed from the base of the PR and between 0126077 and 4c5409c.

📒 Files selected for processing (9)
  • internal/assets/opencode/plugins/review-result-artifacts.ts
  • internal/assets/review_plugin_recovery_test.go
  • internal/cli/review_artifact.go
  • internal/cli/review_artifact_test.go
  • internal/reviewtransaction/artifact_admission.go
  • internal/reviewtransaction/artifact_admission_test.go
  • internal/reviewtransaction/compact.go
  • internal/reviewtransaction/compact_store_test.go
  • internal/reviewtransaction/snapshot.go

Comment on lines +289 to 322
const ADMISSION_DIAGNOSTIC = /; admission_diagnostic=(\{[^\r\n]{1,1024}\})$/
const ADMISSION_DIAGNOSTIC_REASONS = new Set([
"expected_path_and_line", "line_suffix_not_integer", "line_must_be_positive",
"path_must_be_repository_relative", "path_must_be_canonical", "line_not_changed_by_candidate",
])

type AdmissionDiagnostic = {
code: "invalid_finding_location" | "candidate_causality_unproven"
finding_id: string
location: string
reason: string
}

function admissionRejection(cause: unknown): string | undefined {
const match = ADMISSION_REJECTION.exec(errorMessage(cause))
return match ? match[1] : undefined
function admissionRejection(cause: unknown): { decision: string, diagnostic?: AdmissionDiagnostic } | undefined {
const message = errorMessage(cause)
const match = ADMISSION_REJECTION.exec(message)
if (!match) return undefined
const detail = ADMISSION_DIAGNOSTIC.exec(message)
if (!detail) return { decision: match[1] }
try {
const parsed = JSON.parse(detail[1]) as Partial<AdmissionDiagnostic>
const safeLocation = typeof parsed.location === "string" && parsed.location.length <= 256 &&
!/[\u0000-\u001f\u007f\\]/.test(parsed.location) && !/^(?:[A-Za-z]:[\\/]|[\\/])/.test(parsed.location) &&
!parsed.location.split(":", 1)[0].split("/").includes("..")
const safeID = typeof parsed.finding_id === "string" && /^R[1-4]-[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(parsed.finding_id)
const safeCode = parsed.code === "invalid_finding_location" || parsed.code === "candidate_causality_unproven"
const safeReason = typeof parsed.reason === "string" && ADMISSION_DIAGNOSTIC_REASONS.has(parsed.reason)
return safeLocation && safeID && safeCode && safeReason
? { decision: match[1], diagnostic: parsed as AdmissionDiagnostic }
: { decision: match[1] }
} catch {
return { decision: match[1] }
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fix the path-traversal filter: it only checks the segment before the first colon.

parsed.location is fully attacker/reviewer-controlled text: it is exactly the raw finding.Location that failed canonical validation, so it can contain any number of colons. The traversal check at Line 312 is !parsed.location.split(":", 1)[0].split("/").includes(".."), which only inspects the substring before the first colon.

A location such as "good:../../secret:1" bypasses this check: split(":", 1)[0] yields "good", which contains no ".." segment, so safeLocation evaluates to true even though the full string (displayed verbatim via JSON.stringify on Line 330) still carries the ../../secret traversal component. This defeats the "opaque path filtering" goal for this diagnostic path.

Scan the whole string across both separators, not just the pre-first-colon segment.

🛡️ Proposed fix to scan the full location for traversal segments
     const parsed = JSON.parse(detail[1]) as Partial<AdmissionDiagnostic>
     const safeLocation = typeof parsed.location === "string" && parsed.location.length <= 256 &&
       !/[\u0000-\u001f\u007f\\]/.test(parsed.location) && !/^(?:[A-Za-z]:[\\/]|[\\/])/.test(parsed.location) &&
-      !parsed.location.split(":", 1)[0].split("/").includes("..")
+      !parsed.location.split(/[/:]/).includes("..")

Consider adding a regression test in internal/assets/review_plugin_recovery_test.go alongside TestReviewPluginSurfacesStructuredLocationRecoveryDiagnostic for a location containing a colon-embedded .. segment (for example "good:../../secret:1"), to lock in the fix.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ADMISSION_DIAGNOSTIC = /; admission_diagnostic=(\{[^\r\n]{1,1024}\})$/
const ADMISSION_DIAGNOSTIC_REASONS = new Set([
"expected_path_and_line", "line_suffix_not_integer", "line_must_be_positive",
"path_must_be_repository_relative", "path_must_be_canonical", "line_not_changed_by_candidate",
])
type AdmissionDiagnostic = {
code: "invalid_finding_location" | "candidate_causality_unproven"
finding_id: string
location: string
reason: string
}
function admissionRejection(cause: unknown): string | undefined {
const match = ADMISSION_REJECTION.exec(errorMessage(cause))
return match ? match[1] : undefined
function admissionRejection(cause: unknown): { decision: string, diagnostic?: AdmissionDiagnostic } | undefined {
const message = errorMessage(cause)
const match = ADMISSION_REJECTION.exec(message)
if (!match) return undefined
const detail = ADMISSION_DIAGNOSTIC.exec(message)
if (!detail) return { decision: match[1] }
try {
const parsed = JSON.parse(detail[1]) as Partial<AdmissionDiagnostic>
const safeLocation = typeof parsed.location === "string" && parsed.location.length <= 256 &&
!/[\u0000-\u001f\u007f\\]/.test(parsed.location) && !/^(?:[A-Za-z]:[\\/]|[\\/])/.test(parsed.location) &&
!parsed.location.split(":", 1)[0].split("/").includes("..")
const safeID = typeof parsed.finding_id === "string" && /^R[1-4]-[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(parsed.finding_id)
const safeCode = parsed.code === "invalid_finding_location" || parsed.code === "candidate_causality_unproven"
const safeReason = typeof parsed.reason === "string" && ADMISSION_DIAGNOSTIC_REASONS.has(parsed.reason)
return safeLocation && safeID && safeCode && safeReason
? { decision: match[1], diagnostic: parsed as AdmissionDiagnostic }
: { decision: match[1] }
} catch {
return { decision: match[1] }
}
}
const ADMISSION_DIAGNOSTIC = /; admission_diagnostic=(\{[^\r\n]{1,1024}\})$/
const ADMISSION_DIAGNOSTIC_REASONS = new Set([
"expected_path_and_line", "line_suffix_not_integer", "line_must_be_positive",
"path_must_be_repository_relative", "path_must_be_canonical", "line_not_changed_by_candidate",
])
type AdmissionDiagnostic = {
code: "invalid_finding_location" | "candidate_causality_unproven"
finding_id: string
location: string
reason: string
}
function admissionRejection(cause: unknown): { decision: string, diagnostic?: AdmissionDiagnostic } | undefined {
const message = errorMessage(cause)
const match = ADMISSION_REJECTION.exec(message)
if (!match) return undefined
const detail = ADMISSION_DIAGNOSTIC.exec(message)
if (!detail) return { decision: match[1] }
try {
const parsed = JSON.parse(detail[1]) as Partial<AdmissionDiagnostic>
const safeLocation = typeof parsed.location === "string" && parsed.location.length <= 256 &&
!/[\u0000-\u001f\u007f\\]/.test(parsed.location) && !/^(?:[A-Za-z]:[\\/]|[\\/])/.test(parsed.location) &&
!parsed.location.split(/[/:]/).includes("..")
const safeID = typeof parsed.finding_id === "string" && /^R[1-4]-[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(parsed.finding_id)
const safeCode = parsed.code === "invalid_finding_location" || parsed.code === "candidate_causality_unproven"
const safeReason = typeof parsed.reason === "string" && ADMISSION_DIAGNOSTIC_REASONS.has(parsed.reason)
return safeLocation && safeID && safeCode && safeReason
? { decision: match[1], diagnostic: parsed as AdmissionDiagnostic }
: { decision: match[1] }
} catch {
return { decision: match[1] }
}
}
🧰 Tools
🪛 OpenGrep (1.26.0)

[ERROR] 304-304: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 306-306: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 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/assets/opencode/plugins/review-result-artifacts.ts` around lines 289
- 322, Update the safeLocation validation in admissionRejection to scan the
entire parsed.location for ".." path segments across both slash and colon
separators, rather than only the substring before the first colon. Preserve the
existing location safety checks and add a regression test alongside
TestReviewPluginSurfacesStructuredLocationRecoveryDiagnostic covering a
colon-embedded traversal location such as "good:../../secret:1".

Comment on lines +282 to +289
logicalPath, _, locationErr := parseFindingLocation(finding.Location)
if locationErr != nil {
var typedLocationErr *FindingLocationError
errors.As(locationErr, &typedLocationErr)
return failFinding(ArtifactAdmissionOutOfScope, "reviewer finding location is invalid",
findingAdmissionDiagnostic("invalid_finding_location", finding.ID, finding.Location, string(typedLocationErr.Reason)), locationErr)
}
if stringIndex(wantPaths, logicalPath) < 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Check the errors.As result before dereferencing typedLocationErr.

errors.As(locationErr, &typedLocationErr) on Line 285 discards its boolean result. If errors.As returns false, typedLocationErr stays nil, and typedLocationErr.Reason on Line 287 panics with a nil-pointer dereference.

Today parseFindingLocation only ever returns *FindingLocationError, so this path is not currently reachable. But this call site does not enforce that invariant, and it diverges from NewArtifactLocationAdmissionError at Line 114, which checks the same pattern correctly. Guard the dereference the same way there.

🛡️ Proposed fix to guard the type assertion
 		logicalPath, _, locationErr := parseFindingLocation(finding.Location)
 		if locationErr != nil {
 			var typedLocationErr *FindingLocationError
-			errors.As(locationErr, &typedLocationErr)
-			return failFinding(ArtifactAdmissionOutOfScope, "reviewer finding location is invalid",
-				findingAdmissionDiagnostic("invalid_finding_location", finding.ID, finding.Location, string(typedLocationErr.Reason)), locationErr)
+			reason := "invalid_location"
+			if errors.As(locationErr, &typedLocationErr) {
+				reason = string(typedLocationErr.Reason)
+			}
+			return failFinding(ArtifactAdmissionOutOfScope, "reviewer finding location is invalid",
+				findingAdmissionDiagnostic("invalid_finding_location", finding.ID, finding.Location, reason), locationErr)
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logicalPath, _, locationErr := parseFindingLocation(finding.Location)
if locationErr != nil {
var typedLocationErr *FindingLocationError
errors.As(locationErr, &typedLocationErr)
return failFinding(ArtifactAdmissionOutOfScope, "reviewer finding location is invalid",
findingAdmissionDiagnostic("invalid_finding_location", finding.ID, finding.Location, string(typedLocationErr.Reason)), locationErr)
}
if stringIndex(wantPaths, logicalPath) < 0 {
logicalPath, _, locationErr := parseFindingLocation(finding.Location)
if locationErr != nil {
var typedLocationErr *FindingLocationError
reason := "invalid_location"
if errors.As(locationErr, &typedLocationErr) {
reason = string(typedLocationErr.Reason)
}
return failFinding(ArtifactAdmissionOutOfScope, "reviewer finding location is invalid",
findingAdmissionDiagnostic("invalid_finding_location", finding.ID, finding.Location, reason), locationErr)
}
if stringIndex(wantPaths, logicalPath) < 0 {
🤖 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/reviewtransaction/artifact_admission.go` around lines 282 - 289, In
the location error handling of the artifact admission flow, check the boolean
result from errors.As before accessing typedLocationErr.Reason. Update the
parseFindingLocation error branch to safely handle a failed type match,
following the established pattern in NewArtifactLocationAdmissionError, while
preserving the existing diagnostic and return behavior for *FindingLocationError
values.

@Alan-TheGentleman

Alan-TheGentleman commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

The diagnostics work here provided an important early view of how reviewer recovery should behave. I am closing this PR only because commits 37a18c3c and 51a15463 have since landed the current implementation and superseded this precursor. This is not a rejection of your contribution. Your investigation helped clarify the behavior that the final recovery flow needed to support. The beta contains the current implementation; if you have time, please retest the behavior through the beta channel and share what you find: https://github.com/Gentleman-Programming/gentle-ai#beta-channel

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.

fix(review): provide recovery path for rejected reviewer results

2 participants