mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 05:01:28 -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]>
195 lines
6.9 KiB
Go
195 lines
6.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Twigpine/zero/internal/config"
|
|
"github.com/Twigpine/zero/internal/sandbox"
|
|
"github.com/Twigpine/zero/internal/tools"
|
|
"github.com/Twigpine/zero/internal/zeroruntime"
|
|
)
|
|
|
|
// TestRunAddDirDispatchForwardsGrantIntoExecScope pins the dispatch seam
|
|
// end-to-end: a leading --add-dir on the ROOT argv (both the "exec" and the
|
|
// "-p" dispatch shapes in runWithDeps) must reach exec's sandbox scope so a
|
|
// write_file call inside the extra root succeeds. The negative control runs
|
|
// the identical provider WITHOUT --add-dir and must be denied (fail-closed).
|
|
// If addDirFlagArgs forwarding were dropped from either dispatch case, the
|
|
// positive runs here would fail while every parser/registry unit test stayed
|
|
// green.
|
|
func TestRunAddDirDispatchForwardsGrantIntoExecScope(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
args func(extra string) []string
|
|
}{
|
|
{
|
|
name: "exec subcommand",
|
|
args: func(extra string) []string {
|
|
return []string{"--add-dir", extra, "exec", "--skip-permissions-unsafe", "write the note"}
|
|
},
|
|
},
|
|
{
|
|
name: "prompt flag",
|
|
args: func(extra string) []string {
|
|
return []string{"--add-dir", extra, "-p", "write the note", "--skip-permissions-unsafe"}
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Setenv("XDG_DATA_HOME", t.TempDir())
|
|
cwd := t.TempDir()
|
|
extra := tempDirOutsideDefaultTemp(t)
|
|
target := filepath.Join(extra, "granted.txt")
|
|
|
|
exitCode, stderr := runExecAddDirWriteProbe(t, cwd, tc.args(extra), target)
|
|
if exitCode != exitSuccess {
|
|
t.Fatalf("exit = %d, want %d; stderr = %q", exitCode, exitSuccess, stderr)
|
|
}
|
|
content, err := os.ReadFile(target)
|
|
if err != nil {
|
|
t.Fatalf("expected write inside --add-dir root to land, read %s: %v", target, err)
|
|
}
|
|
if string(content) != "granted" {
|
|
t.Fatalf("content = %q, want %q", string(content), "granted")
|
|
}
|
|
|
|
// Negative control: the SAME run without --add-dir must be denied —
|
|
// the grant is the only thing that widens the scope.
|
|
denied := filepath.Join(extra, "denied.txt")
|
|
deniedArgs := tc.args(extra)[2:]
|
|
deniedExit, deniedStderr := runExecAddDirWriteProbe(t, cwd, deniedArgs, denied)
|
|
if deniedExit != exitSuccess {
|
|
t.Fatalf("denied-run exit = %d, want %d (tool denial is not fatal); stderr = %q", deniedExit, exitSuccess, deniedStderr)
|
|
}
|
|
if _, err := os.Stat(denied); !os.IsNotExist(err) {
|
|
t.Fatalf("write outside the workspace without --add-dir must not land, stat err=%v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// runExecAddDirWriteProbe dispatches the given root argv through runWithDeps
|
|
// with a provider that calls write_file on target, then answers. It returns
|
|
// the exit code and stderr.
|
|
func runExecAddDirWriteProbe(t *testing.T, cwd string, args []string, target string) (int, string) {
|
|
t.Helper()
|
|
|
|
arguments, err := json.Marshal(map[string]string{"path": target, "content": "granted"})
|
|
if err != nil {
|
|
t.Fatalf("marshal write_file arguments: %v", err)
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
exitCode := runWithDeps(args, &stdout, &stderr, appDeps{
|
|
getwd: func() (string, error) {
|
|
return cwd, nil
|
|
},
|
|
resolveConfig: func(_ string, _ config.Overrides) (config.ResolvedConfig, error) {
|
|
return execResolvedConfig(), nil
|
|
},
|
|
newProvider: func(config.ProviderProfile) (zeroruntime.Provider, error) {
|
|
return toolCallingExecProvider{
|
|
toolCallID: "call_write_extra",
|
|
toolName: "write_file",
|
|
arguments: string(arguments),
|
|
answer: "done",
|
|
}, nil
|
|
},
|
|
newSandboxStore: func() (*sandbox.GrantStore, error) {
|
|
return sandbox.NewGrantStore(sandbox.StoreOptions{FilePath: filepath.Join(t.TempDir(), "sandbox-grants.json")})
|
|
},
|
|
})
|
|
return exitCode, stderr.String()
|
|
}
|
|
|
|
// TestExecScopeReRegistrationSwapsCoreToolsByName pins the mechanism runExec
|
|
// relies on: a nil-scope core registry is built first (so --list-tools and
|
|
// tool-filter validation work before config resolve), then once the run scope
|
|
// is known the scoped core tools are re-registered and Registry.Register
|
|
// replaces the earlier instances BY NAME. The before/after write_file probes
|
|
// prove both the overwrite and the scoped enforcement it ships.
|
|
func TestExecScopeReRegistrationSwapsCoreToolsByName(t *testing.T) {
|
|
root := t.TempDir()
|
|
extra := tempDirOutsideDefaultTemp(t)
|
|
inside := filepath.Join(extra, "inside.txt")
|
|
|
|
registry := newCoreRegistry(root)
|
|
|
|
// Before re-registration the tools carry a nil scope, so an absolute path
|
|
// inside the extra root (but outside the workspace) must be denied.
|
|
denied := registry.RunWithOptions(context.Background(), "write_file", map[string]any{
|
|
"path": inside,
|
|
"content": "too early",
|
|
}, tools.RunOptions{PermissionGranted: true})
|
|
if denied.Status == tools.StatusOK {
|
|
t.Fatalf("nil-scope registry must deny extra-root write, got ok: %s", denied.Output)
|
|
}
|
|
if _, err := os.Stat(inside); !os.IsNotExist(err) {
|
|
t.Fatalf("expected inside file to remain absent before re-registration, stat err=%v", err)
|
|
}
|
|
|
|
// Re-register exactly like exec.go does once the run scope is resolved.
|
|
scope, err := sandbox.NewScope(root, []string{extra})
|
|
if err != nil {
|
|
t.Fatalf("NewScope: %v", err)
|
|
}
|
|
for _, tool := range tools.CoreToolsScoped(root, scope) {
|
|
registry.Register(tool)
|
|
}
|
|
|
|
allowed := registry.RunWithOptions(context.Background(), "write_file", map[string]any{
|
|
"path": inside,
|
|
"content": "granted",
|
|
}, tools.RunOptions{PermissionGranted: true})
|
|
if allowed.Status != tools.StatusOK {
|
|
t.Fatalf("scoped registry must allow extra-root write, got %s: %s", allowed.Status, allowed.Output)
|
|
}
|
|
content, err := os.ReadFile(inside)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", inside, err)
|
|
}
|
|
if string(content) != "granted" {
|
|
t.Fatalf("inside content = %q, want %q", string(content), "granted")
|
|
}
|
|
|
|
// A path outside every scope root must still be denied after re-registration.
|
|
outside := filepath.Join(tempDirOutsideDefaultTemp(t), "outside.txt")
|
|
deniedOutside := registry.RunWithOptions(context.Background(), "write_file", map[string]any{
|
|
"path": outside,
|
|
"content": "never",
|
|
}, tools.RunOptions{PermissionGranted: true})
|
|
if deniedOutside.Status == tools.StatusOK {
|
|
t.Fatalf("write outside all roots must fail, got ok: %s", deniedOutside.Output)
|
|
}
|
|
if !strings.Contains(deniedOutside.Output, "must stay inside the workspace") {
|
|
t.Fatalf("expected outside-workspace denial, got %q", deniedOutside.Output)
|
|
}
|
|
if _, err := os.Stat(outside); !os.IsNotExist(err) {
|
|
t.Fatalf("expected outside file to remain absent, stat err=%v", err)
|
|
}
|
|
}
|
|
|
|
func tempDirOutsideDefaultTemp(t *testing.T) string {
|
|
t.Helper()
|
|
dir, err := os.MkdirTemp(".", ".zero-sandbox-outside-")
|
|
if err != nil {
|
|
t.Fatalf("MkdirTemp outside default temp: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = os.RemoveAll(dir) })
|
|
abs, err := filepath.Abs(dir)
|
|
if err != nil {
|
|
t.Fatalf("Abs(%q): %v", dir, err)
|
|
}
|
|
if resolved, err := filepath.EvalSymlinks(abs); err == nil {
|
|
return resolved
|
|
}
|
|
return filepath.Clean(abs)
|
|
}
|