Skip to content

Commit 233fd51

Browse files
fix(cli): stop exec when the degraded-sandbox notice cannot be written
The notice goes out before any tool runs, and before MCP servers and plugins start through the same sandbox, but a failed write to stderr was ignored, so the run went ahead with reduced isolation and no warning delivered. It now stops with exitCrash, the way the image and reasoning-effort notices in the same function already do.
1 parent 24de9d1 commit 233fd51

2 files changed

Lines changed: 56 additions & 3 deletions

File tree

‎internal/cli/exec.go‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,10 +347,16 @@ func runExec(args []string, stdout io.Writer, stderr io.Writer, deps appDeps) in
347347
} else if notice != "" {
348348
_, _ = fmt.Fprintln(stderr, "[zero] "+notice)
349349
}
350-
// Before any tool runs, so a run that goes ahead with reduced isolation says
351-
// so where its output starts, not only when someone runs `zero doctor`.
350+
// Before any tool runs, and before the MCP servers and plugins below start
351+
// through the same sandbox, so a run that goes ahead with reduced isolation
352+
// says so where its output starts, not only when someone runs `zero doctor`.
353+
// A notice that cannot be written stops the run, as the image and effort
354+
// notices further down do: going ahead would mean reduced isolation and no
355+
// warning delivered.
352356
if notice := sandboxEngine.DegradedNotice(); notice != "" {
353-
_, _ = fmt.Fprintln(stderr, "[zero] "+notice)
357+
if _, err := fmt.Fprintln(stderr, "[zero] "+notice); err != nil {
358+
return exitCrash
359+
}
354360
}
355361
executionRunner.SetPreparer(sandboxEngine)
356362
if permissionMode != agent.PermissionModePlan {

‎internal/cli/sandbox_degraded_notice_test.go‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,53 @@ func TestExecSaysNothingWhenTheSandboxIsTurnedOff(t *testing.T) {
125125
}
126126
}
127127

128+
// countingExecProvider is echoExecProvider, counting the completions it streams.
129+
type countingExecProvider struct {
130+
echoExecProvider
131+
streamed *int
132+
}
133+
134+
func (provider countingExecProvider) StreamCompletion(ctx context.Context, request zeroruntime.CompletionRequest) (<-chan zeroruntime.StreamEvent, error) {
135+
*provider.streamed++
136+
return provider.echoExecProvider.StreamCompletion(ctx, request)
137+
}
138+
139+
// A degraded notice that cannot be written stops the run before the model is
140+
// asked for anything, as the image and effort notices do, instead of going ahead
141+
// with reduced isolation and no warning delivered.
142+
func TestExecStopsWhenTheDegradedNoticeCannotBeWritten(t *testing.T) {
143+
clearSandboxNestingMarkers(t)
144+
isolateCLIUserState(t)
145+
var stdout bytes.Buffer
146+
cwd := t.TempDir()
147+
streamed := 0
148+
exitCode := runWithDeps([]string{"exec", "hello"}, &stdout, failingWriter{}, appDeps{
149+
getwd: func() (string, error) { return cwd, nil },
150+
resolveConfig: func(string, config.Overrides) (config.ResolvedConfig, error) {
151+
return config.ResolvedConfig{
152+
ActiveProvider: "echo",
153+
Provider: config.ProviderProfile{
154+
Name: "echo",
155+
ProviderKind: config.ProviderKindOpenAICompatible,
156+
BaseURL: "http://127.0.0.1/v1",
157+
Model: "echo-model",
158+
},
159+
MaxTurns: 3,
160+
}, nil
161+
},
162+
newProvider: func(config.ProviderProfile) (zeroruntime.Provider, error) {
163+
return countingExecProvider{streamed: &streamed}, nil
164+
},
165+
selectSandboxBackend: unavailableTestSandbox,
166+
})
167+
if exitCode != exitCrash {
168+
t.Fatalf("exit code = %d, want %d when the degraded notice cannot be written", exitCode, exitCrash)
169+
}
170+
if streamed != 0 {
171+
t.Fatalf("the model was asked %d times: the run went ahead without its warning", streamed)
172+
}
173+
}
174+
128175
// launchTUIWithSandbox runs the root command the way a bare `zero` does, with
129176
// the sandbox backend pinned to the unavailable one, and returns the options the
130177
// TUI would have started with.

0 commit comments

Comments
 (0)