Skip to content

fix(tools): classify silent wrapped Windows command failures as sandbox denials#659

Merged
kevincodex1 merged 2 commits into
Gitlawb:mainfrom
euxaristia:fix/windows-empty-output-sandbox-denial
Jul 14, 2026
Merged

fix(tools): classify silent wrapped Windows command failures as sandbox denials#659
kevincodex1 merged 2 commits into
Gitlawb:mainfrom
euxaristia:fix/windows-empty-output-sandbox-denial

Conversation

@euxaristia

@euxaristia euxaristia commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Problem

A Windows restricted-token sandbox failure is often completely silent. When the token cannot open the target executable or one of its DLLs (see #658), or an MSYS runtime dies during init, the wrapped command exits nonzero having written zero bytes to stdout and stderr. The denial detection in internal/tools/sandbox_denial.go is keyword-based, so these failures are never marked as sandbox denials. The agent loop then never offers the unsandboxed-retry prompt, and the model is left retrying "Command completed with no output" until it gives up. This is exactly the failure mode that stranded a real session: six consecutive gh/git calls returned exit 1 with empty output and no diagnostic.

Fix

Treat a wrapped command on the Windows restricted-token or elevated backend that exits nonzero with completely empty combined output as a likely sandbox denial (kind: sandbox). This routes the result into the existing escalation path, so the user gets the retry-unsandboxed prompt instead of a bare failure.

Trade-off: a command that legitimately fails silently (findstr with no match, for example) now costs one extra approval prompt. The inverse misclassification silently strands the whole session, so the trade goes to prompting. Other backends are unaffected: bwrap and seatbelt failures are not silent in this way, and the existing keyword and seccomp checks are unchanged.

Verification

New unit tests cover the silent-failure classification, the exit-0 and non-Windows non-cases, and a failure that produced output. go build ./... and go test ./internal/tools ./internal/agent ./internal/sandbox pass.

Complementary to #658: that PR fixes the most common cause of these silent failures, this one makes any remaining ones (MSYS init deaths, Schannel-style limitations, DenyRead-configured profiles that keep the fully restricted token) recoverable instead of dead ends.

Summary by CodeRabbit

  • Bug Fixes
    • Improved detection of sandbox-denied commands on wrapped Windows backends when commands fail without producing output.
    • Ensured silent failures are only classified as sandbox denials in the correct wrapped Windows scenarios; prevented false positives for successful runs, non-Windows backends, and commands with unrelated output.

…ox denials

A Windows restricted-token sandbox failure is often completely silent:
when the token cannot open the target executable or its DLLs (or an
MSYS runtime dies during init), the command exits nonzero having
written zero bytes to stdout and stderr. The existing detection is
keyword-based, so these failures were never marked as sandbox denials,
the agent loop never offered the unsandboxed-retry prompt, and the
model was left staring at 'Command completed with no output' until it
gave up.

Treat a wrapped command on the Windows restricted-token or elevated
backend that exits nonzero with completely empty output as a likely
sandbox denial. A command that legitimately fails silently (findstr
with no match, for example) now costs one extra approval prompt; the
inverse misclassification silently strands the session, so the trade
goes to prompting.
@coderabbitai

coderabbitai Bot commented Jul 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

Run ID: 5680832c-5c8d-47ea-81a1-ff0d379039ed

📥 Commits

Reviewing files that changed from the base of the PR and between 0961bac and 5d726af.

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

Walkthrough

The sandbox denial classifier now detects silent failures from wrapped Windows restricted or elevated backends. Tests verify denial metadata and exclude successful commands, non-Windows backends, and output containing unrelated non-empty sections.

Changes

Sandbox denial detection

Layer / File(s) Summary
Windows no-output denial heuristic
internal/tools/sandbox_denial.go
Wrapped Windows backend failures with whitespace-empty sections return SandboxDenialKindSandbox and the "no output from sandboxed command" keyword.
Heuristic boundary tests
internal/tools/sandbox_denial_test.go
Tests validate denial metadata and exclude successful commands, Linux bwrap failures, and failures with unrelated output.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: Vasanthdev2004, anandh8x, gnanam1990, kevincodex1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the core change: silent wrapped Windows failures are now treated as sandbox denials.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

🧹 Nitpick comments (1)
internal/tools/sandbox_denial_test.go (1)

45-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a test for BackendWindowsElevated.

windowsWrappedBackend treats BackendWindowsElevated identically to BackendWindowsRestrictedToken, but no test exercises the elevated backend path. A quick table-driven or parameterized test covering both backends would close this gap.

♻️ Suggested additional test
 func TestLikelySandboxDeniedDetectsSilentWindowsWrappedFailure(t *testing.T) {
+	for _, backend := range []zeroSandbox.BackendName{
+		zeroSandbox.BackendWindowsRestrictedToken,
+		zeroSandbox.BackendWindowsElevated,
+	} {
 	plan := zeroSandbox.CommandPlan{
 		Wrapped:       true,
-		TargetBackend: zeroSandbox.BackendWindowsRestrictedToken,
+		TargetBackend: backend,
 	}
 	if !likelySandboxDenied(plan, 1, "", "  \n") {
 		t.Fatal("wrapped Windows command failing with empty output must be classified as sandbox denied")
 	}
 	meta := map[string]string{}
 	markLikelySandboxDenial(meta, plan, 1, "")
 	if meta[SandboxLikelyDeniedMeta] != "true" || meta[SandboxDenialKindMeta] != SandboxDenialKindSandbox {
 		t.Fatalf("silent windows denial meta = %#v", meta)
 	}
+	}
 }
🤖 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/sandbox_denial_test.go` around lines 45 - 89, Add coverage for
BackendWindowsElevated in the likelySandboxDenied tests, preferably by
parameterizing the Windows restricted-token and elevated backend cases. Verify
the elevated backend matches the existing silent wrapped failure classification
and metadata behavior while preserving the existing success and unrelated-output
expectations.
🤖 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/tools/sandbox_denial_test.go`:
- Around line 45-89: Add coverage for BackendWindowsElevated in the
likelySandboxDenied tests, preferably by parameterizing the Windows
restricted-token and elevated backend cases. Verify the elevated backend matches
the existing silent wrapped failure classification and metadata behavior while
preserving the existing success and unrelated-output expectations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 39324bf0-60c3-428d-a223-8c10747881a5

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1405d and 0961bac.

📒 Files selected for processing (2)
  • internal/tools/sandbox_denial.go
  • internal/tools/sandbox_denial_test.go

…test

The heuristic treats windows-elevated identically to
windows-restricted-token, but only the latter was exercised.
Parameterize the silent-failure test over both backends.
@euxaristia

Copy link
Copy Markdown
Contributor Author

Pushed 5d726af addressing the CodeRabbit suggestion: TestLikelySandboxDeniedDetectsSilentWindowsWrappedFailure is now parameterized over both windows-restricted-token and windows-elevated, so the elevated arm of windowsWrappedBackend has coverage too. No production code changes.

@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.

Reviewed the silent-denial classification. The heuristic is tight: it only fires for wrapped commands on the Windows restricted-token/elevated backends with a nonzero exit AND completely empty stdout+stderr, and the exitCode==0 early return (sandbox_denial.go:55) keeps a real success from ever being mislabeled as a denial. A genuine silent sandbox denial now gets the unsandboxed-retry prompt instead of stranding the session, and the only over-classification cost is one extra approval prompt for legitimately silent-failing wrapped commands like a no-match findstr — which the comment owns explicitly. Tests cover the success, empty-output, other-backend, and has-output cases; build/vet/gofmt/tools tests pass and it merges clean.

One minor note: BackendWindowsElevated is bundled into windowsWrappedBackend, but an elevated-token command isn't really "denied" by the sandbox, so a silent failure there is less clearly a denial — at worst it yields a spurious retry prompt, low harm, but worth a glance if you want to narrow it to the restricted-token backend only.

@euxaristia

Copy link
Copy Markdown
Contributor Author

On the BackendWindowsElevated note: it's currently unreachable as a TargetBackend value. internal/sandbox/manager.go only constructs a Windows backend via nativeBackend(goos, BackendWindowsRestrictedToken, ...); there's no call site that produces one named BackendWindowsElevated. buildPlatformCommandPlan's switch also only has a case for BackendWindowsRestrictedToken, so anything else falls through to nativeSandboxUnavailableError before a command plan is ever built.

Given that, narrowing windowsWrappedBackend() to restricted-token only wouldn't change any live behavior, and it would break the pattern used elsewhere (manager.go, adapters.go, platformForBackendName) of treating the two Windows backends as a matched pair. Leaving it bundled and covering it with the test added in 5d726af seemed like the better trade: no behavior change now, and it stays correct if the elevated path ever gets wired up to produce real command plans.

@kevincodex1
kevincodex1 merged commit 8bd9742 into Gitlawb:main Jul 14, 2026
7 checks passed
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.

3 participants