Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes. Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending. Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete. Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
190 lines
6.6 KiB
Go
190 lines
6.6 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
agenttools "agent-native/runtime/tools"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
var errToolActorDisabled = errors.New("tool actor disabled")
|
|
|
|
type guardedToolContext struct {
|
|
ToolExecCtx
|
|
ctx context.Context
|
|
check func() error
|
|
}
|
|
|
|
func (c guardedToolContext) Context() context.Context {
|
|
if c.ctx != nil {
|
|
return c.ctx
|
|
}
|
|
return context.Background()
|
|
}
|
|
|
|
func (c guardedToolContext) CheckExternalEffect() error {
|
|
if c.check != nil {
|
|
return c.check()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestHTTPToolChecksActorAndPropagatesCancellation(t *testing.T) {
|
|
var calls atomic.Int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
calls.Add(1)
|
|
<-r.Context().Done()
|
|
}))
|
|
defer srv.Close()
|
|
exec := HTTPExecutor(core.HTTPToolSpec{Method: "POST", URL: srv.URL}, nil)
|
|
denied := guardedToolContext{ToolExecCtx: dummyCtx{}, check: func() error { return errToolActorDisabled }}
|
|
if _, err := exec(denied, nil); !errors.Is(err, errToolActorDisabled) || calls.Load() != 0 {
|
|
t.Fatalf("disabled HTTP effect: %v, calls=%d", err, calls.Load())
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, err := exec(guardedToolContext{ToolExecCtx: dummyCtx{}, ctx: ctx}, nil); !errors.Is(err, context.Canceled) || calls.Load() != 0 {
|
|
t.Fatalf("canceled HTTP effect: %v, calls=%d", err, calls.Load())
|
|
}
|
|
}
|
|
|
|
func TestHTTPToolCancelsInFlightRequest(t *testing.T) {
|
|
started := make(chan struct{})
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
close(started)
|
|
<-r.Context().Done()
|
|
}))
|
|
defer srv.Close()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
rc := guardedToolContext{ToolExecCtx: dummyCtx{}, ctx: ctx}
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
_, err := HTTPExecutor(core.HTTPToolSpec{Method: "GET", URL: srv.URL}, nil)(rc, nil)
|
|
done <- err
|
|
}()
|
|
select {
|
|
case <-started:
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("HTTP request did not reach server")
|
|
}
|
|
cancel()
|
|
select {
|
|
case err := <-done:
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("in-flight request cancellation: %v", err)
|
|
}
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("in-flight HTTP request ignored cancellation")
|
|
}
|
|
}
|
|
|
|
type observedSearchProvider struct {
|
|
calls int
|
|
ctx context.Context
|
|
}
|
|
|
|
func (p *observedSearchProvider) Search(ctx context.Context, _ agenttools.SearchRequest) (agenttools.SearchResponse, error) {
|
|
p.calls++
|
|
p.ctx = ctx
|
|
return agenttools.SearchResponse{}, nil
|
|
}
|
|
|
|
func TestSearchToolChecksActorAndPassesRunContext(t *testing.T) {
|
|
p := &observedSearchProvider{}
|
|
r := NewRegistry()
|
|
if err := RegisterSearchTools(r, p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
denied := guardedToolContext{ToolExecCtx: dummyCtx{}, check: func() error { return errToolActorDisabled }}
|
|
if res := r.Execute(denied, core.ToolCallRequest{Name: "web_search", Arguments: json.RawMessage(`{"query":"q"}`)}); res.Status != "error" || !strings.Contains(res.Output, errToolActorDisabled.Error()) || p.calls != 0 {
|
|
t.Fatalf("disabled search effect: %+v, calls=%d", res, p.calls)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if res := r.Execute(guardedToolContext{ToolExecCtx: dummyCtx{}, ctx: ctx}, core.ToolCallRequest{Name: "web_search", Arguments: json.RawMessage(`{"query":"q"}`)}); res.Status != "error" || p.calls != 0 {
|
|
t.Fatalf("canceled search effect: %+v, calls=%d", res, p.calls)
|
|
}
|
|
if res := r.Execute(guardedToolContext{ToolExecCtx: dummyCtx{}}, core.ToolCallRequest{Name: "web_search", Arguments: json.RawMessage(`{"query":"q"}`)}); res.Status != "ok" || p.calls != 1 || p.ctx == nil {
|
|
t.Fatalf("search context: %+v, calls=%d, context=%v", res, p.calls, p.ctx)
|
|
}
|
|
}
|
|
|
|
func TestFSAndInterpreterCheckBeforeFilesystemEffects(t *testing.T) {
|
|
root := t.TempDir()
|
|
fsCtx := &fakeFSCtx{root: root, owner: "o", conv: "c", fsToolsOn: true}
|
|
deniedFS := guardedToolContext{ToolExecCtx: fsCtx, check: func() error { return errToolActorDisabled }}
|
|
if _, err := execFSWrite(deniedFS, json.RawMessage(`{"path":"note.md","content":"x"}`)); !errors.Is(err, errToolActorDisabled) {
|
|
t.Fatalf("disabled fs write: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "o", "c")); !os.IsNotExist(err) {
|
|
t.Fatalf("disabled fs write created sandbox: %v", err)
|
|
}
|
|
execCtx := &execFake{root: root, owner: "o", conv: "c", execOn: true}
|
|
deniedExec := guardedToolContext{ToolExecCtx: execCtx, check: func() error { return errToolActorDisabled }}
|
|
if _, err := execShell(deniedExec, json.RawMessage(`{"script":"echo x"}`)); !errors.Is(err, errToolActorDisabled) {
|
|
t.Fatalf("disabled shell write/start: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "o", "c")); !os.IsNotExist(err) {
|
|
t.Fatalf("disabled shell created sandbox: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInterpreterRechecksBeforeProcessStart(t *testing.T) {
|
|
root := t.TempDir()
|
|
mark := filepath.Join(t.TempDir(), "executed")
|
|
base := &execFake{root: root, owner: "o", conv: "c", execOn: true}
|
|
checks := 0
|
|
rc := guardedToolContext{ToolExecCtx: base, check: func() error {
|
|
checks++
|
|
if checks >= 4 {
|
|
return errToolActorDisabled
|
|
}
|
|
return nil
|
|
}}
|
|
_, err := execShell(rc, json.RawMessage(`{"script":"touch `+mark+`"}`))
|
|
if !errors.Is(err, errToolActorDisabled) || checks < 4 {
|
|
t.Fatalf("process start recheck: %v, checks=%d", err, checks)
|
|
}
|
|
if _, err := os.Stat(mark); !os.IsNotExist(err) {
|
|
t.Fatalf("script ran after actor disable: %v", err)
|
|
}
|
|
}
|
|
|
|
type guardedCodeContext struct{ guardedToolContext }
|
|
|
|
func (guardedCodeContext) ExecuteTool(core.ToolCallRequest) core.ToolCallResult {
|
|
return core.ToolCallResult{Status: "ok"}
|
|
}
|
|
|
|
func TestCodeOrchestrationChecksBeforeStarting(t *testing.T) {
|
|
root := t.TempDir()
|
|
base := &execFake{root: root, owner: "o", conv: "c", settings: map[string]bool{"runtime.codeOrchestration": true}}
|
|
rc := guardedCodeContext{guardedToolContext{ToolExecCtx: base, check: func() error { return errToolActorDisabled }}}
|
|
if _, err := execCodeScript(rc, json.RawMessage(`{"script":"print('hi')"}`)); !errors.Is(err, errToolActorDisabled) {
|
|
t.Fatalf("disabled code orchestration: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApplyUpperCheckedStopsBeforeWorkspaceWrite(t *testing.T) {
|
|
dir := t.TempDir()
|
|
upper := t.TempDir()
|
|
write(t, filepath.Join(upper, "new.txt"), "new")
|
|
if _, err := applyUpperChecked(context.Background(), func() error { return errToolActorDisabled }, upper, dir, map[string]string{}); !errors.Is(err, errToolActorDisabled) {
|
|
t.Fatalf("disabled workspace apply: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "new.txt")); !os.IsNotExist(err) {
|
|
t.Fatalf("disabled workspace apply wrote file: %v", err)
|
|
}
|
|
}
|