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