mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 18:00:54 -10:00
* fix(agent): reject a malformed additional_permissions payload before prompting A shell tool call that sets sandbox_permissions: with_additional_permissions but omits (or malforms) the additional_permissions payload was accepted into the normal permission-prompt flow. buildPermissionEvent silently swallowed the validation error while rendering the prompt's scope text, so the user saw an ordinary-looking request, approved it, and only then hit a hard denial from the same validation running again at grant time (loop.go's grantInlineAdditionalPermissions), with the confusing message "failed to grant session additional permissions: missing additional_permissions". A malformed payload can never succeed regardless of the user's decision, so run the same check (inlineAdditionalPermissionsProfile, already the single source of truth for this shape) up front, before any prompt, and return a plain tool error the driving model can see and retry from. This also covers the mismatched case (additional_permissions present without the flag). Co-Authored-By: Claude Fable 5 <[email protected]> * fix(agent): make the additional_permissions rejection message actionable The rejection added in the previous commit correctly stops a malformed sandbox_permissions: with_additional_permissions call before any prompt, but its message ("missing additional_permissions; provide at least one of network or file_system") gave a model nothing concrete to correct itself with. Observed in practice: a weaker tool-calling model (xai/grok-build-0.1) kept re-attaching sandbox_permissions out of habit from an earlier, correct gh/git call onto later commands that didn't need it (network was already open via a session-scoped grant), hit this same rejection 4 times in a row with no change in its retry, and gave up. Both messages (missing payload, and empty-but-present payload) now include a concrete, schema-accurate example - {"network": {"enabled": true}} or {"file_system": {"write": ["/path"]}} - and the missing-payload message adds the other half of the fix: if the command doesn't actually need elevated permissions, omit sandbox_permissions entirely and retry. Co-Authored-By: Claude Fable 5 <[email protected]> --------- Co-authored-by: Claude Fable 5 <[email protected]>
147 lines
4.6 KiB
Go
147 lines
4.6 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Gitlawb/zero/internal/tools"
|
|
)
|
|
|
|
// A shell call that sets sandbox_permissions: with_additional_permissions but
|
|
// omits (or malforms) additional_permissions can never succeed: the same
|
|
// validation runs again at grant time regardless of the user's decision. It
|
|
// must be rejected as a plain tool error BEFORE any permission prompt, not
|
|
// surfaced as a confusing "approved but denied" prompt outcome.
|
|
func TestExecuteToolCallRejectsMissingAdditionalPermissionsBeforePrompt(t *testing.T) {
|
|
root := t.TempDir()
|
|
registry := tools.NewRegistry()
|
|
registry.Register(tools.NewBashTool(root))
|
|
|
|
promptCalls := 0
|
|
result, err := executeToolCall(
|
|
context.Background(),
|
|
registry,
|
|
ToolCall{
|
|
ID: "c1",
|
|
Name: "bash",
|
|
Arguments: `{"command":"echo hi","sandbox_permissions":"with_additional_permissions"}`,
|
|
},
|
|
PermissionModeAuto,
|
|
Options{
|
|
OnPermissionRequest: func(ctx context.Context, request PermissionRequest) (PermissionDecision, error) {
|
|
promptCalls++
|
|
return PermissionDecision{Action: PermissionDecisionAllow}, nil
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("executeToolCall: %v", err)
|
|
}
|
|
if result.Status != tools.StatusError {
|
|
t.Fatalf("status = %q, want error", result.Status)
|
|
}
|
|
if promptCalls != 0 {
|
|
t.Fatalf("OnPermissionRequest called %d times, want 0 (must reject before prompting)", promptCalls)
|
|
}
|
|
}
|
|
|
|
// The mismatched-flag case (additional_permissions present without the
|
|
// sandbox_permissions flag) must be rejected the same way.
|
|
func TestExecuteToolCallRejectsAdditionalPermissionsWithoutFlagBeforePrompt(t *testing.T) {
|
|
root := t.TempDir()
|
|
registry := tools.NewRegistry()
|
|
registry.Register(tools.NewBashTool(root))
|
|
|
|
promptCalls := 0
|
|
result, err := executeToolCall(
|
|
context.Background(),
|
|
registry,
|
|
ToolCall{
|
|
ID: "c1",
|
|
Name: "bash",
|
|
Arguments: `{"command":"echo hi","additional_permissions":{"network":{"enabled":true}}}`,
|
|
},
|
|
PermissionModeAuto,
|
|
Options{
|
|
OnPermissionRequest: func(ctx context.Context, request PermissionRequest) (PermissionDecision, error) {
|
|
promptCalls++
|
|
return PermissionDecision{Action: PermissionDecisionAllow}, nil
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("executeToolCall: %v", err)
|
|
}
|
|
if result.Status != tools.StatusError {
|
|
t.Fatalf("status = %q, want error", result.Status)
|
|
}
|
|
if promptCalls != 0 {
|
|
t.Fatalf("OnPermissionRequest called %d times, want 0 (must reject before prompting)", promptCalls)
|
|
}
|
|
}
|
|
|
|
// The rejection message must show a concrete, correctly-shaped example: a
|
|
// model that gets this wrong once (attaching sandbox_permissions out of habit
|
|
// from an earlier call, without a valid payload) needs enough in the error to
|
|
// self-correct on retry instead of repeating the identical mistake.
|
|
func TestExecuteToolCallMissingAdditionalPermissionsErrorIsActionable(t *testing.T) {
|
|
root := t.TempDir()
|
|
registry := tools.NewRegistry()
|
|
registry.Register(tools.NewBashTool(root))
|
|
|
|
result, err := executeToolCall(
|
|
context.Background(),
|
|
registry,
|
|
ToolCall{
|
|
ID: "c1",
|
|
Name: "bash",
|
|
Arguments: `{"command":"ping example.com","sandbox_permissions":"with_additional_permissions"}`,
|
|
},
|
|
PermissionModeAuto,
|
|
Options{},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("executeToolCall: %v", err)
|
|
}
|
|
if !strings.Contains(result.Output, `{"network": {"enabled": true}}`) {
|
|
t.Fatalf("output = %q, want a concrete additional_permissions example", result.Output)
|
|
}
|
|
if !strings.Contains(result.Output, "omit sandbox_permissions") {
|
|
t.Fatalf("output = %q, want guidance that the flag can simply be omitted", result.Output)
|
|
}
|
|
}
|
|
|
|
// A well-formed additional_permissions payload must still reach the normal
|
|
// permission prompt: this fix only rejects malformed payloads.
|
|
func TestExecuteToolCallStillPromptsForValidAdditionalPermissions(t *testing.T) {
|
|
root := t.TempDir()
|
|
registry := tools.NewRegistry()
|
|
registry.Register(tools.NewBashTool(root))
|
|
|
|
promptCalls := 0
|
|
_, err := executeToolCall(
|
|
context.Background(),
|
|
registry,
|
|
ToolCall{
|
|
ID: "c1",
|
|
Name: "bash",
|
|
Arguments: `{"command":"echo hi","sandbox_permissions":"with_additional_permissions",` +
|
|
`"additional_permissions":{"network":{"enabled":true}}}`,
|
|
},
|
|
PermissionModeAuto,
|
|
Options{
|
|
OnPermissionRequest: func(ctx context.Context, request PermissionRequest) (PermissionDecision, error) {
|
|
promptCalls++
|
|
return PermissionDecision{Action: PermissionDecisionDeny}, nil
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("executeToolCall: %v", err)
|
|
}
|
|
if promptCalls != 1 {
|
|
t.Fatalf("OnPermissionRequest called %d times, want exactly 1 for a valid elevation request", promptCalls)
|
|
}
|
|
}
|