返回 DeepSeek-Reasonix
write_access.go
根目录 / internal / boot / write_access.go
1 package boot
2
3 import (
4 "context"
5 "fmt"
6 "io"
7 "os"
8
9 "reasonix/internal/ablation"
10 "reasonix/internal/agent"
11 "reasonix/internal/config"
12 "reasonix/internal/control"
13 "reasonix/internal/event"
14 "reasonix/internal/hook"
15 "reasonix/internal/lsp"
16 "reasonix/internal/provider"
17 "reasonix/internal/sandbox"
18 "reasonix/internal/skill"
19 "reasonix/internal/tool"
20 "reasonix/internal/workspacelease"
21 )
22
23 func newSubagentSkillOptionsFactory(
24 cfg config.AgentConfig,
25 quoteCtx *event.QuoteContext,
26 gate agent.Gate,
27 keepPolicy agent.KeepPolicy,
28 maxDepth int,
29 ablationSet ablation.Set,
30 lease *workspacelease.Owner,
31 writeRoots *sandbox.WritableRootSet,
32 hookRunner *hook.Runner,
33 imageRoutes ...childImageRouting,
34 ) func(context.Context, int, *provider.Pricing, int, int) agent.Options {
35 home, stateRoot := userHomeDir(), config.MemoryUserDir()
36 return func(ctx context.Context, steps int, price *provider.Pricing, ctxWin, childDepth int) agent.Options {
37 opts := agent.Options{
38 MaxSteps: steps, Temperature: cfg.Temperature, Pricing: price, QuoteContext: quoteCtx, UsageSource: event.UsageSourceSubagent,
39 Gate: gate, ContextWindow: ctxWin, RecentKeep: cfg.RecentKeep,
40 SoftCompactRatio: cfg.SoftCompactRatio, ToolResultSnipRatio: cfg.ToolResultSnipRatio,
41 CompactRatio: cfg.CompactRatio, CompactForceRatio: cfg.CompactForceRatio, ContextEditing: cfg.ContextEditing,
42 ArchiveDir: config.ArchiveDir(), KeepPolicy: keepPolicy,
43 ResponseLanguage: agent.ResponseLanguageFromContext(ctx), ReasoningLanguage: agent.ReasoningLanguageFromContext(ctx),
44 SubagentDepth: childDepth, MaxSubagentDepth: maxDepth,
45 Ablation: ablationSet, WorkspaceLease: lease, WriteRoots: writeRoots,
46 DisableWriteAccessExpand: true, HomeDir: home, StateRoot: stateRoot,
47 }
48 if len(imageRoutes) > 0 {
49 opts.ImageRequestResolver = imageRoutes[0].controller()
50 opts.ImageInput = imageRoutes[0].config
51 }
52 callID, _, _, _ := agent.CallContext(ctx)
53 opts.Hooks = hookRunner.ForSession("subagent:" + callID)
54 return opts
55 }
56 }
57
58 func newBootHookRunner(resolved []hook.ResolvedHook, root string, shell sandbox.Shell, sink event.Sink) *hook.Runner {
59 if held, pending := hook.PendingProjectHooks(hook.LoadOptions{ProjectRoot: root}); pending {
60 sink.Emit(projectHooksHeldEvent(held))
61 }
62 return newHookRunner(resolved, root, shell,
63 func(msg string) { sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelWarn, Text: msg}) })
64 }
65
66 // NewCommandHookRunner resolves hooks for a command that drives an agent
67 // without Build, from the sources load names; hooks run in load.ProjectRoot.
68 // Resolving shellCfg may execute its path, so it must come from a source the
69 // command trusts as much as the hooks themselves. Warnings go to warn.
70 func NewCommandHookRunner(shellCfg config.ShellConfig, load hook.LoadOptions, warn io.Writer) *hook.Runner {
71 shell := sandbox.ResolveShell(shellCfg.Prefer, shellCfg.Path, warn)
72 return newHookRunner(hook.Load(load), load.ProjectRoot, shell,
73 func(msg string) { _, _ = fmt.Fprintln(warn, msg) })
74 }
75
76 func newHookRunner(resolved []hook.ResolvedHook, root string, shell sandbox.Shell, notify func(string)) *hook.Runner {
77 runtime := hook.RuntimeOptions{}
78 if shell.Kind == sandbox.ShellBash {
79 runtime.BashPath = shell.Path
80 }
81 return hook.NewRunner(resolved, root, hook.NewDefaultSpawner(runtime), notify)
82 }
83
84 func reviewSubagentSkillOptions(
85 ctx context.Context,
86 profile, task string,
87 steps int,
88 price *provider.Pricing,
89 ctxWin, childDepth int,
90 factory func(context.Context, int, *provider.Pricing, int, int) agent.Options,
91 ) (string, agent.Options) {
92 reviewTokens := 0
93 if reviewTask, tokens, ok := agent.PrepareReviewSubagentContext(ctx, profile, task); ok {
94 task, reviewTokens = reviewTask, tokens
95 }
96 opts := factory(ctx, steps, price, ctxWin, childDepth)
97 if reviewTokens > 0 {
98 opts.MaxOutputTokens = reviewTokens
99 }
100 return task, opts
101 }
102
103 func skillSubagentRegistry(
104 sk skill.Skill,
105 parent *tool.Registry,
106 childDepth, maxDepth int,
107 runtime *agent.MCPCapabilityRuntime,
108 writeRoots *sandbox.WritableRootSet,
109 ) (*tool.Registry, *sandbox.WritableRootSet) {
110 if sk.ReadOnly {
111 return agent.ReadOnlySubagentToolRegistryForDepthWithRuntime(parent, sk.AllowedTools, childDepth, maxDepth, runtime), nil
112 }
113 reg := agent.SubagentToolRegistryForDepthWithRuntime(parent, sk.AllowedTools, childDepth, maxDepth, runtime)
114 return agent.BindChildWriteRoots(reg, writeRoots, agent.WritePathSet{})
115 }
116
117 func projectWriteAccessPersister(root string) control.PersistWriteAccessFunc {
118 return func(dirs []string, permRule string) error {
119 return config.PersistWorkspaceWriteAccess(config.ReasonixHomeDir(), root, dirs, permRule)
120 }
121 }
122
123 func userHomeDir() string {
124 home, _ := os.UserHomeDir()
125 return home
126 }
127
128 // projectHooksHeldEvent says which project hooks did not load and how to let them.
129 func projectHooksHeldEvent(p config.ProjectProgram) event.Event {
130 return event.Event{
131 Kind: event.Notice,
132 Level: event.LevelWarn,
133 Text: "This project's hooks are off until you approve them.",
134 Detail: fmt.Sprintf("%s declares: %s\nRun `reasonix trust` in this workspace to review and approve them; any later change needs approval again.", p.Name, p.Detail),
135 }
136 }
137
138 // verifiedLSPSpecs checks a workspace-declared server against its approval
139 // before every start, so a file it names that changed mid-session is refused.
140 func verifiedLSPSpecs(cfg *config.Config) map[string]lsp.ServerSpec {
141 specs := LSPSpecs(cfg.LSP)
142 for lang, spec := range specs {
143 spec.Verify = cfg.ProjectProgramVerifier(config.ProjectProgramLSP, lang)
144 specs[lang] = spec
145 }
146 return specs
147 }
148
148 lines GO