mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 13:31:26 -10:00
Update the Go module path, imports, install scripts, docs, workflows, packaging and action defaults from Gitlawb/zero to Twigpine/zero. The npm scope (already @twigpine/zero on main, with its @gitlawb/zero migration notes), LICENSE, action author, the OpenGateway provider and historical CHANGELOG links are unchanged. The X-AIMLAPI-Integration-Repo header keeps Gitlawb/zero: it is what AIMLAPI sees for attribution, so changing it needs a maintainer's yes. docs/INSTALL.md keeps `go install github.com/Gitlawb/zero/...`, because every tagged release so far declares that module path. The Homebrew formula comment names the Twigpine/homebrew-tap tap. Co-authored-by: Claude Opus 5.5 <[email protected]>
234 lines
8.0 KiB
Go
234 lines
8.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/Twigpine/zero/internal/sandbox"
|
|
"github.com/Twigpine/zero/internal/tools"
|
|
)
|
|
|
|
func TestProposedCommandPrefixUsesSafeSimpleCommands(t *testing.T) {
|
|
got := proposedCommandPrefix("bash", map[string]any{"command": "go test ./..."})
|
|
want := []string{"go", "test", "./..."}
|
|
if !equalStringSlices(got, want) {
|
|
t.Fatalf("prefix = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestProposedCommandPrefixSupportsExecCommand(t *testing.T) {
|
|
got := proposedCommandPrefix("exec_command", map[string]any{"cmd": "go test ./..."})
|
|
want := []string{"go", "test", "./..."}
|
|
if !equalStringSlices(got, want) {
|
|
t.Fatalf("prefix = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestProposedCommandPrefixHonorsValidatedRequestedPrefix(t *testing.T) {
|
|
got := proposedCommandPrefix("bash", map[string]any{
|
|
"command": "git status --short",
|
|
"prefix_rule": []any{"git", "status"},
|
|
})
|
|
want := []string{"git", "status"}
|
|
if !equalStringSlices(got, want) {
|
|
t.Fatalf("prefix = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestProposedCommandPrefixSupportsSegmentedCommands(t *testing.T) {
|
|
got := proposedCommandPrefix("bash", map[string]any{"command": "ps aux | head -5"})
|
|
if runtime.GOOS == "windows" {
|
|
// head is MSYS-prone on Windows (#458), so proposedCommandPrefix must
|
|
// not offer "ps aux" as a reusable prefix here: approving it would
|
|
// escalate the whole command, including the uncovered head segment,
|
|
// to bypass the sandbox unreviewed. See
|
|
// TestProposedCommandPrefixRejectsPrefixLeavingUnsafeTailUncovered for
|
|
// the platform-independent regression coverage of this behavior.
|
|
if got != nil {
|
|
t.Fatalf("expected no prefix on Windows because head is MSYS-prone, got %#v", got)
|
|
}
|
|
return
|
|
}
|
|
want := []string{"ps", "aux"}
|
|
if !equalStringSlices(got, want) {
|
|
t.Fatalf("prefix = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
// TestProposedCommandPrefixRejectsPrefixLeavingUnsafeTailUncovered guards
|
|
// against proposedCommandPrefix offering to approve one segment of a
|
|
// multi-segment command (e.g. "ps aux") while a different segment (e.g. "npm
|
|
// install") is not known-safe. shellExecutionArgsForApproval escalates the
|
|
// entire command once any prefix is approved, so an uncovered unsafe segment
|
|
// would bypass the sandbox unreviewed. Uses npm, which is never known-safe on
|
|
// any platform, so the assertion does not depend on runtime.GOOS.
|
|
func TestProposedCommandPrefixRejectsPrefixLeavingUnsafeTailUncovered(t *testing.T) {
|
|
if got := proposedCommandPrefix("bash", map[string]any{"command": "ps aux && npm install"}); got != nil {
|
|
t.Fatalf("expected no prefix because npm segment is not known-safe, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestProposedCommandPrefixHonorsRequestedPrefixAcrossSegments(t *testing.T) {
|
|
got := proposedCommandPrefix("bash", map[string]any{
|
|
"command": "git status --short && git status --branch",
|
|
"prefix_rule": []any{"git", "status"},
|
|
})
|
|
want := []string{"git", "status"}
|
|
if !equalStringSlices(got, want) {
|
|
t.Fatalf("prefix = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestProposedCommandPrefixRejectsRequestedPrefixThatDoesNotCoverSegments(t *testing.T) {
|
|
got := proposedCommandPrefix("bash", map[string]any{
|
|
"command": "ps aux && npm install",
|
|
"prefix_rule": []any{"ps", "aux"},
|
|
})
|
|
if got != nil {
|
|
t.Fatalf("partial requested prefix should be rejected, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestProposedCommandPrefixRejectsUnsafeRequestedPrefix(t *testing.T) {
|
|
got := proposedCommandPrefix("bash", map[string]any{
|
|
"command": "git status --short",
|
|
"prefix_rule": []any{"git"},
|
|
})
|
|
if got != nil {
|
|
t.Fatalf("broad requested prefix should be rejected, got %#v", got)
|
|
}
|
|
}
|
|
|
|
// The launcher denylist is only worth anything if it holds on the path a user
|
|
// actually sees. A model naming its own prefix_rule is that path: before
|
|
// launcher names were normalized, "python3.11" was offered as a one-token
|
|
// grant, and hasStringPrefix then matched every later python3.11 invocation.
|
|
func TestProposedCommandPrefixRejectsLauncherSpellingsInRequestedPrefix(t *testing.T) {
|
|
for _, testCase := range []struct {
|
|
command string
|
|
rule []any
|
|
}{
|
|
{"python3.11 script.py", []any{"python3.11"}},
|
|
{"python.exe script.py", []any{"python.exe"}},
|
|
{"node.exe app.js", []any{"node.exe"}},
|
|
{"cmd /c whoami", []any{"cmd"}},
|
|
{"busybox sh -c id", []any{"busybox"}},
|
|
{"uv run main.py", []any{"uv", "run"}},
|
|
{"python3.11 -c import_os", []any{"python3.11", "-c"}},
|
|
} {
|
|
got := proposedCommandPrefix("bash", map[string]any{
|
|
"command": testCase.command,
|
|
"prefix_rule": testCase.rule,
|
|
})
|
|
if got != nil {
|
|
t.Errorf("prefix_rule %v for %q was offered as %#v, want rejected", testCase.rule, testCase.command, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProposedCommandPrefixStillOffersOrdinaryCommands(t *testing.T) {
|
|
got := proposedCommandPrefix("bash", map[string]any{
|
|
"command": "cargo build --release",
|
|
"prefix_rule": []any{"cargo", "build"},
|
|
})
|
|
if len(got) != 2 || got[0] != "cargo" || got[1] != "build" {
|
|
t.Fatalf("ordinary prefix rule = %#v, want [cargo build]", got)
|
|
}
|
|
}
|
|
|
|
func TestProposedCommandPrefixRejectsUnsafeShellForms(t *testing.T) {
|
|
cases := []string{
|
|
"cat < in > out",
|
|
"FOO=bar go test",
|
|
"echo $(whoami)",
|
|
"cat *.go",
|
|
"bash -lc go test",
|
|
}
|
|
for _, command := range cases {
|
|
t.Run(command, func(t *testing.T) {
|
|
if got := proposedCommandPrefix("bash", map[string]any{"command": command}); got != nil {
|
|
t.Fatalf("unsafe command got prefix %#v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProposedCommandPrefixRejectsUnsafeLaunchers(t *testing.T) {
|
|
cases := []string{
|
|
"find . -type f",
|
|
"xargs rm -rf /tmp/x",
|
|
"timeout 5 go test ./...",
|
|
"nice go test ./...",
|
|
"nohup go test ./...",
|
|
"watch ls",
|
|
"ssh host ls",
|
|
"make test",
|
|
"npm run dev",
|
|
"command git status",
|
|
"eval echo hi",
|
|
"exec echo hi",
|
|
"python script.py",
|
|
"node script.js",
|
|
"./script.sh --flag",
|
|
"/tmp/script.sh --flag",
|
|
}
|
|
for _, command := range cases {
|
|
t.Run(command, func(t *testing.T) {
|
|
if got := proposedCommandPrefix("bash", map[string]any{"command": command}); got != nil {
|
|
t.Fatalf("unsafe launcher got prefix %#v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMatchCommandPrefixCoversSegmentedCommandWithSafeTail(t *testing.T) {
|
|
engine := sandbox.NewEngine(sandbox.EngineOptions{WorkspaceRoot: t.TempDir()})
|
|
engine.GrantCommandPrefixForSession("bash", []string{"ps", "aux"})
|
|
// head is MSYS-prone on Windows (#458) and must not count as a known-safe tail.
|
|
command := "ps aux | head -5"
|
|
if runtime.GOOS == "windows" {
|
|
command = "ps aux | echo ok"
|
|
}
|
|
|
|
grant, ok, session := matchCommandPrefix("bash", map[string]any{"command": command}, Options{Sandbox: engine})
|
|
if !ok || !session || !equalStringSlices(grant.Prefix, []string{"ps", "aux"}) {
|
|
t.Fatalf("match = %#v ok=%v session=%v, want session ps aux prefix", grant, ok, session)
|
|
}
|
|
}
|
|
|
|
func TestKnownSafeCommandSegmentRejectsMsysProneOnWindows(t *testing.T) {
|
|
if runtime.GOOS != "windows" {
|
|
t.Skip("windows-only known-safe MSYS guard")
|
|
}
|
|
for _, command := range [][]string{{"head", "-5"}, {"cat", "file.txt"}, {"grep", "pat"}} {
|
|
if knownSafeCommandSegment(command) {
|
|
t.Fatalf("expected %q to be unsafe on Windows, got known-safe", command)
|
|
}
|
|
}
|
|
if !knownSafeCommandSegment([]string{"echo", "ok"}) {
|
|
t.Fatal("expected echo to remain known-safe on Windows")
|
|
}
|
|
if !tools.MsysProneCommandName("head") {
|
|
t.Fatal("expected head to be MSYS-prone")
|
|
}
|
|
}
|
|
|
|
func TestMatchCommandPrefixRejectsUncoveredSegment(t *testing.T) {
|
|
engine := sandbox.NewEngine(sandbox.EngineOptions{WorkspaceRoot: t.TempDir()})
|
|
engine.GrantCommandPrefixForSession("bash", []string{"ps", "aux"})
|
|
|
|
if grant, ok, session := matchCommandPrefix("bash", map[string]any{"command": "ps aux && npm install"}, Options{Sandbox: engine}); ok {
|
|
t.Fatalf("match = %#v session=%v, want no match because npm segment is uncovered", grant, session)
|
|
}
|
|
}
|
|
|
|
func TestProposedCommandPrefixRejectsRequestedUnsafeLauncherPrefix(t *testing.T) {
|
|
got := proposedCommandPrefix("bash", map[string]any{
|
|
"command": "find . -type f",
|
|
"prefix_rule": []any{"find", "."},
|
|
})
|
|
if got != nil {
|
|
t.Fatalf("unsafe requested launcher prefix should be rejected, got %#v", got)
|
|
}
|
|
}
|