返回 DeepSeek-Reasonix
managed_config.go
根目录 / internal / tool / builtin / managed_config.go
1 package builtin
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "slices"
8 "strings"
9
10 "reasonix/internal/tool"
11 )
12
13 // ManagedConfigPaths is the set of Reasonix-owned configuration FILES a file
14 // tool may write outside the workspace roots, each write gated by a fresh
15 // human approval (see tool.ConfigWriteApprover). The zero value matches
16 // nothing, preserving plain workspace confinement. Entries are individual
17 // files, never directories: the Reasonix home also holds credentials (.env),
18 // global hooks (settings.json), skills, and session stores, which must not
19 // become writable through this escape hatch.
20 type ManagedConfigPaths struct {
21 paths []string
22 }
23
24 // NewManagedConfigPaths resolves each candidate file to an absolute,
25 // symlink-free path (mirroring realRoots), dropping empty or unresolvable
26 // entries.
27 func NewManagedConfigPaths(paths []string) ManagedConfigPaths {
28 out := make([]string, 0, len(paths))
29 for _, p := range paths {
30 if strings.TrimSpace(p) == "" {
31 continue
32 }
33 if real, err := realPath(p); err == nil {
34 out = append(out, real)
35 }
36 }
37 return ManagedConfigPaths{paths: out}
38 }
39
40 // Match reports whether target resolves to exactly one of the managed config
41 // files. Exact file equality with no case folding: this is an allow-side rule,
42 // and folding an allow rule on a case-sensitive filesystem would wave a
43 // genuinely different file through (see withinFold).
44 func (m ManagedConfigPaths) Match(target string) bool {
45 if len(m.paths) == 0 {
46 return false
47 }
48 abs, err := realPath(target)
49 if err != nil {
50 return false
51 }
52 return slices.Contains(m.paths, abs)
53 }
54
55 // approve asks the user whether this managed-config write may proceed, via the
56 // approver carried on ctx. No approver — a headless run, or a sub-agent whose
57 // parent has no interactive frontend — fails closed. The error text is written
58 // for the model: it names the boundary and the durable ways forward.
59 func (m ManagedConfigPaths) approve(ctx context.Context, target string) error {
60 approver, ok := tool.ConfigWriteApproverFrom(ctx)
61 if !ok {
62 return fmt.Errorf("path %q is a Reasonix-managed config file outside the writable roots; writing it requires interactive user approval, which this session cannot provide. "+
63 "Ask the user to retry in an interactive session, or to add the directory to [sandbox] allow_write in reasonix.toml", target)
64 }
65 req := tool.ConfigWriteRequest{Path: target}
66 if checker, ok := approver.(tool.ConfigWriteSessionChecker); ok && checker.ManagedConfigWriteSessionAllowed(ctx, req) {
67 return nil
68 }
69 allow, reason, err := approver.ApproveManagedConfigWrite(ctx, req)
70 if err != nil {
71 return err
72 }
73 if !allow {
74 if strings.TrimSpace(reason) == "" {
75 reason = "the user declined this Reasonix config write — do not retry it; ask how they would like to proceed."
76 }
77 return errors.New(reason)
78 }
79 return nil
80 }
81
81 lines GO