mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 02:16:56 +00:00
feat(sandbox): hint LLM when tool/binary missing in sandbox container
Detect exit code 127 and "command not found" patterns in sandbox execution paths and append a [SANDBOX] hint to the error output, so the model can inform the user about sandbox environment limitations. Only applies to sandbox execution — host paths are untouched.
This commit is contained in:
1 parent
711395f81f
commit
b80ee0ae43
4 files changed
+103
-2
No files matched your search
@@ -112,7 +112,11 @@ func (t *ExecTool) executeCredentialed(ctx context.Context, cred *store.SecureCL
|
||||
// Step 2: Resolve binary to absolute path and verify against config
|
||||
absPath, err := resolveAndMatchBinary(binary, cred.BinaryPath)
|
||||
if err != nil {
|
||||
return credentialedPathError(binary, err)
|
||||
r := credentialedPathError(binary, err)
|
||||
if t.sandboxMgr != nil && sandboxKey != "" {
|
||||
r.ForLLM += sandboxHint
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Step 3: Per-binary deny check (deny_args)
|
||||
@@ -199,7 +203,8 @@ func (t *ExecTool) executeCredentialedSandbox(ctx context.Context, absPath strin
|
||||
output += "STDERR:\n" + result.Stderr
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return credentialedExecFailError(absPath, args, result.ExitCode, ScrubCredentials(output))
|
||||
scrubbed := ScrubCredentials(output)
|
||||
return credentialedExecFailError(absPath, args, result.ExitCode, scrubbed+MaybeSandboxHint(result.ExitCode, scrubbed))
|
||||
}
|
||||
if output == "" {
|
||||
output = "(command completed with no output)"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package tools
|
||||
|
||||
import "strings"
|
||||
|
||||
// sandboxHint is appended to tool error output when a command fails inside
|
||||
// a Docker sandbox container due to a missing binary/tool (exit code 127).
|
||||
const sandboxHint = "\n\n[SANDBOX] This command ran inside a Docker sandbox container. " +
|
||||
"The required tool/binary is not installed in the sandbox image. " +
|
||||
"Tell the user this failed due to sandbox environment limitations — " +
|
||||
"they can install the binary in the sandbox image or disable sandbox mode for this agent."
|
||||
|
||||
// isBinaryNotFound returns true if the error output and exit code indicate
|
||||
// a missing binary. Exit code 127 is the POSIX convention for "command not found".
|
||||
func isBinaryNotFound(exitCode int, output string) bool {
|
||||
if exitCode == 127 {
|
||||
return true
|
||||
}
|
||||
// Fallback: match stderr patterns from non-standard shells.
|
||||
lower := strings.ToLower(output)
|
||||
return strings.Contains(lower, "not found") &&
|
||||
(strings.Contains(lower, "command") || strings.Contains(lower, "sh:"))
|
||||
}
|
||||
|
||||
// MaybeSandboxHint returns the sandbox hint suffix if the error looks like
|
||||
// a missing binary. Returns empty string otherwise.
|
||||
// Only call this from sandbox execution paths — never from host execution.
|
||||
func MaybeSandboxHint(exitCode int, output string) string {
|
||||
if isBinaryNotFound(exitCode, output) {
|
||||
return sandboxHint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsBinaryNotFound(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
exitCode int
|
||||
output string
|
||||
want bool
|
||||
}{
|
||||
{"exit 127 empty output", 127, "", true},
|
||||
{"exit 127 with sh not found", 127, "sh: git: not found", true},
|
||||
{"exit 127 with bash error", 127, "bash: python3: command not found", true},
|
||||
{"exit 1 permission denied", 1, "permission denied", false},
|
||||
{"exit 0 success", 0, "ok", false},
|
||||
{"exit 1 sh not found pattern", 1, "sh: node: not found", true},
|
||||
{"exit 1 command not found pattern", 1, "bash: command not found: curl", true},
|
||||
{"exit 1 unrelated not found", 1, "file not found: config.yaml", false},
|
||||
{"exit 2 no such file", 2, "No such file or directory", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isBinaryNotFound(tt.exitCode, tt.output)
|
||||
if got != tt.want {
|
||||
t.Errorf("isBinaryNotFound(%d, %q) = %v, want %v", tt.exitCode, tt.output, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaybeSandboxHint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
exitCode int
|
||||
output string
|
||||
wantHint bool
|
||||
}{
|
||||
{"binary not found returns hint", 127, "sh: git: not found", true},
|
||||
{"success returns empty", 0, "ok", false},
|
||||
{"non-binary error returns empty", 1, "segfault", false},
|
||||
{"sh pattern returns hint", 1, "sh: python3: not found", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := MaybeSandboxHint(tt.exitCode, tt.output)
|
||||
if tt.wantHint {
|
||||
if !strings.Contains(got, "[SANDBOX]") {
|
||||
t.Errorf("MaybeSandboxHint(%d, %q) = %q, want hint containing [SANDBOX]", tt.exitCode, tt.output, got)
|
||||
}
|
||||
} else {
|
||||
if got != "" {
|
||||
t.Errorf("MaybeSandboxHint(%d, %q) = %q, want empty", tt.exitCode, tt.output, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -322,6 +322,7 @@ func (t *ExecTool) executeInSandbox(ctx context.Context, command, cwd, sandboxKe
|
||||
if output == "" {
|
||||
output = fmt.Sprintf("command exited with code %d", result.ExitCode)
|
||||
}
|
||||
output += MaybeSandboxHint(result.ExitCode, output)
|
||||
return ErrorResult(output)
|
||||
}
|
||||
if output == "" {
|
||||
|
||||
Reference in new issue
Block a user