mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 17:57:54 -10:00
- Remove unused safeRequestedPrefix, safeShellCommandTokens (internal/agent/command_prefix.go), and truncateHeadTail (internal/tools/bash.go): each was a thin wrapper with no remaining callers. - Drop an ineffectual assignment in stringListArgWebSearch: every return path after it already sets the named return explicitly. - Simplify a negated-and condition in partition_cache_stable_test.go via De Morgan's law. - Use strings.Contains instead of strings.Index(...) < 0 in edit_replacers.go. - Fix error string style (ST1005): lowercase "unsafe redirect URL" and drop the trailing period from webFetchPublicOnlyHint. Skipped the errcheck findings on this pass (unchecked defer Close()/Write() errors): those are stylistic, not correctness bugs, and this repo's CI only runs go vet, not golangci-lint.
546 lines
19 KiB
Go
546 lines
19 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
zeroSandbox "github.com/Gitlawb/zero/internal/sandbox"
|
|
)
|
|
|
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (fn roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
return fn(request)
|
|
}
|
|
|
|
type webFetchResolverFunc func(context.Context, string, string) ([]netip.Addr, error)
|
|
|
|
func (fn webFetchResolverFunc) LookupNetIP(ctx context.Context, network string, host string) ([]netip.Addr, error) {
|
|
return fn(ctx, network, host)
|
|
}
|
|
|
|
type webFetchDialFunc func(context.Context, string, string) (net.Conn, error)
|
|
|
|
func (fn webFetchDialFunc) DialContext(ctx context.Context, network string, address string) (net.Conn, error) {
|
|
return fn(ctx, network, address)
|
|
}
|
|
|
|
func TestWebFetchToolSafetyAndSchema(t *testing.T) {
|
|
tool := NewWebFetchTool()
|
|
|
|
if tool.Name() != "web_fetch" {
|
|
t.Fatalf("Name = %q, want web_fetch", tool.Name())
|
|
}
|
|
if tool.Description() == "" {
|
|
t.Fatal("Description is empty")
|
|
}
|
|
safety := tool.Safety()
|
|
if safety.SideEffect != SideEffectNetwork || safety.Permission != PermissionPrompt || !safety.AdvertiseInAuto {
|
|
t.Fatalf("unexpected safety metadata: %#v", safety)
|
|
}
|
|
if safety.Reason == "" {
|
|
t.Fatal("Safety reason is empty")
|
|
}
|
|
|
|
schema := tool.Parameters()
|
|
if schema.Type != "object" || schema.AdditionalProperties {
|
|
t.Fatalf("unexpected schema envelope: %#v", schema)
|
|
}
|
|
if len(schema.Required) != 1 || schema.Required[0] != "url" {
|
|
t.Fatalf("required fields = %#v, want url only", schema.Required)
|
|
}
|
|
if schema.Properties["url"].Type != "string" {
|
|
t.Fatalf("url schema = %#v, want string", schema.Properties["url"])
|
|
}
|
|
maxBytes := schema.Properties["max_bytes"]
|
|
if maxBytes.Type != "integer" || maxBytes.Minimum == nil || *maxBytes.Minimum != 1 || maxBytes.Maximum == nil {
|
|
t.Fatalf("max_bytes schema = %#v, want bounded integer", maxBytes)
|
|
}
|
|
}
|
|
|
|
func TestWebFetchToolFetchesHTTPText(t *testing.T) {
|
|
tool := newWebFetchToolWithClient(webFetchTestClient(func(request *http.Request) (*http.Response, error) {
|
|
if request.Method != http.MethodGet {
|
|
t.Fatalf("method = %s, want GET", request.Method)
|
|
}
|
|
if request.Header.Get("User-Agent") == "" {
|
|
t.Fatal("expected User-Agent header")
|
|
}
|
|
return webFetchTestResponse(request, http.StatusOK, "text/plain; charset=utf-8", "hello zero"), nil
|
|
}))
|
|
|
|
result := tool.Run(context.Background(), map[string]any{
|
|
"url": "https://example.com/guide?token=secret-token",
|
|
})
|
|
|
|
if result.Status != StatusOK {
|
|
t.Fatalf("expected ok status, got %s: %s", result.Status, result.Output)
|
|
}
|
|
for _, want := range []string{"URL: https://example.com/guide?token=[REDACTED]", "Status: 200 OK", "Content-Type: text/plain; charset=utf-8", "hello zero"} {
|
|
if !strings.Contains(result.Output, want) {
|
|
t.Fatalf("expected output to contain %q, got %q", want, result.Output)
|
|
}
|
|
}
|
|
if strings.Contains(result.Output, "secret-token") {
|
|
t.Fatalf("expected URL secrets to be redacted, got %q", result.Output)
|
|
}
|
|
if result.Meta["status_code"] != "200" || result.Meta["content_type"] != "text/plain; charset=utf-8" || result.Meta["truncated"] != "false" {
|
|
t.Fatalf("unexpected metadata: %#v", result.Meta)
|
|
}
|
|
}
|
|
|
|
func TestWebFetchToolTruncatesAtMaxBytes(t *testing.T) {
|
|
tool := newWebFetchToolWithClient(webFetchTestClient(func(request *http.Request) (*http.Response, error) {
|
|
return webFetchTestResponse(request, http.StatusOK, "text/plain", "abcdefg"), nil
|
|
}))
|
|
|
|
result := tool.Run(context.Background(), map[string]any{
|
|
"url": "https://example.com/long",
|
|
"max_bytes": 4,
|
|
})
|
|
|
|
if result.Status != StatusOK {
|
|
t.Fatalf("expected ok status, got %s: %s", result.Status, result.Output)
|
|
}
|
|
if !result.Truncated || result.Meta["truncated"] != "true" || result.Meta["bytes"] != "4" {
|
|
t.Fatalf("expected truncation metadata, got truncated=%v meta=%#v", result.Truncated, result.Meta)
|
|
}
|
|
if !strings.Contains(result.Output, "abcd") || strings.Contains(result.Output, "efg") {
|
|
t.Fatalf("expected output to contain only truncated body, got %q", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestWebFetchToolRejectsUnsafeURLsBeforeNetwork(t *testing.T) {
|
|
tool := newWebFetchToolWithClient(webFetchTestClient(func(*http.Request) (*http.Response, error) {
|
|
t.Fatal("unsafe URL should be rejected before network transport")
|
|
return nil, nil
|
|
}))
|
|
|
|
for _, rawURL := range []string{
|
|
"file:///tmp/secret",
|
|
"ftp://example.com/file",
|
|
"http://127.0.0.1/admin",
|
|
"http://localhost/status",
|
|
"http://169.254.169.254/latest/meta-data",
|
|
"http://100.64.0.1/internal",
|
|
"http://0.1.2.3/internal",
|
|
"http://255.255.255.255/internal",
|
|
"http://[64:ff9b::7f00:1]/internal",
|
|
"http://[64:ff9b::a9fe:a9fe]/latest/meta-data",
|
|
"http://[64:ff9b:1:7f00:1::]/internal",
|
|
"http://[2002:7f00:1::]/internal",
|
|
"http://[::7f00:1]/internal",
|
|
"http://user:[email protected]/private",
|
|
} {
|
|
t.Run(rawURL, func(t *testing.T) {
|
|
result := tool.Run(context.Background(), map[string]any{"url": rawURL})
|
|
if result.Status != StatusError {
|
|
t.Fatalf("expected unsafe URL error, got %s: %s", result.Status, result.Output)
|
|
}
|
|
if !strings.Contains(result.Output, "Unsafe URL") {
|
|
t.Fatalf("expected unsafe URL message, got %q", result.Output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWebFetchRejectBeforePermissionClassifiesURLs(t *testing.T) {
|
|
tool, ok := NewWebFetchTool().(PrePermissionRejecter)
|
|
if !ok {
|
|
t.Fatalf("NewWebFetchTool returned %T, want PrePermissionRejecter", NewWebFetchTool())
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
args map[string]any
|
|
wantRejected bool
|
|
wantContains string
|
|
}{
|
|
{
|
|
name: "public remote",
|
|
args: map[string]any{"url": "https://example.com/docs"},
|
|
},
|
|
{
|
|
name: "hostname needs resolver later",
|
|
args: map[string]any{"url": "https://private.example/status"},
|
|
},
|
|
{
|
|
name: "localhost custom port",
|
|
args: map[string]any{"url": "http://localhost:8000/status"},
|
|
wantRejected: true,
|
|
wantContains: "bash with curl",
|
|
},
|
|
{
|
|
name: "loopback ipv4",
|
|
args: map[string]any{"url": "http://127.0.0.1/admin"},
|
|
wantRejected: true,
|
|
wantContains: "bash with curl",
|
|
},
|
|
{
|
|
name: "loopback ipv6",
|
|
args: map[string]any{"url": "http://[::1]/admin"},
|
|
wantRejected: true,
|
|
wantContains: "bash with curl",
|
|
},
|
|
{
|
|
name: "private ipv4",
|
|
args: map[string]any{"url": "http://10.0.0.1/status"},
|
|
wantRejected: true,
|
|
wantContains: "bash with curl",
|
|
},
|
|
{
|
|
name: "private ipv6",
|
|
args: map[string]any{"url": "http://[fc00::1]/status"},
|
|
wantRejected: true,
|
|
wantContains: "bash with curl",
|
|
},
|
|
{
|
|
name: "localhost suffix",
|
|
args: map[string]any{"url": "http://app.localhost/status"},
|
|
wantRejected: true,
|
|
wantContains: "bash with curl",
|
|
},
|
|
{
|
|
name: "local suffix",
|
|
args: map[string]any{"url": "http://printer.local/status"},
|
|
wantRejected: true,
|
|
wantContains: "bash with curl",
|
|
},
|
|
{
|
|
name: "metadata host",
|
|
args: map[string]any{"url": "http://metadata.google.internal/latest"},
|
|
wantRejected: true,
|
|
wantContains: "bash with curl",
|
|
},
|
|
{
|
|
name: "public non-default port",
|
|
args: map[string]any{"url": "https://example.com:8443/docs"},
|
|
wantRejected: true,
|
|
wantContains: "default ports",
|
|
},
|
|
{
|
|
name: "unsupported scheme",
|
|
args: map[string]any{"url": "ftp://example.com/file"},
|
|
wantRejected: true,
|
|
wantContains: "only http and https",
|
|
},
|
|
{
|
|
name: "bad max bytes",
|
|
args: map[string]any{"url": "https://example.com/docs", "max_bytes": 0},
|
|
wantRejected: true,
|
|
wantContains: "Invalid arguments",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result, rejected := tool.RejectBeforePermission(tc.args)
|
|
if rejected != tc.wantRejected {
|
|
t.Fatalf("rejected = %v, want %v; result=%#v", rejected, tc.wantRejected, result)
|
|
}
|
|
if tc.wantContains != "" && !strings.Contains(result.Output, tc.wantContains) {
|
|
t.Fatalf("expected output to contain %q, got %q", tc.wantContains, result.Output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWebFetchToolRejectsLocalhostBeforeDefaultPort(t *testing.T) {
|
|
tool := newWebFetchToolWithClient(webFetchTestClient(func(*http.Request) (*http.Response, error) {
|
|
t.Fatal("localhost URL should be rejected before network transport")
|
|
return nil, nil
|
|
}))
|
|
|
|
result := tool.Run(context.Background(), map[string]any{"url": "http://localhost:8000/status"})
|
|
|
|
if result.Status != StatusError {
|
|
t.Fatalf("expected error status, got %s: %s", result.Status, result.Output)
|
|
}
|
|
if !strings.Contains(result.Output, "bash with curl") {
|
|
t.Fatalf("expected curl guidance, got %q", result.Output)
|
|
}
|
|
if strings.Contains(result.Output, "default port") {
|
|
t.Fatalf("localhost should be classified before port validation, got %q", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestWebFetchToolRejectsNonDefaultPorts(t *testing.T) {
|
|
tool := newWebFetchToolWithClient(webFetchTestClient(func(*http.Request) (*http.Response, error) {
|
|
t.Fatal("non-default port should be rejected before network transport")
|
|
return nil, nil
|
|
}))
|
|
|
|
for _, rawURL := range []string{
|
|
"http://example.com:22/",
|
|
"https://example.com:80/",
|
|
"https://example.com:6379/",
|
|
} {
|
|
t.Run(rawURL, func(t *testing.T) {
|
|
result := tool.Run(context.Background(), map[string]any{"url": rawURL})
|
|
if result.Status != StatusError {
|
|
t.Fatalf("expected unsafe URL error, got %s: %s", result.Status, result.Output)
|
|
}
|
|
if !strings.Contains(result.Output, "default port") {
|
|
t.Fatalf("expected default-port message, got %q", result.Output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWebFetchToolRejectsHostnamesResolvingToPrivateAddresses(t *testing.T) {
|
|
tool := newWebFetchToolWithClientAndResolver(
|
|
webFetchTestClient(func(*http.Request) (*http.Response, error) {
|
|
t.Fatal("private resolved host should be rejected before network transport")
|
|
return nil, nil
|
|
}),
|
|
webFetchResolverFunc(func(_ context.Context, network string, host string) ([]netip.Addr, error) {
|
|
if network != "ip" || host != "private.example" {
|
|
t.Fatalf("unexpected lookup network=%q host=%q", network, host)
|
|
}
|
|
return []netip.Addr{netip.MustParseAddr("10.0.0.12")}, nil
|
|
}),
|
|
)
|
|
|
|
result := tool.Run(context.Background(), map[string]any{"url": "https://private.example/status"})
|
|
|
|
if result.Status != StatusError {
|
|
t.Fatalf("expected private resolved host error, got %s: %s", result.Status, result.Output)
|
|
}
|
|
if !strings.Contains(result.Output, "private network hosts are blocked") {
|
|
t.Fatalf("expected private host message, got %q", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestWebFetchToolConfiguresDialTimeSafetyForDefaultTransport(t *testing.T) {
|
|
tool, ok := NewWebFetchTool().(webFetchTool)
|
|
if !ok {
|
|
t.Fatalf("NewWebFetchTool returned %T, want webFetchTool", NewWebFetchTool())
|
|
}
|
|
|
|
client := tool.clientForRun()
|
|
transport, ok := client.Transport.(*http.Transport)
|
|
if !ok {
|
|
t.Fatalf("client transport = %T, want *http.Transport", client.Transport)
|
|
}
|
|
if transport.DialContext == nil {
|
|
t.Fatal("expected web_fetch transport to install a safe DialContext")
|
|
}
|
|
if transport.Proxy != nil {
|
|
t.Fatal("expected web_fetch transport to disable proxy resolution")
|
|
}
|
|
}
|
|
|
|
func TestWebFetchSafeDialRejectsPrivateRebindAddress(t *testing.T) {
|
|
dialCalled := false
|
|
dial := webFetchSafeDialContext(
|
|
webFetchResolverFunc(func(_ context.Context, network string, host string) ([]netip.Addr, error) {
|
|
if network != "ip" || host != "rebind.example" {
|
|
t.Fatalf("unexpected lookup network=%q host=%q", network, host)
|
|
}
|
|
return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, nil
|
|
}),
|
|
webFetchDialFunc(func(context.Context, string, string) (net.Conn, error) {
|
|
dialCalled = true
|
|
return nil, errors.New("dial should not run")
|
|
}),
|
|
)
|
|
|
|
_, err := dial(context.Background(), "tcp", "rebind.example:443")
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "loopback hosts are blocked") {
|
|
t.Fatalf("expected loopback rejection, got %v", err)
|
|
}
|
|
if dialCalled {
|
|
t.Fatal("dialer was called after unsafe DNS result")
|
|
}
|
|
}
|
|
|
|
func TestWebFetchSafeDialPinsResolvedPublicAddress(t *testing.T) {
|
|
var dialedAddress string
|
|
stop := errors.New("stop after address capture")
|
|
dial := webFetchSafeDialContext(
|
|
webFetchResolverFunc(func(_ context.Context, network string, host string) ([]netip.Addr, error) {
|
|
if network != "ip" || host != "public.example" {
|
|
t.Fatalf("unexpected lookup network=%q host=%q", network, host)
|
|
}
|
|
return []netip.Addr{netip.MustParseAddr("8.8.8.8")}, nil
|
|
}),
|
|
webFetchDialFunc(func(_ context.Context, _ string, address string) (net.Conn, error) {
|
|
dialedAddress = address
|
|
return nil, stop
|
|
}),
|
|
)
|
|
|
|
_, err := dial(context.Background(), "tcp", "public.example:443")
|
|
|
|
if !errors.Is(err, stop) {
|
|
t.Fatalf("expected captured dial error, got %v", err)
|
|
}
|
|
if dialedAddress != "8.8.8.8:443" {
|
|
t.Fatalf("dialed address = %q, want resolved IP address", dialedAddress)
|
|
}
|
|
}
|
|
|
|
func TestWebFetchToolRejectsUnsafeRedirects(t *testing.T) {
|
|
for _, location := range []string{
|
|
"http://127.0.0.1/private",
|
|
"http://100.64.0.1/internal",
|
|
} {
|
|
t.Run(location, func(t *testing.T) {
|
|
tool := newWebFetchToolWithClient(webFetchTestClient(func(request *http.Request) (*http.Response, error) {
|
|
response := webFetchTestResponse(request, http.StatusFound, "text/plain", "redirect")
|
|
response.Header.Set("Location", location)
|
|
return response, nil
|
|
}))
|
|
|
|
result := tool.Run(context.Background(), map[string]any{"url": "https://example.com/start"})
|
|
|
|
if result.Status != StatusError {
|
|
t.Fatalf("expected redirect safety error, got %s: %s", result.Status, result.Output)
|
|
}
|
|
if !strings.Contains(result.Output, "unsafe redirect URL") {
|
|
t.Fatalf("expected unsafe redirect message, got %q", result.Output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWebFetchToolRedactsContentType(t *testing.T) {
|
|
tool := newWebFetchToolWithClient(webFetchTestClient(func(request *http.Request) (*http.Response, error) {
|
|
return webFetchTestResponse(request, http.StatusOK, "text/plain; token=secret-token", "hello"), nil
|
|
}))
|
|
|
|
result := tool.Run(context.Background(), map[string]any{"url": "https://example.com/redact"})
|
|
|
|
if result.Status != StatusOK {
|
|
t.Fatalf("expected ok status, got %s: %s", result.Status, result.Output)
|
|
}
|
|
if strings.Contains(result.Output, "secret-token") || strings.Contains(result.Meta["content_type"], "secret-token") {
|
|
t.Fatalf("expected content type to be redacted, output=%q meta=%#v", result.Output, result.Meta)
|
|
}
|
|
if !strings.Contains(result.Output, "token=[REDACTED]") || !strings.Contains(result.Meta["content_type"], "token=[REDACTED]") {
|
|
t.Fatalf("expected redacted token in content type, output=%q meta=%#v", result.Output, result.Meta)
|
|
}
|
|
}
|
|
|
|
func TestWebFetchToolRejectsNonSuccessStatus(t *testing.T) {
|
|
tool := newWebFetchToolWithClient(webFetchTestClient(func(request *http.Request) (*http.Response, error) {
|
|
return webFetchTestResponse(request, http.StatusNotFound, "text/plain", "missing page"), nil
|
|
}))
|
|
|
|
result := tool.Run(context.Background(), map[string]any{"url": "https://example.com/missing"})
|
|
|
|
if result.Status != StatusError {
|
|
t.Fatalf("expected status error, got %s: %s", result.Status, result.Output)
|
|
}
|
|
if !strings.Contains(result.Output, "HTTP 404 Not Found") || !strings.Contains(result.Output, "missing page") {
|
|
t.Fatalf("unexpected non-success output: %q", result.Output)
|
|
}
|
|
}
|
|
|
|
func webFetchTestClient(handler func(*http.Request) (*http.Response, error)) *http.Client {
|
|
return &http.Client{Transport: roundTripFunc(handler)}
|
|
}
|
|
|
|
func webFetchTestResponse(request *http.Request, statusCode int, contentType string, body string) *http.Response {
|
|
response := &http.Response{
|
|
StatusCode: statusCode,
|
|
Status: httpStatusLine(statusCode),
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
Request: request,
|
|
}
|
|
if contentType != "" {
|
|
response.Header.Set("Content-Type", contentType)
|
|
}
|
|
return response
|
|
}
|
|
|
|
func httpStatusLine(statusCode int) string {
|
|
return strings.TrimSpace(strings.Join([]string{strconv.Itoa(statusCode), http.StatusText(statusCode)}, " "))
|
|
}
|
|
|
|
func TestWebFetchRunWithSandboxAllowsUnderShellNetworkDeny(t *testing.T) {
|
|
called := false
|
|
client := webFetchTestClient(func(request *http.Request) (*http.Response, error) {
|
|
called = true
|
|
return webFetchTestResponse(request, http.StatusOK, "text/plain", "ok"), nil
|
|
})
|
|
tool, ok := newWebFetchToolWithClientAndResolver(client, publicWebFetchResolver(t, "example.com")).(webFetchTool)
|
|
if !ok {
|
|
t.Fatalf("newWebFetchToolWithClientAndResolver returned %T", newWebFetchToolWithClientAndResolver(client, nil))
|
|
}
|
|
|
|
engine := zeroSandbox.NewEngine(zeroSandbox.EngineOptions{
|
|
Policy: zeroSandbox.Policy{Mode: zeroSandbox.ModeEnforce, Network: zeroSandbox.NetworkDeny},
|
|
})
|
|
result := tool.RunWithSandbox(context.Background(), map[string]any{"url": "https://example.com"}, engine)
|
|
if result.Status != StatusOK {
|
|
t.Fatalf("web_fetch must run under shell network deny, got %q: %s", result.Status, result.Output)
|
|
}
|
|
if !called {
|
|
t.Fatal("web_fetch transport must be called")
|
|
}
|
|
}
|
|
|
|
func TestWebFetchRunWithSandboxAllowsUnderShellNetworkAllow(t *testing.T) {
|
|
called := false
|
|
client := webFetchTestClient(func(request *http.Request) (*http.Response, error) {
|
|
called = true
|
|
return webFetchTestResponse(request, http.StatusOK, "text/plain", "ok"), nil
|
|
})
|
|
tool := newWebFetchToolWithClientAndResolver(client, publicWebFetchResolver(t, "evil.test")).(webFetchTool)
|
|
|
|
engine := zeroSandbox.NewEngine(zeroSandbox.EngineOptions{
|
|
Policy: zeroSandbox.Policy{Mode: zeroSandbox.ModeEnforce, Network: zeroSandbox.NetworkAllow},
|
|
})
|
|
result := tool.RunWithSandbox(context.Background(), map[string]any{"url": "https://evil.test"}, engine)
|
|
if result.Status != StatusOK {
|
|
t.Fatalf("web_fetch must run under shell network allow, got %q: %s", result.Status, result.Output)
|
|
}
|
|
if !called {
|
|
t.Fatal("web_fetch transport must be called")
|
|
}
|
|
}
|
|
|
|
func TestRegistryRoutesWebFetchThroughSandboxPath(t *testing.T) {
|
|
called := false
|
|
client := webFetchTestClient(func(request *http.Request) (*http.Response, error) {
|
|
called = true
|
|
return webFetchTestResponse(request, http.StatusOK, "text/plain", "ok"), nil
|
|
})
|
|
registry := NewRegistry()
|
|
registry.Register(newWebFetchToolWithClientAndResolver(client, publicWebFetchResolver(t, "evil.test")))
|
|
|
|
engine := zeroSandbox.NewEngine(zeroSandbox.EngineOptions{
|
|
Policy: zeroSandbox.Policy{Mode: zeroSandbox.ModeEnforce, Network: zeroSandbox.NetworkDeny},
|
|
})
|
|
res := registry.RunWithOptions(context.Background(), "web_fetch", map[string]any{"url": "https://evil.test"}, RunOptions{
|
|
Sandbox: engine,
|
|
PermissionGranted: true,
|
|
})
|
|
if res.Status != StatusOK {
|
|
t.Fatalf("registry must route web_fetch through RunWithSandbox, got %q: %s", res.Status, res.Output)
|
|
}
|
|
if !called {
|
|
t.Fatal("web_fetch transport must be called")
|
|
}
|
|
}
|
|
|
|
func publicWebFetchResolver(t *testing.T, expectedHost string) webFetchResolver {
|
|
t.Helper()
|
|
return webFetchResolverFunc(func(_ context.Context, network string, host string) ([]netip.Addr, error) {
|
|
if network != "ip" || host != expectedHost {
|
|
t.Fatalf("unexpected lookup network=%q host=%q", network, host)
|
|
}
|
|
return []netip.Addr{netip.MustParseAddr("93.184.216.34")}, nil
|
|
})
|
|
}
|