mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
* fix(sandbox): grant ancestor metadata so cd into the workspace works macOS seatbelt granted file-read* on each read root via (subpath "<root>"), which covers the root and its descendants but NOT its parent directories. An absolute cd (and any path the kernel canonicalises) must stat each ancestor to traverse to a deeply-nested workspace like /Users/me/Downloads/app — those parents weren't granted, so seatbelt denied the chdir and surfaced it as the misleading 'cd: <path>: Not a directory' (ENOTDIR). This blocked the agent from cd-ing into its own workspace (e.g. 'cd /abs/workspace && go test'). Fix: grant file-read-metadata + file-test-existence on the path-ancestors of each read root (metadata only — ancestor contents stay unreadable). Same pattern already used for /opt/homebrew, /usr/local, /System/Volumes/Data/private. Verified end-to-end: 'cd /abs/workspace && ...' now succeeds under the real generated profile. go build/vet/test green. * fix(sandbox): skip path-ancestors for filesystem roots (invalid SBPL) Follow-up to the ancestor-metadata traversal fix. A read root of "/" has no ancestors, and (path-ancestors "/") is invalid SBPL — sandbox-exec aborts with exit 65 and the sandboxed command can't launch at all. This was reachable via the compat builder (ReadRoots=["/"] flipped to restricted under EnforceWorkspace) and any degenerate "/" workspace. seatbeltAncestorMetadataRule now skips any root that is its own parent (filepath.Dir(clean)==clean), neutralizing "/" and volume roots regardless of entry point. Verified: the compat profile now compiles under sandbox-exec (exit 0). Found by adversarial verification of the prior commit. * fix(sandbox): grant the CLT toolchain so git/clang/make work On macOS, /usr/bin/git (and clang, make, etc.) are thin stubs that resolve the real binary under the active developer dir — usually /Library/Developer/CommandLineTools. The sandbox's platform read roots didn't include it, so the stub couldn't reach the real tool and failed with the misleading 'xcode-select: No developer tools were found' error. Add /Library/Developer (read-only) to the platform read roots. * fix(sandbox): allow reading the user's global git config git reads identity/config from the user's global git config, but the sandbox confines reads away from HOME, so it failed with 'unable to access ~/.gitconfig: Operation not permitted'. Grant read-only on the git config FILES specifically (~/.gitconfig and ~/.config/git/config) — not the ~/.config/git directory, which can hold an XDG credential store — so git runs while credentials and the rest of HOME stay unreadable. The path-ancestors traversal grant covers reaching them. * fix(sandbox): let the agent terminate user-owned processes on request The seatbelt profile only allowed signaling the agent's own process group ((target self) (target pgrp)), so a process started by a PREVIOUS session — e.g. a dev server still listening on a port — was in a different group and could not be killed. The agent would thrash through kill/pkill/osascript/sudo, all denied, and never stop the process the user asked it to terminate. Relax to (allow signal). The kernel still enforces UID ownership: a sandboxed command runs as the user, so it can only signal the user's own processes, never root's or another user's. Verified end-to-end: a sandboxed command now kills a different-group user-owned process under the real generated profile. * fix(sandbox): let the agent find processes + guide it to working tools Pairs with the signal fix (8b5018d). Two more gaps blocked process management: - process-info was scoped to (target same-sandbox), so the agent couldn't even inspect processes outside its own sandbox. Relaxed to (allow process-info*). - `ps` is setuid root and macOS sandbox-exec refuses to exec setuid binaries (privilege-escalation guard) — unfixable via profile. `pgrep` needs a blocked system service. So the agent kept thrashing on ps/pgrep/os.kill. Add macOS shell guidance pointing the model at the tools that DO work: lsof to find a process, kill to stop it. With the signal grant, lsof+kill now manages processes reliably (verified end-to-end). * fix(sandbox): grant git config at the seatbelt rule, not the shared profile The git-config read paths (~/.gitconfig, ~/.config/git/config) were added to the cross-platform permissionProfileReadRoots, so they leaked HOME-dependent absolute paths into the platform-agnostic 'sandbox policy --json' snapshot and broke the golden test on every OS (machine-specific paths + Windows separators). Move the grant to seatbeltReadRule (macOS only, where reads are actually confined): the PermissionProfile stays platform-agnostic so the golden is stable, and the seatbelt profile still grants the config files + their ancestor metadata for traversal. Verified: golden test green, full 'go test ./...' green, and git still reads ~/.gitconfig under the real generated profile. * fix(sandbox): grant ancestor metadata for platform read roots too Addresses CodeRabbit review on #302. seatbeltAncestorMetadataRule was passed only fs.ReadRoots + gitConfig, on the assumption that platform read roots are top-level or already covered. /Library/Developer (the CLT toolchain) breaks that: it needs /Library stat-able as an ancestor, so a chdir-style traversal into it (cd /Library/Developer/CommandLineTools) failed with ENOTDIR even though reads succeeded (exec doesn't require ancestor-stat the way chdir does). Include the platform read roots in ancestorRoots when IncludePlatformRoots is set. Verified: cd into the CLT toolchain now succeeds under the real profile; golden + sandbox tests green. Regression test added.
90 lines
2.9 KiB
Go
90 lines
2.9 KiB
Go
package tools
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
type shellRuntime struct {
|
|
GOOS string
|
|
Executable string
|
|
Syntax string
|
|
}
|
|
|
|
type shellIssue struct {
|
|
Kind string
|
|
Message string
|
|
Suggestion string
|
|
}
|
|
|
|
var (
|
|
windowsBashStyleCDPattern = regexp.MustCompile(`(?i)(^|[&|;]\s*)cd\s+/(?:[a-ce-z0-9_./~-]|d[a-z0-9_./~-])[a-z0-9_./~-]*`)
|
|
windowsLSCommandPattern = regexp.MustCompile(`(?i)(^|[&|;]\s*)ls\b(?:\s+|$)`)
|
|
)
|
|
|
|
func detectShellRuntime(goos string) shellRuntime {
|
|
if goos == "windows" {
|
|
return shellRuntime{GOOS: goos, Executable: "cmd.exe", Syntax: "Windows cmd.exe"}
|
|
}
|
|
return shellRuntime{GOOS: goos, Executable: "/bin/sh", Syntax: "/bin/sh"}
|
|
}
|
|
|
|
func shellGuidanceForGOOS(goos string) string {
|
|
runtime := detectShellRuntime(goos)
|
|
if goos == "windows" {
|
|
return "Uses " + runtime.Syntax + " syntax on Windows; prefer cwd over cd when changing directories."
|
|
}
|
|
guidance := "Uses " + runtime.Syntax + " syntax."
|
|
if goos == "darwin" {
|
|
// `ps` is setuid root and cannot run under the macOS sandbox; `pgrep` needs a
|
|
// blocked system service. Point the model at the tools that DO work so it
|
|
// doesn't waste turns: lsof to find a process, kill to stop it.
|
|
guidance += " To find or stop a process, use `lsof -i :PORT` (or `lsof -nP -iTCP -sTCP:LISTEN`) for the PID then `kill <pid>`; `ps` and `pgrep` do not work under the sandbox."
|
|
}
|
|
return guidance
|
|
}
|
|
|
|
func detectShellCommandIssue(command string, goos string) *shellIssue {
|
|
if goos != "windows" {
|
|
return nil
|
|
}
|
|
trimmed := strings.TrimSpace(command)
|
|
if windowsBashStyleCDPattern.MatchString(trimmed) ||
|
|
windowsLSCommandPattern.MatchString(trimmed) {
|
|
return &shellIssue{
|
|
Kind: "windows_shell_syntax",
|
|
Message: "Command looks like POSIX/Bash syntax, but Zero runs bash tool commands through Windows cmd.exe on this host.",
|
|
Suggestion: "Use the cwd argument instead of cd, use Windows cmd.exe syntax, or use native tools such as list_directory, read_file, grep, and glob.",
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func detectShellOutputIssue(command string, output string, goos string) *shellIssue {
|
|
if goos != "windows" {
|
|
return nil
|
|
}
|
|
haystack := strings.ToLower(command + "\n" + output)
|
|
if strings.Contains(haystack, "the syntax of the command is incorrect") ||
|
|
strings.Contains(haystack, "is not recognized as an internal or external command") {
|
|
return &shellIssue{
|
|
Kind: "windows_shell_syntax",
|
|
Message: "Windows cmd.exe rejected the command syntax.",
|
|
Suggestion: "Translate the command to Windows cmd.exe syntax, set the bash tool cwd argument instead of running cd, or prefer native Zero tools for file inspection.",
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func appendShellIssueHint(output string, issue shellIssue) string {
|
|
output = strings.TrimRight(output, "\r\n")
|
|
hint := "[zero] shell issue: " + issue.Message
|
|
if strings.TrimSpace(issue.Suggestion) != "" {
|
|
hint += "\nSuggestion: " + issue.Suggestion
|
|
}
|
|
if output == "" {
|
|
return hint
|
|
}
|
|
return output + "\n" + hint
|
|
}
|