mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
Rewrite internal/runtime/delegation around one Runner and one tool. spawn_agents runs 1 to 8 independent tasks in parallel, each in its own session with three base tools (read_file, write_file, bash); plan mode and read-only skills keep a read-only child. A child's full answer is archived; the caller gets the opening, a reference, and the files the child read and wrote, and pages the rest through read_subagent_result. Removed with it: task, read_only_task, parallel_tasks, fleet, list_subagents, background sub-tasks, steering and its limits and settings UI, continue_from, write_paths claims, isolated worktrees, best-of-n, the run graph and the runtime-resume benchmark. Stale writes by children stay refused through the per-agent file views. The use_capability description no longer names task:fleet, so the stable prefix and its golden files change once.
301 lines
14 KiB
Go
301 lines
14 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"reasonix/internal/base/diff"
|
|
"reasonix/internal/base/testenv"
|
|
"reasonix/internal/contract/event"
|
|
"reasonix/internal/contract/provider"
|
|
"reasonix/internal/contract/tool"
|
|
"reasonix/internal/ext/hook"
|
|
"reasonix/internal/state/checkpoint"
|
|
"reasonix/internal/state/sessionstore"
|
|
"reasonix/internal/tools/builtin"
|
|
)
|
|
|
|
type accessProbeTool struct {
|
|
tool.Tool
|
|
previews, executions int
|
|
}
|
|
|
|
func (p *accessProbeTool) WritePaths(args json.RawMessage) ([]string, error) {
|
|
return p.Tool.(tool.WritePathResolver).WritePaths(args)
|
|
}
|
|
func (p *accessProbeTool) ReadTarget(args json.RawMessage) string {
|
|
if reader, ok := p.Tool.(tool.ReadTargeter); ok {
|
|
return reader.ReadTarget(args)
|
|
}
|
|
return ""
|
|
}
|
|
func (p *accessProbeTool) FormatReadRefusal(err error) error {
|
|
if formatter, ok := p.Tool.(tool.ReadRefusalFormatter); ok {
|
|
return formatter.FormatReadRefusal(err)
|
|
}
|
|
return err
|
|
}
|
|
func (p *accessProbeTool) WritesNamedPaths() bool { return tool.WritesNamedPaths(p.Tool) }
|
|
func (p *accessProbeTool) Preview(ctx context.Context, args json.RawMessage) (diff.Change, error) {
|
|
p.previews++
|
|
if pv, ok := p.Tool.(tool.Previewer); ok {
|
|
return pv.Preview(ctx, args)
|
|
}
|
|
return diff.Change{}, nil
|
|
}
|
|
func (p *accessProbeTool) Execute(ctx context.Context, args json.RawMessage) (string, error) {
|
|
p.executions++
|
|
return p.Tool.Execute(ctx, args)
|
|
}
|
|
|
|
func TestPreToolAccessRejectsForbiddenFileTools(t *testing.T) {
|
|
root := testenv.TempDir(t)
|
|
private := filepath.Join(root, "private")
|
|
if err := os.MkdirAll(private, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(private, "note.go")
|
|
const original = "package main\n// ACCESS_CANARY_DO_NOT_DISCLOSE\nfunc RemoveMe() {}\n"
|
|
if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ws := builtin.Workspace{Dir: root, ForbidReadRoots: []string{private}}
|
|
for _, name := range []string{"read_file", "edit_file", "multi_edit", "write_file", "notebook_edit", "delete_range", "delete_symbol"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
tl := ws.Tools(name)[0]
|
|
probe := &accessProbeTool{Tool: tl}
|
|
reg := tool.NewRegistry()
|
|
reg.Add(probe)
|
|
h := &stubHooks{}
|
|
a := New(nil, reg, sessionstore.NewSession(""), Options{Hooks: h, CheckTargetAccess: ws.TargetAccessCheck()}, event.Discard)
|
|
args, _ := json.Marshal(map[string]any{"path": path, "old_string": "not present", "new_string": "changed", "content": "changed", "edits": []map[string]string{{"old_string": "not present", "new_string": "changed"}}, "cell_number": 0, "new_source": "changed", "start_anchor": "package main", "end_anchor": "func RemoveMe() {}", "name": "RemoveMe", "source_path": path, "destination_path": filepath.Join(root, "moved.go")})
|
|
out := a.executeOne(context.Background(), &a.turn, provider.ToolCall{ID: "blocked", Name: name, Arguments: string(args)})
|
|
message, code := "forbidden-read list", builtin.CodeReadForbidden
|
|
if name == "write_file" {
|
|
message, code = "Cannot overwrite target file: permission to read existing contents is required.", builtin.CodeOverwriteReadForbidden
|
|
}
|
|
if !out.blocked || !strings.Contains(out.output, message) || out.refusalCode != code {
|
|
t.Fatalf("wrong refusal: %+v", out)
|
|
}
|
|
if probe.previews != 0 || probe.executions != 0 || len(h.preSeen) != 0 {
|
|
t.Fatalf("denied access reached hook or tool: previews=%d execute=%d hooks=%v", probe.previews, probe.executions, h.preSeen)
|
|
}
|
|
if b, err := os.ReadFile(path); err != nil || string(b) != original {
|
|
t.Fatalf("protected file changed: %q %v", b, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPreToolUseReadOnlyWithoutMutationHooksDoesNotRecordHookWriteGap(t *testing.T) {
|
|
root := testenv.TempDir(t)
|
|
path := filepath.Join(root, "note.txt")
|
|
if err := os.WriteFile(path, []byte("status=draft\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ws := builtin.Workspace{Dir: root}
|
|
reg := tool.NewRegistry()
|
|
reg.Add(ws.Tools("read_file")[0])
|
|
store := checkpoint.New("", root)
|
|
store.Begin(1, "read the file", 0)
|
|
a := New(nil, reg, sessionstore.NewSession(""), Options{
|
|
Hooks: hook.NewRunner(nil, root, nil, nil),
|
|
CheckTargetAccess: ws.TargetAccessCheck(),
|
|
}, event.Discard)
|
|
a.SetMutationObserver(checkpoint.NewMutationObserver(checkpoint.ObserverOptions{Store: store}))
|
|
|
|
out := a.executeOne(context.Background(), &a.turn, provider.ToolCall{
|
|
ID: "read-only-hook-gap",
|
|
Name: "read_file",
|
|
Arguments: `{"path":"note.txt"}`,
|
|
})
|
|
if out.blocked || out.errMsg != "" {
|
|
t.Fatalf("read_file failed: %+v", out)
|
|
}
|
|
metas := store.List()
|
|
if len(metas) != 1 {
|
|
t.Fatalf("checkpoint count = %d, want 1", len(metas))
|
|
}
|
|
for _, gap := range metas[0].CoverageGaps {
|
|
if gap.Reason == checkpoint.GapHookWrite {
|
|
t.Fatalf("read-only call recorded a hook-write gap: %+v", gap)
|
|
}
|
|
}
|
|
}
|
|
func TestPreToolUseRejectsBeforePreimageAndExecution(t *testing.T) {
|
|
root := testenv.TempDir(t)
|
|
path := filepath.Join(root, "note.txt")
|
|
if err := os.WriteFile(path, []byte("status=draft\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ws := builtin.Workspace{Dir: root}
|
|
probe := &accessProbeTool{Tool: ws.Tools("edit_file")[0]}
|
|
reg := tool.NewRegistry()
|
|
reg.Add(probe)
|
|
h := &stubHooks{blockPre: map[string]bool{"edit_file": true}}
|
|
prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
|
|
{toolCallChunk("c1", "edit_file", `{"path":"note.txt","old_string":"draft","new_string":"ready"}`), {Type: provider.ChunkDone}},
|
|
{{Type: provider.ChunkText, Text: "denied"}, {Type: provider.ChunkDone}},
|
|
}}
|
|
a := New(prov, reg, sessionstore.NewSession(""), Options{Hooks: h, CheckTargetAccess: ws.TargetAccessCheck()}, event.Discard)
|
|
preimages := 0
|
|
a.SetPreEditHook(func(diff.Change) { preimages++ })
|
|
if err := a.Run(context.Background(), "edit note"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(h.preSeen) != 1 || probe.previews != 1 || probe.executions != 0 || preimages != 0 {
|
|
t.Fatalf("hook refusal captured or executed a write: hooks=%v previews=%d execute=%d preimages=%d", h.preSeen, probe.previews, probe.executions, preimages)
|
|
}
|
|
if got, err := os.ReadFile(path); err != nil || string(got) != "status=draft\n" {
|
|
t.Fatalf("hook refusal changed the file: %q %v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestPreToolAccessReadScopeAndAllowedEdit(t *testing.T) {
|
|
root := testenv.TempDir(t)
|
|
outside := testenv.TempDir(t)
|
|
path := filepath.Join(outside, "note.txt")
|
|
if err := os.WriteFile(path, []byte("outside\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ws := builtin.Workspace{Dir: root, WriteRoots: []string{root, outside}, ReadRoots: []string{root}}
|
|
reg := tool.NewRegistry()
|
|
for _, tl := range ws.Tools("read_file", "edit_file") {
|
|
reg.Add(tl)
|
|
}
|
|
a := New(nil, reg, sessionstore.NewSession(""), Options{CheckTargetAccess: ws.TargetAccessCheck()}, event.Discard)
|
|
args, _ := json.Marshal(map[string]string{"path": path, "old_string": "outside", "new_string": "changed"})
|
|
out := a.executeOne(context.Background(), &a.turn, provider.ToolCall{Name: "edit_file", Arguments: string(args)})
|
|
if !out.blocked || !strings.Contains(out.output, "outside the folders this session may read") || !strings.Contains(out.output, filepath.Base(path)+"`") || out.refusalCode != builtin.CodeReadOutsideScope {
|
|
t.Fatalf("read scope not enforced: %+v", out)
|
|
}
|
|
local := filepath.Join(root, "note.txt")
|
|
if err := os.WriteFile(local, []byte("draft\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
args, _ = json.Marshal(map[string]string{"path": local, "old_string": "draft", "new_string": "ready"})
|
|
out = a.executeOne(context.Background(), &a.turn, provider.ToolCall{ID: "allowed", Name: "edit_file", Arguments: string(args)})
|
|
if out.blocked || out.errMsg != "" || out.preview == nil || !strings.Contains(out.preview.Diff, "ready") {
|
|
t.Fatalf("allowed edit failed: %+v", out)
|
|
}
|
|
if b, err := os.ReadFile(local); err != nil || string(b) != "ready\n" {
|
|
t.Fatalf("allowed edit not applied: %q %v", b, err)
|
|
}
|
|
}
|
|
|
|
type accessNestedCaller struct{ args json.RawMessage }
|
|
|
|
func (accessNestedCaller) Name() string { return "access_nested" }
|
|
func (accessNestedCaller) Description() string { return "nested access probe" }
|
|
func (accessNestedCaller) Schema() json.RawMessage { return json.RawMessage(`{"type":"object"}`) }
|
|
func (accessNestedCaller) ReadOnly() bool { return true }
|
|
func (n accessNestedCaller) Execute(ctx context.Context, _ json.RawMessage) (string, error) {
|
|
inv, _ := tool.InvokerFrom(ctx)
|
|
return inv.Invoke(ctx, "edit_file", n.args)
|
|
}
|
|
|
|
func TestPreToolAccessNestedAndBatch(t *testing.T) {
|
|
root := testenv.TempDir(t)
|
|
private := filepath.Join(root, "private")
|
|
if err := os.MkdirAll(private, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(private, "note.txt")
|
|
const original = "PRIVATE_CANARY=do-not-disclose\n"
|
|
if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ws := builtin.Workspace{Dir: root, ForbidReadRoots: []string{private}}
|
|
probe := &accessProbeTool{Tool: ws.Tools("edit_file")[0]}
|
|
reg := tool.NewRegistry()
|
|
reg.Add(probe)
|
|
args, _ := json.Marshal(map[string]string{"path": path, "old_string": "PRIVATE_CANARY", "new_string": "changed"})
|
|
reg.Add(accessNestedCaller{args: args})
|
|
a := New(nil, reg, sessionstore.NewSession(""), Options{CheckTargetAccess: ws.TargetAccessCheck()}, event.Discard)
|
|
out := a.executeOne(context.Background(), &a.turn, provider.ToolCall{Name: "access_nested", Arguments: `{}`})
|
|
if !strings.Contains(out.output, "Cannot read `") || probe.previews != 0 || probe.executions != 0 {
|
|
t.Fatalf("nested call bypassed access check: %+v previews=%d exec=%d", out, probe.previews, probe.executions)
|
|
}
|
|
public := filepath.Join(root, "public.txt")
|
|
if err := os.WriteFile(public, []byte("draft\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first, _ := json.Marshal(map[string]string{"path": public, "old_string": "draft", "new_string": "ready"})
|
|
calls := []provider.ToolCall{{ID: "first", Name: "edit_file", Arguments: string(first)}, {ID: "second", Name: "edit_file", Arguments: string(args)}}
|
|
a.sess.conversation.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: calls})
|
|
result := a.executeBatch(context.Background(), &a.turn, calls)
|
|
if !result.outcomes[1].blocked || probe.previews != 2 || probe.executions != 1 {
|
|
t.Fatalf("batch access failed: previews=%d executions=%d outcomes=%+v", probe.previews, probe.executions, result.outcomes)
|
|
}
|
|
if b, err := os.ReadFile(path); err != nil || string(b) != original {
|
|
t.Fatalf("protected file changed: %q %v", b, err)
|
|
}
|
|
}
|
|
|
|
func TestPreToolAccessChecksResolvedProxyTarget(t *testing.T) {
|
|
root := testenv.TempDir(t)
|
|
path := filepath.Join(root, "private.txt")
|
|
if err := os.WriteFile(path, []byte("INTERNAL_NOTE=do-not-disclose\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ws := builtin.Workspace{Dir: root, ForbidReadRoots: []string{path}}
|
|
probe := &accessProbeTool{Tool: ws.Tools("edit_file")[0]}
|
|
args := json.RawMessage(`{"path":"private.txt","old_string":"missing","new_string":"x"}`)
|
|
reg := tool.NewRegistry()
|
|
reg.Add(readOnlyBoundaryProxy{resolved: tool.ResolvedCall{TargetName: "edit_file", Target: probe, Args: args, ReadOnly: false}})
|
|
h := &stubHooks{}
|
|
a := New(nil, reg, sessionstore.NewSession(""), Options{Hooks: h, CheckTargetAccess: ws.TargetAccessCheck()}, event.Discard)
|
|
out := a.executeOne(context.Background(), &a.turn, provider.ToolCall{Name: "use_capability", Arguments: `{"path":"public.txt"}`})
|
|
if !out.blocked || out.refusalCode != builtin.CodeReadForbidden || probe.previews != 0 || probe.executions != 0 || len(h.preSeen) != 0 {
|
|
t.Fatalf("proxy checked its facade rather than the resolved target: %+v previews=%d execute=%d hooks=%v", out, probe.previews, probe.executions, h.preSeen)
|
|
}
|
|
}
|
|
|
|
func TestPreToolAccessRejectsForbiddenWriterAlias(t *testing.T) {
|
|
root := testenv.TempDir(t)
|
|
private := filepath.Join(root, "private")
|
|
if err := os.MkdirAll(private, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(private, "secret.txt")
|
|
if err := os.WriteFile(path, []byte("PRIVATE_CANARY\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
alias := filepath.Join(root, "alias.txt")
|
|
if err := os.Symlink(path, alias); err != nil {
|
|
t.Skipf("symlink unavailable: %v", err)
|
|
}
|
|
ws := builtin.Workspace{Dir: root, ForbidReadRoots: []string{private}}
|
|
probe := &accessProbeTool{Tool: ws.Tools("write_file")[0]}
|
|
reg := tool.NewRegistry()
|
|
reg.Add(probe)
|
|
a := New(nil, reg, sessionstore.NewSession(""), Options{CheckTargetAccess: ws.TargetAccessCheck()}, event.Discard)
|
|
out := a.executeOne(context.Background(), &a.turn, provider.ToolCall{Name: "write_file", Arguments: `{"path":"alias.txt","content":"changed"}`})
|
|
if !out.blocked || out.refusalCode != builtin.CodeOverwriteReadForbidden || probe.previews != 0 || probe.executions != 0 {
|
|
t.Fatalf("ambiguous writer alias skipped read policy: %+v previews=%d execute=%d", out, probe.previews, probe.executions)
|
|
}
|
|
}
|
|
|
|
func TestMissingTargetAccessCheckRejectsFileBeforePreview(t *testing.T) {
|
|
root := testenv.TempDir(t)
|
|
path := filepath.Join(root, "private.txt")
|
|
if err := os.WriteFile(path, []byte("PRIVATE_CANARY\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ws := builtin.Workspace{Dir: root, ForbidReadRoots: []string{path}}
|
|
probe := &accessProbeTool{Tool: ws.Tools("edit_file")[0]}
|
|
reg := tool.NewRegistry()
|
|
reg.Add(probe)
|
|
h := &stubHooks{}
|
|
a := New(nil, reg, sessionstore.NewSession(""), Options{Hooks: h}, event.Discard)
|
|
args, _ := json.Marshal(map[string]string{"path": path, "old_string": "missing", "new_string": "changed"})
|
|
out := a.executeOne(context.Background(), &a.turn, provider.ToolCall{ID: "missing-policy", Name: "edit_file", Arguments: string(args)})
|
|
if !out.blocked || out.refusalCode != "workspace.target_access_unconfigured" || probe.previews != 0 || probe.executions != 0 || len(h.preSeen) != 0 {
|
|
t.Fatalf("missing policy reached file tool: %+v previews=%d execute=%d hooks=%v", out, probe.previews, probe.executions, h.preSeen)
|
|
}
|
|
}
|