mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
Enforce workspace read policy before previews, diagnostics, hooks, and file execution. Provide admitted previews to permission approval and Auto Guard, then capture fresh preimages after PreToolUse. Share target access checks across execution, planning, review, and delegated agents. Keep genuinely write-only targets available, report existing-file overwrite refusals with write-specific messages, and avoid hook-write coverage gaps for read-only calls. Preserve network-path protection before filesystem lookup while allowing authorized network roots and registered folder references. Include baseline-compatible disclosure regressions, production target-access wiring checks, and localized tool-card tests.
151 lines
4.3 KiB
Go
151 lines
4.3 KiB
Go
package boot
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"reasonix/internal/base/fileutil"
|
|
"reasonix/internal/contract/config"
|
|
"reasonix/internal/contract/event"
|
|
"reasonix/internal/contract/provider"
|
|
"reasonix/internal/session/control"
|
|
"reasonix/internal/tools/builtin"
|
|
)
|
|
|
|
// writeScopeProvider asks for one write_file outside the workspace, then stops.
|
|
type writeScopeProvider struct {
|
|
mu sync.Mutex
|
|
target string
|
|
turn int
|
|
reqs []provider.Request
|
|
}
|
|
|
|
func (p *writeScopeProvider) Name() string { return "boot-write-scope" }
|
|
|
|
func (p *writeScopeProvider) Stream(_ context.Context, req provider.Request) (<-chan provider.Chunk, error) {
|
|
p.mu.Lock()
|
|
p.reqs = append(p.reqs, req)
|
|
p.turn++
|
|
turn := p.turn
|
|
p.mu.Unlock()
|
|
ch := make(chan provider.Chunk, 2)
|
|
if turn == 1 {
|
|
ch <- provider.Chunk{Type: provider.ChunkToolCall, ToolCall: &provider.ToolCall{
|
|
ID: "call-write", Name: "write_file",
|
|
Arguments: `{"path":` + jsonString(p.target) + `,"content":"x\n"}`,
|
|
}}
|
|
} else {
|
|
ch <- provider.Chunk{Type: provider.ChunkText, Text: "done"}
|
|
}
|
|
ch <- provider.Chunk{Type: provider.ChunkDone}
|
|
close(ch)
|
|
return ch, nil
|
|
}
|
|
|
|
var (
|
|
writeScopeRegister sync.Once
|
|
writeScopeMu sync.Mutex
|
|
writeScopeCurrent *writeScopeProvider
|
|
)
|
|
|
|
// A write outside the workspace is refused by the file tools' write scope, on
|
|
// every platform. With bash unconfined — the only mode Windows has — nothing
|
|
// about the refusal may be attributed to an OS sandbox: the model and the
|
|
// frontend both receive the workspace cause, the path, and the setting.
|
|
func TestEffectWriteOutsideWorkspaceNamesTheWriteScope(t *testing.T) {
|
|
if got := config.Default().BashModeForGOOS("windows"); got != "off" {
|
|
t.Fatalf("the Windows posture this test stands in for is bash %q, want off", got)
|
|
}
|
|
for _, bash := range []string{"off", "enforce"} {
|
|
t.Run("bash="+bash, func(t *testing.T) {
|
|
isolateConfigHome(t)
|
|
dir := robustTempDir(t)
|
|
outside := robustTempDir(t)
|
|
t.Chdir(dir)
|
|
target := filepath.Join(outside, "notes.md")
|
|
rec := &writeScopeProvider{target: target}
|
|
writeScopeRegister.Do(func() {
|
|
provider.Register("boot-write-scope", func(provider.Config) (provider.Provider, error) {
|
|
writeScopeMu.Lock()
|
|
defer writeScopeMu.Unlock()
|
|
return writeScopeCurrent, nil
|
|
})
|
|
})
|
|
writeScopeMu.Lock()
|
|
writeScopeCurrent = rec
|
|
writeScopeMu.Unlock()
|
|
writeUserConfig(t, "[sandbox]\nbash = \""+bash+"\"\n")
|
|
writeFile(t, dir, "reasonix.toml", `
|
|
default_model = "test-model"
|
|
|
|
[codegraph]
|
|
enabled = false
|
|
|
|
[[providers]]
|
|
name = "test-model"
|
|
kind = "boot-write-scope"
|
|
model = "x"
|
|
`)
|
|
approveWorkspace(t, dir)
|
|
var mu sync.Mutex
|
|
var results []event.Tool
|
|
sink := event.FuncSink(func(e event.Event) {
|
|
if e.Kind == event.ToolResult && e.Tool.Name == "write_file" {
|
|
mu.Lock()
|
|
results = append(results, e.Tool)
|
|
mu.Unlock()
|
|
}
|
|
})
|
|
ctrl, err := Build(context.Background(), Options{Sink: sink})
|
|
if err != nil {
|
|
t.Fatalf("Build: %v", err)
|
|
}
|
|
defer ctrl.Close()
|
|
ctrl.SetToolApprovalMode(control.ToolApprovalYolo)
|
|
_ = ctrl.Run(context.Background(), "write the notes")
|
|
|
|
if _, err := os.Stat(target); !os.IsNotExist(err) {
|
|
t.Fatalf("a write outside the workspace landed (stat err %v); the scope must not loosen", err)
|
|
}
|
|
rec.mu.Lock()
|
|
last := rec.reqs[len(rec.reqs)-1]
|
|
rec.mu.Unlock()
|
|
got := effectToolResults(last)
|
|
if len(got) == 0 {
|
|
t.Fatal("no tool result reached the provider")
|
|
}
|
|
model := got[len(got)-1]
|
|
canonicalTarget, err := fileutil.CanonicalWritePath(target)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"workspace write scope",
|
|
"not an OS sandbox",
|
|
"`" + canonicalTarget + "`",
|
|
"allow_write",
|
|
} {
|
|
if !strings.Contains(model, want) {
|
|
t.Errorf("model-visible refusal lacks %q:\n%s", want, model)
|
|
}
|
|
}
|
|
if strings.Contains(model, "confined") || strings.Contains(model, "widen [sandbox]") {
|
|
t.Errorf("model-visible refusal still reads as a sandbox confinement:\n%s", model)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(results) != 1 {
|
|
t.Fatalf("want one write_file result at the frontend sink, got %d", len(results))
|
|
}
|
|
if code := results[0].RefusalCode; code != builtin.CodeWriteOutsideScope {
|
|
t.Fatalf("frontend refusal code = %q, want %q", code, builtin.CodeWriteOutsideScope)
|
|
}
|
|
})
|
|
}
|
|
}
|