From 1474eeaaed79e9aeeaad10bb556f1f4c9cb13285 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Tue, 11 Aug 2026 17:15:01 +0200 Subject: [PATCH] olliesrv/fs: eliminate Hctx adapter struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handlers now accept their actual dependencies directly: - Root handlers: ModelCache, Broker, context.Context, etc. - Session handlers: *SessionNode, removeFn, renameFn - Agent handlers: *agent.Agent, *AgentLog, *SessionNode Ctl dispatch tables are now per-instance closures built at binding time rather than package-level vars with generic Hctx. Also inlined the intermediate internalBinding type — adapt functions now build fsedsl.Binding directly from raw data. Removed: Hctx struct, rdwrHandler type, rdwrDispatch func, bypassBindings helper, processBindings helper, internalBinding type. --- cmd/olliesrv/internal/fs/ctl.go | 30 +- cmd/olliesrv/internal/fs/handlers_agent.go | 429 +++++++++---------- cmd/olliesrv/internal/fs/handlers_root.go | 99 ++--- cmd/olliesrv/internal/fs/handlers_session.go | 117 +++-- cmd/olliesrv/internal/fs/spec.go | 296 +++++-------- 5 files changed, 420 insertions(+), 551 deletions(-) diff --git a/cmd/olliesrv/internal/fs/ctl.go b/cmd/olliesrv/internal/fs/ctl.go index 5a0d701..462921d 100644 --- a/cmd/olliesrv/internal/fs/ctl.go +++ b/cmd/olliesrv/internal/fs/ctl.go @@ -5,23 +5,17 @@ import ( "strings" ) -// rdwrHandler is a request-response ctl command handler. -type rdwrHandler func(ctx Hctx, args []string) ([]byte, error) - -// rdwrDispatch returns a Request handler that dispatches based on the first -// whitespace-delimited word in the input. -func rdwrDispatch(handlers map[string]rdwrHandler) func(Hctx, []byte) ([]byte, error) { - return func(ctx Hctx, data []byte) ([]byte, error) { - input := strings.TrimSpace(string(data)) - if input == "" { - return nil, nil - } - parts := strings.Fields(input) - cmd := parts[0] - h, ok := handlers[cmd] - if !ok { - return nil, fmt.Errorf("unknown ctl command: %s", cmd) - } - return h(ctx, parts[1:]) +// dispatch routes a command to the matching handler based on the first word. +func dispatch(handlers map[string]func([]string) ([]byte, error), data []byte) ([]byte, error) { + input := strings.TrimSpace(string(data)) + if input == "" { + return nil, nil } + parts := strings.Fields(input) + cmd := parts[0] + h, ok := handlers[cmd] + if !ok { + return nil, fmt.Errorf("unknown ctl command: %s", cmd) + } + return h(parts[1:]) } diff --git a/cmd/olliesrv/internal/fs/handlers_agent.go b/cmd/olliesrv/internal/fs/handlers_agent.go index fe6644a..75e4620 100644 --- a/cmd/olliesrv/internal/fs/handlers_agent.go +++ b/cmd/olliesrv/internal/fs/handlers_agent.go @@ -14,46 +14,45 @@ import ( "ollie/cmd/olliesrv/internal/session" ) -func requestAgentNew(ctx Hctx, data []byte) ([]byte, error) { - args := strings.Fields(string(data)) - ag, err := session.CreateAgent(ctx.Session.Name(), args) +func requestAgentNew(s *SessionNode, data []byte) ([]byte, error) { + ag, err := session.CreateAgent(s.Name(), strings.Fields(string(data))) if err != nil { return nil, fmt.Errorf("create agent: %w", err) } // Create AgentLog for this agent and wire up the event handler al := NewAgentLog("") - ctx.Session.SetAgentLog(ag.ID(), al) - wireAgentEvents(ctx.Session.ID(), ag, al) + s.SetAgentLog(ag.ID(), al) + wireAgentEvents(s.ID(), ag, al) return []byte(ag.ID() + "\n"), nil } -func writeAgentPrompt(ctx Hctx, data []byte) error { +func writeAgentPrompt(s *SessionNode, ag *agent.Agent, al *AgentLog, data []byte) error { input := strings.TrimSpace(string(data)) if input == "" { return nil } if input == "/invalidate" { - ctx.Session.InvalidateModelsCache() + s.InvalidateModelsCache() return nil } go func() { - ctx.Agent.Submit(ctx.Session.Ctx(), input) - ctx.AgentLog.EnsureTrailingNewline() + ag.Submit(s.Ctx(), input) + al.EnsureTrailingNewline() }() return nil } -func writeAgentFifoIn(ctx Hctx, data []byte) error { +func writeAgentFifoIn(ag *agent.Agent, data []byte) error { input := strings.TrimSpace(string(data)) if input == "" { return nil } - ctx.Agent.Queue(input) + ag.Queue(input) return nil } -func readAgentFifoOut(ctx Hctx) ([]byte, error) { - item, ok := ctx.Agent.PopQueue() +func readAgentFifoOut(ag *agent.Agent) ([]byte, error) { + item, ok := ag.PopQueue() if !ok { return nil, nil } @@ -61,37 +60,34 @@ func readAgentFifoOut(ctx Hctx) ([]byte, error) { } func statAgentChat() os.FileInfo { - // Note: This needs ctx but Stat doesn't receive it. - // We'll need to handle this specially or pass via closure. - // For now, return a placeholder - the real stat is done in builder. return &SyntheticFileInfo{Name_: "chat", Mode_: 0444, Size_: 64 * 1024} } -func readAgentChat(ctx Hctx) ([]byte, error) { - ctx.AgentLog.mu.RLock() - defer ctx.AgentLog.mu.RUnlock() - data := make([]byte, len(ctx.AgentLog.log)) - copy(data, ctx.AgentLog.log) +func readAgentChat(al *AgentLog) ([]byte, error) { + al.mu.RLock() + defer al.mu.RUnlock() + data := make([]byte, len(al.log)) + copy(data, al.log) return data, nil } -func readAgentChatText(ctx Hctx) ([]byte, error) { - ctx.AgentLog.mu.RLock() - defer ctx.AgentLog.mu.RUnlock() - return stripMarkers(ctx.AgentLog.log), nil +func readAgentChatText(al *AgentLog) ([]byte, error) { + al.mu.RLock() + defer al.mu.RUnlock() + return stripMarkers(al.log), nil } -func streamAgentChat(ctx Hctx, cctx context.Context, base string) ([]byte, string, error) { - merged, cancel := mergeCtx(ctx, cctx) +func streamAgentChat(s *SessionNode, al *AgentLog, cctx context.Context, base string) ([]byte, string, error) { + merged, cancel := mergeCtx(s.Ctx(), cctx) defer cancel() - return streamChat(ctx.AgentLog, merged, base) + return streamChat(al, merged, base) } -func streamAgentChatText(ctx Hctx, cctx context.Context, base string) ([]byte, string, error) { - merged, cancel := mergeCtx(ctx, cctx) +func streamAgentChatText(s *SessionNode, al *AgentLog, cctx context.Context, base string) ([]byte, string, error) { + merged, cancel := mergeCtx(s.Ctx(), cctx) defer cancel() for { - data, nextBase, err := streamChat(ctx.AgentLog, merged, base) + data, nextBase, err := streamChat(al, merged, base) if err != nil || len(data) == 0 { return data, nextBase, err } @@ -140,63 +136,63 @@ func stripMarkers(data []byte) []byte { return out } -func blockAgentStateWait(ctx Hctx, cctx context.Context, base string) ([]byte, string, error) { - merged, cancel := mergeCtx(ctx, cctx) +func blockAgentStateWait(s *SessionNode, ag *agent.Agent, cctx context.Context, base string) ([]byte, string, error) { + merged, cancel := mergeCtx(s.Ctx(), cctx) defer cancel() if base == "" { - base = ctx.Agent.State() + base = ag.State() } - v, ok := ctx.Agent.WaitChange(merged, agent.WatchState, base) + v, ok := ag.WaitChange(merged, agent.WatchState, base) if !ok { - st := ctx.Agent.State() + st := ag.State() return []byte(st + "\n"), st, nil } return []byte(v + "\n"), v, nil } -func readAgentLog(ctx Hctx) ([]byte, error) { +func readAgentLog(al *AgentLog) ([]byte, error) { const maxWindow = 64 * 1024 - ctx.AgentLog.mu.RLock() - log := ctx.AgentLog.log + al.mu.RLock() + log := al.log start := 0 if len(log) > maxWindow { start = len(log) - maxWindow } data := make([]byte, len(log)-start) copy(data, log[start:]) - ctx.AgentLog.mu.RUnlock() + al.mu.RUnlock() return data, nil } -func readAgentPlan(ctx Hctx) ([]byte, error) { - ctx.AgentLog.mu.RLock() - data := make([]byte, len(ctx.AgentLog.plan)) - copy(data, ctx.AgentLog.plan) - ctx.AgentLog.mu.RUnlock() +func readAgentPlan(al *AgentLog) ([]byte, error) { + al.mu.RLock() + data := make([]byte, len(al.plan)) + copy(data, al.plan) + al.mu.RUnlock() return data, nil } -func writeAgentPlan(ctx Hctx, data []byte) error { - ctx.AgentLog.mu.Lock() - ctx.AgentLog.plan = make([]byte, len(data)) - copy(ctx.AgentLog.plan, data) - ctx.AgentLog.mu.Unlock() +func writeAgentPlan(al *AgentLog, data []byte) error { + al.mu.Lock() + al.plan = make([]byte, len(data)) + copy(al.plan, data) + al.mu.Unlock() return nil } -func readAgentCfg(ctx Hctx) ([]byte, error) { - ctx.AgentLog.mu.RLock() - defer ctx.AgentLog.mu.RUnlock() - p := ctx.Agent.GenParams() +func readAgentCfg(s *SessionNode, ag *agent.Agent, al *AgentLog) ([]byte, error) { + al.mu.RLock() + defer al.mu.RUnlock() + p := ag.GenParams() var sb strings.Builder - fmt.Fprintf(&sb, "name=%s\n", ctx.Session.ID()) - fmt.Fprintf(&sb, "backend=%s\n", ctx.Agent.BackendName()) - fmt.Fprintf(&sb, "model=%s\n", ctx.Agent.ModelName()) - fmt.Fprintf(&sb, "id=%s\n", ctx.Agent.ID()) - fmt.Fprintf(&sb, "profile=%s\n", ctx.Agent.Profile()) - fmt.Fprintf(&sb, "displayName=%s\n", ctx.Agent.Name()) - fmt.Fprintf(&sb, "cwd=%s\n", ctx.Agent.Cwd()) - fmt.Fprintf(&sb, "remote=%s\n", ctx.AgentLog.remote) + fmt.Fprintf(&sb, "name=%s\n", s.ID()) + fmt.Fprintf(&sb, "backend=%s\n", ag.BackendName()) + fmt.Fprintf(&sb, "model=%s\n", ag.ModelName()) + fmt.Fprintf(&sb, "id=%s\n", ag.ID()) + fmt.Fprintf(&sb, "profile=%s\n", ag.Profile()) + fmt.Fprintf(&sb, "displayName=%s\n", ag.Name()) + fmt.Fprintf(&sb, "cwd=%s\n", ag.Cwd()) + fmt.Fprintf(&sb, "remote=%s\n", al.remote) fmt.Fprintf(&sb, "maxTokens=%d\n", p.MaxTokens) if p.Temperature != nil { fmt.Fprintf(&sb, "temperature=%g\n", *p.Temperature) @@ -213,7 +209,7 @@ func readAgentCfg(ctx Hctx) ([]byte, error) { return []byte(sb.String()), nil } -func writeAgentCfg(ctx Hctx, data []byte) error { +func writeAgentCfg(ag *agent.Agent, data []byte) error { input := strings.TrimSpace(string(data)) if input == "" { return nil @@ -225,7 +221,7 @@ func writeAgentCfg(ctx Hctx, data []byte) error { switch parts[0] { case "name": if strings.TrimSpace(parts[1]) != "" { - ctx.Agent.SetName(strings.TrimSpace(parts[1])) + ag.SetName(strings.TrimSpace(parts[1])) } default: return fmt.Errorf("unknown cfg key: %s", parts[0]) @@ -233,123 +229,120 @@ func writeAgentCfg(ctx Hctx, data []byte) error { return nil } -func requestAgentCtl(ctx Hctx, data []byte) ([]byte, error) { - return rdwrDispatch(agentCtlHandlers)(ctx, data) -} - -var agentCtlHandlers = map[string]rdwrHandler{ - "kill": func(ctx Hctx, _ []string) ([]byte, error) { - if ctx.Session.Session != nil { - ctx.Session.Session.RemoveAgent(ctx.Agent.ID()) - session.PublishEvent("session."+ctx.Session.ID()+".agent."+ctx.Agent.ID()+".kill", "") - } - return []byte("ok\n"), nil - }, - "stop": func(ctx Hctx, _ []string) ([]byte, error) { - ctx.Agent.Interrupt(agent.ErrInterrupted) - return []byte("ok\n"), nil - }, - "compact": func(ctx Hctx, _ []string) ([]byte, error) { - if err := ctx.Agent.Compact(ctx.Session.Ctx()); err != nil { - return nil, err - } - return []byte("ok\n"), nil - }, - "clear": func(ctx Hctx, _ []string) ([]byte, error) { - if err := ctx.Agent.Clear(); err != nil { - return nil, err - } - return []byte("ok\n"), nil - }, - "inject": func(ctx Hctx, args []string) ([]byte, error) { - text := strings.Join(args, " ") - if text == "" { - return nil, fmt.Errorf("inject requires text") - } - ctx.Agent.InjectRewrite(text) - return []byte("ok\n"), nil - }, - "i": func(ctx Hctx, args []string) ([]byte, error) { - text := strings.Join(args, " ") - if text == "" { - return nil, fmt.Errorf("inject requires text") - } - ctx.Agent.InjectRewrite(text) - return []byte("ok\n"), nil - }, - "agent": func(ctx Hctx, args []string) ([]byte, error) { - if len(args) == 0 { - return []byte(ctx.Agent.Profile() + "\n"), nil - } - if err := ctx.Agent.SwitchProfile(args[0]); err != nil { - return nil, err - } - return []byte(args[0] + "\n"), nil - }, - "model": func(ctx Hctx, args []string) ([]byte, error) { - if len(args) == 0 { - if be := ctx.Agent.Backend(); be != nil { - return []byte(be.Model() + "\n"), nil +func agentCtlHandler(s *SessionNode, ag *agent.Agent) func([]byte) ([]byte, error) { + handlers := map[string]func([]string) ([]byte, error){ + "kill": func(_ []string) ([]byte, error) { + if s.Session != nil { + s.Session.RemoveAgent(ag.ID()) + session.PublishEvent("session."+s.ID()+".agent."+ag.ID()+".kill", "") } - return nil, nil - } - if be := ctx.Agent.Backend(); be != nil { - be.SetModel(strings.Join(args, " ")) - } - return []byte(strings.Join(args, " ") + "\n"), nil - }, - "models": func(ctx Hctx, _ []string) ([]byte, error) { - return []byte(ctx.Session.CachedListModels() + "\n"), nil - }, - "tools": func(ctx Hctx, _ []string) ([]byte, error) { - return readAgentTools(ctx) - }, - "tool_load": func(ctx Hctx, args []string) ([]byte, error) { - if len(args) == 0 { - return nil, fmt.Errorf("tool_load requires a tool name") - } - ts := ctx.Agent.ToolServer() - if ts == nil { - return nil, fmt.Errorf("no tool server") - } - if err := ts.LoadTool(args[0]); err != nil { - return nil, err - } - ctx.Agent.RefreshTools() - return []byte(args[0] + "\n"), nil - }, - "backend": func(ctx Hctx, args []string) ([]byte, error) { - if len(args) == 0 { - if be := ctx.Agent.Backend(); be != nil { - return []byte(be.Name() + "\n"), nil + return []byte("ok\n"), nil + }, + "stop": func(_ []string) ([]byte, error) { + ag.Interrupt(agent.ErrInterrupted) + return []byte("ok\n"), nil + }, + "compact": func(_ []string) ([]byte, error) { + if err := ag.Compact(s.Ctx()); err != nil { + return nil, err } - return nil, nil - } - return nil, fmt.Errorf("backend switching not supported via ctl; use cfg") - }, - "name": func(ctx Hctx, args []string) ([]byte, error) { - if len(args) == 0 { - return []byte(ctx.Agent.Name() + "\n"), nil - } - ctx.Agent.SetName(strings.Join(args, " ")) - return []byte(strings.Join(args, " ") + "\n"), nil - }, - "cwd": func(ctx Hctx, args []string) ([]byte, error) { - if len(args) == 0 { - return []byte(ctx.Agent.Cwd() + "\n"), nil - } - dir := paths.ExpandHome(strings.Join(args, " ")) - if _, err := os.Stat(dir); err != nil { - return nil, fmt.Errorf("cwd: %w", err) - } - ctx.Agent.SetCWD(dir) - return []byte(dir + "\n"), nil - }, - "systemprompt": func(ctx Hctx, _ []string) ([]byte, error) { - return []byte(ctx.Agent.SystemPrompt() + "\n"), nil - }, - "help": func(_ Hctx, _ []string) ([]byte, error) { - return []byte(`stop — interrupt running agent + return []byte("ok\n"), nil + }, + "clear": func(_ []string) ([]byte, error) { + if err := ag.Clear(); err != nil { + return nil, err + } + return []byte("ok\n"), nil + }, + "inject": func(args []string) ([]byte, error) { + text := strings.Join(args, " ") + if text == "" { + return nil, fmt.Errorf("inject requires text") + } + ag.InjectRewrite(text) + return []byte("ok\n"), nil + }, + "i": func(args []string) ([]byte, error) { + text := strings.Join(args, " ") + if text == "" { + return nil, fmt.Errorf("inject requires text") + } + ag.InjectRewrite(text) + return []byte("ok\n"), nil + }, + "agent": func(args []string) ([]byte, error) { + if len(args) == 0 { + return []byte(ag.Profile() + "\n"), nil + } + if err := ag.SwitchProfile(args[0]); err != nil { + return nil, err + } + return []byte(args[0] + "\n"), nil + }, + "model": func(args []string) ([]byte, error) { + if len(args) == 0 { + if be := ag.Backend(); be != nil { + return []byte(be.Model() + "\n"), nil + } + return nil, nil + } + if be := ag.Backend(); be != nil { + be.SetModel(strings.Join(args, " ")) + } + return []byte(strings.Join(args, " ") + "\n"), nil + }, + "models": func(_ []string) ([]byte, error) { + return []byte(s.CachedListModels() + "\n"), nil + }, + "tools": func(_ []string) ([]byte, error) { + return readAgentTools(ag) + }, + "tool_load": func(args []string) ([]byte, error) { + if len(args) == 0 { + return nil, fmt.Errorf("tool_load requires a tool name") + } + ts := ag.ToolServer() + if ts == nil { + return nil, fmt.Errorf("no tool server") + } + if err := ts.LoadTool(args[0]); err != nil { + return nil, err + } + ag.RefreshTools() + return []byte(args[0] + "\n"), nil + }, + "backend": func(args []string) ([]byte, error) { + if len(args) == 0 { + if be := ag.Backend(); be != nil { + return []byte(be.Name() + "\n"), nil + } + return nil, nil + } + return nil, fmt.Errorf("backend switching not supported via ctl; use cfg") + }, + "name": func(args []string) ([]byte, error) { + if len(args) == 0 { + return []byte(ag.Name() + "\n"), nil + } + ag.SetName(strings.Join(args, " ")) + return []byte(strings.Join(args, " ") + "\n"), nil + }, + "cwd": func(args []string) ([]byte, error) { + if len(args) == 0 { + return []byte(ag.Cwd() + "\n"), nil + } + dir := paths.ExpandHome(strings.Join(args, " ")) + if _, err := os.Stat(dir); err != nil { + return nil, fmt.Errorf("cwd: %w", err) + } + ag.SetCWD(dir) + return []byte(dir + "\n"), nil + }, + "systemprompt": func(_ []string) ([]byte, error) { + return []byte(ag.SystemPrompt() + "\n"), nil + }, + "help": func(_ []string) ([]byte, error) { + return []byte(`stop — interrupt running agent compact — compact context clear — clear history inject — inject prompt mid-turn (overwrites pending) @@ -364,18 +357,22 @@ backend — show backend systemprompt — show rendered system prompt help — this message `), nil - }, + }, + } + return func(data []byte) ([]byte, error) { + return dispatch(handlers, data) + } } -func readAgentStats(ctx Hctx) ([]byte, error) { - s := "usage=" + ctx.Agent.UsageStr() + "\n" + - "cost=" + ctx.Agent.CostStr() + "\n" + - "ctxsz=" + ctx.Agent.CtxSz() + "\n" +func readAgentStats(ag *agent.Agent) ([]byte, error) { + s := "usage=" + ag.UsageStr() + "\n" + + "cost=" + ag.CostStr() + "\n" + + "ctxsz=" + ag.CtxSz() + "\n" return []byte(s), nil } -func readAgentTools(ctx Hctx) ([]byte, error) { - ts := ctx.Agent.ToolServer() +func readAgentTools(ag *agent.Agent) ([]byte, error) { + ts := ag.ToolServer() if ts == nil { return []byte("(no tool server)\n"), nil } @@ -394,38 +391,38 @@ func readAgentTools(ctx Hctx) ([]byte, error) { return []byte(sb.String()), nil } -func writeAgentTools(ctx Hctx, data []byte) error { +func writeAgentTools(ag *agent.Agent, data []byte) error { name := strings.TrimSpace(string(data)) if name == "" { return nil } - ts := ctx.Agent.ToolServer() + ts := ag.ToolServer() if ts == nil { return fmt.Errorf("no tool server") } if err := ts.LoadTool(name); err != nil { return err } - ctx.Agent.RefreshTools() + ag.RefreshTools() return nil } -func readAgentName(ctx Hctx) ([]byte, error) { - return []byte(ctx.Agent.Name() + "\n"), nil +func readAgentName(ag *agent.Agent) ([]byte, error) { + return []byte(ag.Name() + "\n"), nil } -func writeAgentName(ctx Hctx, data []byte) error { +func writeAgentName(s *SessionNode, ag *agent.Agent, data []byte) error { newName := strings.TrimSpace(string(data)) if newName == "" { return nil } - ctx.Agent.SetName(newName) - session.PersistSession(ctx.Session.Name()) + ag.SetName(newName) + session.PersistSession(s.Name()) return nil } -func readAgentID(ctx Hctx) ([]byte, error) { - return []byte(ctx.Agent.ID() + "\n"), nil +func readAgentID(ag *agent.Agent) ([]byte, error) { + return []byte(ag.ID() + "\n"), nil } func streamChat(al *AgentLog, cctx context.Context, base string) ([]byte, string, error) { @@ -459,34 +456,8 @@ func streamChat(al *AgentLog, cctx context.Context, base string) ([]byte, string return data, fmt.Sprintf("%d", newOffset), nil } -// processBindings returns bindings for detached processes. -func processBindings(ctx Hctx) ([]internalBinding, error) { - if ctx.Session == nil { - return nil, nil - } - conn := ctx.Session.DialToolServer() - if conn == nil { - return nil, nil - } - defer conn.Close() - raw := conn.ListDetachedRaw() - out := make([]internalBinding, 0, len(raw)) - for _, r := range raw { - if m, ok := r.(map[string]any); ok { - if pid, ok := m["pid"].(int); ok { - out = append(out, internalBinding{ - Name: strconv.Itoa(pid), - data: pid, - }) - } - } - } - return out, nil -} - -func readProcess(ctx Hctx) ([]byte, error) { - pid := ctx.Data.(int) - conn := ctx.Session.DialToolServer() +func readProcess(s *SessionNode, pid int) ([]byte, error) { + conn := s.DialToolServer() if conn == nil { return nil, fmt.Errorf("no tool server") } @@ -498,9 +469,8 @@ func readProcess(ctx Hctx) ([]byte, error) { return []byte(output), nil } -func readProcessStatus(ctx Hctx) ([]byte, error) { - pid := ctx.Data.(int) - conn := ctx.Session.DialToolServer() +func readProcessStatus(s *SessionNode, pid int) ([]byte, error) { + conn := s.DialToolServer() if conn == nil { return nil, fmt.Errorf("no tool server") } @@ -523,9 +493,8 @@ func readProcessStatus(ctx Hctx) ([]byte, error) { return nil, fmt.Errorf("process %d not found", pid) } -func writeProcessCtl(ctx Hctx, data []byte) error { - pid := ctx.Data.(int) - conn := ctx.Session.DialToolServer() +func writeProcessCtl(s *SessionNode, pid int, data []byte) error { + conn := s.DialToolServer() if conn == nil { return fmt.Errorf("no tool server") } diff --git a/cmd/olliesrv/internal/fs/handlers_root.go b/cmd/olliesrv/internal/fs/handlers_root.go index 55f3bb7..e753b62 100644 --- a/cmd/olliesrv/internal/fs/handlers_root.go +++ b/cmd/olliesrv/internal/fs/handlers_root.go @@ -37,7 +37,7 @@ func wireAgentEvents(sessID string, ag *agent.Agent, al *AgentLog) { }) } -func readBackends(_ Hctx) ([]byte, error) { +func readBackends() ([]byte, error) { return []byte(strings.Join(backend.Backends(), "\n") + "\n"), nil } @@ -46,7 +46,7 @@ var helpFn func() string // helpSpec stores the spec for help generation. Set during tree build. var helpSpec *fsedsl.FsNodeDecl -func readHelp(_ Hctx) ([]byte, error) { +func readHelp() ([]byte, error) { if helpFn == nil { return []byte("help not initialized\n"), nil } @@ -69,14 +69,14 @@ var ( helpText string ) -func readModels(ctx Hctx) ([]byte, error) { - if ctx.Models != nil { - return ctx.Models.Get(), nil +func readModels(mc *ModelCache) ([]byte, error) { + if mc != nil { + return mc.Get(), nil } return []byte("(no model cache)\n"), nil } -func readAgents(_ Hctx) ([]byte, error) { +func readAgents() ([]byte, error) { var sb strings.Builder for _, dir := range agent.AgentsDirs() { entries, err := os.ReadDir(dir) @@ -93,7 +93,7 @@ func readAgents(_ Hctx) ([]byte, error) { return []byte(sb.String()), nil } -func readTools(_ Hctx) ([]byte, error) { +func readTools() ([]byte, error) { tools := toolsrv.DiscoverTools() if len(tools) == 0 { return []byte("(no tools found)\n"), nil @@ -105,37 +105,38 @@ func readTools(_ Hctx) ([]byte, error) { return []byte(sb.String()), nil } -func requestRootCtl(ctx Hctx, data []byte) ([]byte, error) { - return rdwrDispatch(rootCtlHandlers)(ctx, data) +func rootCtlHandler(mc *ModelCache, invalidate func(), shutdown func()) func([]byte) ([]byte, error) { + handlers := map[string]func([]string) ([]byte, error){ + "invalidate": func(_ []string) ([]byte, error) { + if mc != nil { + mc.Invalidate() + } + if invalidate != nil { + invalidate() + } + return []byte("ok\n"), nil + }, + "kill": func(_ []string) ([]byte, error) { + if shutdown != nil { + shutdown() + } + return []byte("ok\n"), nil + }, + } + return func(data []byte) ([]byte, error) { + return dispatch(handlers, data) + } } -var rootCtlHandlers = map[string]rdwrHandler{ - "invalidate": func(ctx Hctx, _ []string) ([]byte, error) { - if ctx.Models != nil { - ctx.Models.Invalidate() - } - if ctx.Invalidate != nil { - ctx.Invalidate() - } - return []byte("ok\n"), nil - }, - "kill": func(ctx Hctx, _ []string) ([]byte, error) { - if ctx.Shutdown != nil { - ctx.Shutdown() - } - return []byte("ok\n"), nil - }, -} - -func readEventwait(_ Hctx) ([]byte, error) { +func readEventwait() ([]byte, error) { return nil, fmt.Errorf("eventwait: use blocking read") } -func blockEventwait(_ Hctx, cctx context.Context, _ string) ([]byte, string, error) { +func blockEventwait(cctx context.Context, _ string) ([]byte, string, error) { return session.WaitEvent(cctx) } -func requestGenerate(ctx Hctx, data []byte) ([]byte, error) { +func requestGenerate(ctx context.Context, data []byte) ([]byte, error) { var req backend.GenerateRequest if err := json.Unmarshal(data, &req); err != nil { req.Prompt = strings.TrimSpace(string(data)) @@ -147,7 +148,7 @@ func requestGenerate(ctx Hctx, data []byte) ([]byte, error) { return []byte(result + "\n"), nil } -func readAliases(_ Hctx) ([]byte, error) { +func readAliases() ([]byte, error) { var sb strings.Builder for name, sess := range session.Sessions() { sid := sess.ID @@ -161,48 +162,30 @@ func readAliases(_ Hctx) ([]byte, error) { var errNoBypass = fmt.Errorf("bypass broker not available") -func readBypassPolicy(ctx Hctx) ([]byte, error) { - if ctx.Bypass == nil { +func readBypassPolicy(b *bypass.Broker) ([]byte, error) { + if b == nil { return nil, errNoBypass } - p := ctx.Bypass.GlobalPolicy().Global() + p := b.GlobalPolicy().Global() return p.Marshal() } -func writeBypassPolicy(ctx Hctx, data []byte) error { - if ctx.Bypass == nil { +func writeBypassPolicy(b *bypass.Broker, data []byte) error { + if b == nil { return errNoBypass } var p bypass.Policy if err := bypass.ParsePolicy(data, &p); err != nil { return err } - return ctx.Bypass.GlobalPolicy().SetGlobal(p) + return b.GlobalPolicy().SetGlobal(p) } -// bypassBindings returns bindings for pending bypass requests. -func bypassBindings(ctx Hctx) ([]internalBinding, error) { - if ctx.Bypass == nil { - return nil, errNoBypass - } - pending := ctx.Bypass.Pending() - out := make([]internalBinding, len(pending)) - for i, r := range pending { - out[i] = internalBinding{ - Name: r.ID, - data: r, - } - } - return out, nil -} - -func readBypassRequest(ctx Hctx) ([]byte, error) { - req := ctx.Data.(*bypass.Request) +func readBypassRequest(req *bypass.Request) ([]byte, error) { return []byte(req.Summary() + "\n"), nil } -func writeBypassRequest(ctx Hctx, data []byte) error { - req := ctx.Data.(*bypass.Request) +func writeBypassRequest(b *bypass.Broker, req *bypass.Request, data []byte) error { cmd := strings.TrimSpace(string(data)) var res bypass.Resolution switch cmd { @@ -215,7 +198,7 @@ func writeBypassRequest(ctx Hctx, data []byte) error { default: return fmt.Errorf("unknown resolution: %s (use approve/deny/persist)", cmd) } - if !ctx.Bypass.Resolve(req.ID, res) { + if !b.Resolve(req.ID, res) { return fmt.Errorf("request already resolved or timed out") } return nil diff --git a/cmd/olliesrv/internal/fs/handlers_session.go b/cmd/olliesrv/internal/fs/handlers_session.go index 6d8773f..2ffa503 100644 --- a/cmd/olliesrv/internal/fs/handlers_session.go +++ b/cmd/olliesrv/internal/fs/handlers_session.go @@ -9,15 +9,15 @@ import ( "ollie/cmd/olliesrv/internal/session" ) -func readSessionIdx(_ Hctx) ([]byte, error) { +func readSessionIdx() ([]byte, error) { return session.BuildIndex(), nil } -func readAgentIdx(ctx Hctx) ([]byte, error) { - return session.BuildAgentIndex(ctx.Session.Session), nil +func readAgentIdx(sess *session.Session) ([]byte, error) { + return session.BuildAgentIndex(sess), nil } -func requestSessionNew(_ Hctx, data []byte) ([]byte, error) { +func requestSessionNew(data []byte) ([]byte, error) { args := strings.Fields(string(data)) name := "" remote := "" @@ -38,9 +38,7 @@ func requestSessionNew(_ Hctx, data []byte) ([]byte, error) { return []byte(sess.Name() + "\n"), nil } - -func readSessionEnv(ctx Hctx) ([]byte, error) { - s := ctx.Session +func readSessionEnv(s *SessionNode) ([]byte, error) { var sb strings.Builder fmt.Fprintf(&sb, "OLLIE_SESSION_ID=%s\n", s.ID()) for _, e := range os.Environ() { @@ -52,93 +50,94 @@ func readSessionEnv(ctx Hctx) ([]byte, error) { return []byte(sb.String()), nil } -func readSessionPaused(ctx Hctx) ([]byte, error) { - if ctx.Session.IsPaused() { +func readSessionPaused(s *SessionNode) ([]byte, error) { + if s.IsPaused() { return []byte("true\n"), nil } return []byte("false\n"), nil } -func readSessionConnected(ctx Hctx) ([]byte, error) { - if ctx.Session.IsConnected() { +func readSessionConnected(s *SessionNode) ([]byte, error) { + if s.IsConnected() { return []byte("true\n"), nil } return []byte("false\n"), nil } -func requestSessionCtl(ctx Hctx, data []byte) ([]byte, error) { - return rdwrDispatch(sessionCtlHandlers)(ctx, data) +func sessionCtlHandler(s *SessionNode, removeFn func(), renameFn func(string) error) func([]byte) ([]byte, error) { + handlers := map[string]func([]string) ([]byte, error){ + "kill": func(_ []string) ([]byte, error) { + removeFn() + return []byte("ok\n"), nil + }, + ".": func(_ []string) ([]byte, error) { + removeFn() + return []byte("ok\n"), nil + }, + "save": func(_ []string) ([]byte, error) { + if s.Session != nil { + s.Session.Save() + } + return []byte("ok\n"), nil + }, + "invalidate": func(_ []string) ([]byte, error) { + s.InvalidateModelsCache() + return []byte("ok\n"), nil + }, + "pause": func(_ []string) ([]byte, error) { + if err := s.Pause(); err != nil { + return nil, err + } + return []byte("ok\n"), nil + }, + "resume": func(_ []string) ([]byte, error) { + if err := s.Resume(); err != nil { + return nil, err + } + return []byte("ok\n"), nil + }, + } + return func(data []byte) ([]byte, error) { + return dispatch(handlers, data) + } } -var sessionCtlHandlers = map[string]rdwrHandler{ - "kill": func(ctx Hctx, _ []string) ([]byte, error) { - ctx.Remove() - return []byte("ok\n"), nil - }, - ".": func(ctx Hctx, _ []string) ([]byte, error) { - ctx.Remove() - return []byte("ok\n"), nil - }, - "save": func(ctx Hctx, _ []string) ([]byte, error) { - if ctx.Session.Session != nil { - ctx.Session.Session.Save() - } - return []byte("ok\n"), nil - }, - "invalidate": func(ctx Hctx, _ []string) ([]byte, error) { - ctx.Session.InvalidateModelsCache() - return []byte("ok\n"), nil - }, - "pause": func(ctx Hctx, _ []string) ([]byte, error) { - if err := ctx.Session.Pause(); err != nil { - return nil, err - } - return []byte("ok\n"), nil - }, - "resume": func(ctx Hctx, _ []string) ([]byte, error) { - if err := ctx.Session.Resume(); err != nil { - return nil, err - } - return []byte("ok\n"), nil - }, +func readSessionName(s *SessionNode) ([]byte, error) { + return []byte(s.Name() + "\n"), nil } -func readSessionName(ctx Hctx) ([]byte, error) { - return []byte(ctx.Session.Name() + "\n"), nil -} - -func writeSessionName(ctx Hctx, data []byte) error { +func writeSessionName(s *SessionNode, renameFn func(string) error, data []byte) error { newName := strings.TrimSpace(string(data)) if newName == "" { return nil } - oldName := ctx.Session.Name() + oldName := s.Name() if newName != oldName { - return ctx.Rename(newName) + return renameFn(newName) } return nil } -func readSessionID(ctx Hctx) ([]byte, error) { - return []byte(ctx.Session.ID() + "\n"), nil +func readSessionID(s *SessionNode) ([]byte, error) { + return []byte(s.ID() + "\n"), nil } -func readSessionBypass(ctx Hctx) ([]byte, error) { - if ctx.Bypass == nil { +func readSessionBypass(b *bypass.Broker, s *SessionNode) ([]byte, error) { + if b == nil { return nil, fmt.Errorf("bypass broker not available") } - p := ctx.Bypass.SessionPolicy(ctx.Session.ID()) + p := b.SessionPolicy(s.ID()) return p.Marshal() } -func writeSessionBypass(ctx Hctx, data []byte) error { - if ctx.Bypass == nil { +func writeSessionBypass(b *bypass.Broker, s *SessionNode, data []byte) error { + if b == nil { return fmt.Errorf("bypass broker not available") } var p bypass.Policy if err := bypass.ParsePolicy(data, &p); err != nil { return err } - ctx.Bypass.SetSessionPolicy(ctx.Session.ID(), p) + b.SetSessionPolicy(s.ID(), p) return nil } diff --git a/cmd/olliesrv/internal/fs/spec.go b/cmd/olliesrv/internal/fs/spec.go index 3b48d52..d7cbffd 100644 --- a/cmd/olliesrv/internal/fs/spec.go +++ b/cmd/olliesrv/internal/fs/spec.go @@ -6,32 +6,14 @@ package fs import ( "context" "os/user" + "strconv" "ollie/cmd/olliesrv/internal/agent" "ollie/cmd/olliesrv/internal/bypass" "ollie/cmd/olliesrv/internal/session" "ollie/fsedsl" - olog "ollie/log" ) -// Hctx is an internal adapter — holds scope-level dependencies for handlers -// during the incremental migration to pure closures. Will be removed. -type Hctx struct { - context.Context - RootState *RootState - Log *olog.Logger - Models *ModelCache - Bypass *bypass.Broker - Invalidate func() - Shutdown func() - Session *SessionNode - Remove func() - Rename func(string) error - Agent *agent.Agent - AgentLog *AgentLog - Data any -} - // Type aliases for fsedsl types used throughout the package. type ( Tree = fsedsl.Tree @@ -47,46 +29,14 @@ const ( PermMkdirPrivate = fsedsl.PermMkdirPrivate ) -// internalBinding is used by old handler functions during migration. -// It carries the data needed by the adapter functions in this file. -type internalBinding struct { - Name string - Aliases []string - data any -} - var treeSpecUID, treeSpecGID string // buildTreeSpec constructs the full 9P namespace as an fsedsl.FsNodeDecl. -// All handlers are closures that capture the root context (rs, log, etc.). +// All handlers are closures that capture their dependencies at binding time. func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl { - // Root-level Hctx for handlers that need it during migration. - rootCtx := func() Hctx { - return Hctx{ - Context: rs.Cfg.Ctx, - RootState: rs, - Log: rs.Cfg.Log, - Models: rs.Cfg.ModelCache, - Bypass: rs.Cfg.BypassBroker, - Shutdown: rs.Cfg.Shutdown, - Invalidate: rs.Cfg.Invalidate, - } - } - - // Helper: session-scoped Hctx - sessCtx := func(node *SessionNode) Hctx { - ctx := rootCtx() - ctx.Session = node - return ctx - } - - // Helper: agent-scoped Hctx - agentCtx := func(node *SessionNode, ag *agent.Agent, al *AgentLog) Hctx { - ctx := sessCtx(node) - ctx.Agent = ag - ctx.AgentLog = al - return ctx - } + cfg := &rs.Cfg + mc := cfg.ModelCache + broker := cfg.BypassBroker u, _ := user.Current() treeSpecUID = "ollie" @@ -101,54 +51,51 @@ func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl { fsedsl.FileNode("backends", 0444, fsedsl.Doc("Available backend names, one per line"), - fsedsl.Read(func() ([]byte, error) { return readBackends(rootCtx()) }), + fsedsl.Read(readBackends), ), fsedsl.FileNode("help", 0444, fsedsl.Doc("Filesystem reference"), - fsedsl.Read(func() ([]byte, error) { return readHelp(rootCtx()) }), + fsedsl.Read(readHelp), ), fsedsl.FileNode("models", 0444, fsedsl.Doc("Available models. Format: backendmodel per line."), - fsedsl.Read(func() ([]byte, error) { return readModels(rootCtx()) }), + fsedsl.Read(func() ([]byte, error) { return readModels(mc) }), ), fsedsl.FileNode("agents", 0444, fsedsl.Doc("Available agent profiles, one per line"), - fsedsl.Read(func() ([]byte, error) { return readAgents(rootCtx()) }), + fsedsl.Read(readAgents), ), fsedsl.FileNode("tools", 0444, fsedsl.Doc("Available tools. Format: namedescription per line."), - fsedsl.Read(func() ([]byte, error) { return readTools(rootCtx()) }), + fsedsl.Read(readTools), ), fsedsl.FileNode("ctl", 0666, fsedsl.Doc("Server control. Write: 'invalidate', 'kill'"), - fsedsl.Request(func(data []byte) ([]byte, error) { return requestRootCtl(rootCtx(), data) }), + fsedsl.Request(rootCtlHandler(mc, cfg.Invalidate, cfg.Shutdown)), ), fsedsl.FileNode("eventwait", 0444, fsedsl.Doc("Blocking read for server events"), - fsedsl.Read(func() ([]byte, error) { return readEventwait(rootCtx()) }), - fsedsl.BlockOnce(func(cctx context.Context, base string) ([]byte, string, error) { - return blockEventwait(rootCtx(), cctx, base) - }), + fsedsl.Read(readEventwait), + fsedsl.BlockOnce(blockEventwait), ), fsedsl.FileNode("generate", 0666, fsedsl.Doc("One-shot LLM generation"), - fsedsl.Request(func(data []byte) ([]byte, error) { return requestGenerate(rootCtx(), data) }), + fsedsl.Request(func(data []byte) ([]byte, error) { return requestGenerate(cfg.Ctx, data) }), ), fsedsl.FileNode("aliases", 0444, fsedsl.Doc("Alias table. Format: idpath per line."), - fsedsl.Read(func() ([]byte, error) { return readAliases(rootCtx()) }), + fsedsl.Read(readAliases), ), fsedsl.DirNode("bypass", fsedsl.Doc("Bypass broker for sandboxed command approval"), fsedsl.FileNode("policy", 0666, fsedsl.Doc("Global bypass policy"), - fsedsl.Read(func() ([]byte, error) { return readBypassPolicy(rootCtx()) }), - fsedsl.Write(func(data []byte) error { return writeBypassPolicy(rootCtx(), data) }), + fsedsl.Read(func() ([]byte, error) { return readBypassPolicy(broker) }), + fsedsl.Write(func(data []byte) error { return writeBypassPolicy(broker, data) }), ), fsedsl.Each("{reqid}", func() ([]fsedsl.Binding, error) { - ctx := rootCtx() - return adaptBypassBindings(ctx) + return adaptBypassBindings(broker) }), ), @@ -156,26 +103,21 @@ func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl { fsedsl.Doc("Session management"), fsedsl.FileNode("new", 0666, fsedsl.Doc("Create session"), - fsedsl.Request(func(data []byte) ([]byte, error) { return requestSessionNew(rootCtx(), data) }), + fsedsl.Request(requestSessionNew), ), fsedsl.FileNode("idx", 0444, fsedsl.Doc("Session index"), - fsedsl.Read(func() ([]byte, error) { return readSessionIdx(rootCtx()) }), + fsedsl.Read(readSessionIdx), ), fsedsl.Each("{sname}", func() ([]fsedsl.Binding, error) { - return buildSessionBindings(rs, rootCtx, sessCtx, agentCtx) + return buildSessionBindings(rs, broker) }), ), ) } // buildSessionBindings produces bindings for each session. -func buildSessionBindings( - rs *RootState, - rootCtx func() Hctx, - sessCtx func(*SessionNode) Hctx, - agentCtx func(*SessionNode, *agent.Agent, *AgentLog) Hctx, -) ([]fsedsl.Binding, error) { +func buildSessionBindings(rs *RootState, broker *bypass.Broker) ([]fsedsl.Binding, error) { sessions := session.Sessions() var out []fsedsl.Binding @@ -197,11 +139,11 @@ func buildSessionBindings( } out = append(out, fsedsl.Binding{ - Name: sess.Name(), - Aliases: []string{sess.ID}, - Remove: removeFn, - Rename: renameFn, - Children: buildSessionChildren(s, rs, removeFn, renameFn, rootCtx, sessCtx, agentCtx), + Name: sess.Name(), + Aliases: []string{sess.ID}, + Remove: removeFn, + Rename: renameFn, + Children: buildSessionChildren(s, rs, broker, removeFn, renameFn), }) } return out, nil @@ -211,64 +153,50 @@ func buildSessionBindings( func buildSessionChildren( s *SessionNode, rs *RootState, + broker *bypass.Broker, removeFn func(), renameFn func(string) error, - rootCtx func() Hctx, - sessCtx func(*SessionNode) Hctx, - agentCtx func(*SessionNode, *agent.Agent, *AgentLog) Hctx, ) []fsedsl.FsNodeDecl { - sc := func() Hctx { - ctx := sessCtx(s) - ctx.Remove = removeFn - ctx.Rename = renameFn - return ctx - } - return []fsedsl.FsNodeDecl{ fsedsl.FileNode("env", 0444, - fsedsl.Read(func() ([]byte, error) { return readSessionEnv(sc()) }), + fsedsl.Read(func() ([]byte, error) { return readSessionEnv(s) }), ), fsedsl.FileNode("paused", 0444, - fsedsl.Read(func() ([]byte, error) { return readSessionPaused(sc()) }), + fsedsl.Read(func() ([]byte, error) { return readSessionPaused(s) }), ), fsedsl.FileNode("connected", 0444, - fsedsl.Read(func() ([]byte, error) { return readSessionConnected(sc()) }), + fsedsl.Read(func() ([]byte, error) { return readSessionConnected(s) }), ), fsedsl.FileNode("ctl", 0666, - fsedsl.Request(func(data []byte) ([]byte, error) { return requestSessionCtl(sc(), data) }), + fsedsl.Request(sessionCtlHandler(s, removeFn, renameFn)), ), fsedsl.FileNode("name", 0666, - fsedsl.Read(func() ([]byte, error) { return readSessionName(sc()) }), - fsedsl.Write(func(data []byte) error { return writeSessionName(sc(), data) }), + fsedsl.Read(func() ([]byte, error) { return readSessionName(s) }), + fsedsl.Write(func(data []byte) error { return writeSessionName(s, renameFn, data) }), ), fsedsl.FileNode("id", 0444, - fsedsl.Read(func() ([]byte, error) { return readSessionID(sc()) }), + fsedsl.Read(func() ([]byte, error) { return readSessionID(s) }), ), fsedsl.FileNode("bypass", 0666, - fsedsl.Read(func() ([]byte, error) { return readSessionBypass(sc()) }), - fsedsl.Write(func(data []byte) error { return writeSessionBypass(sc(), data) }), + fsedsl.Read(func() ([]byte, error) { return readSessionBypass(broker, s) }), + fsedsl.Write(func(data []byte) error { return writeSessionBypass(broker, s, data) }), ), fsedsl.DirNode("agent", fsedsl.FileNode("new", 0666, - fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentNew(sc(), data) }), + fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentNew(s, data) }), ), fsedsl.FileNode("idx", 0444, - fsedsl.Read(func() ([]byte, error) { return readAgentIdx(sc()) }), + fsedsl.Read(func() ([]byte, error) { return readAgentIdx(s.Session) }), ), fsedsl.Each("{aname}", func() ([]fsedsl.Binding, error) { - return buildAgentBindings(s, rs, sessCtx, agentCtx) + return buildAgentBindings(s, rs) }), ), } } // buildAgentBindings produces bindings for each agent in a session. -func buildAgentBindings( - s *SessionNode, - rs *RootState, - sessCtx func(*SessionNode) Hctx, - agentCtx func(*SessionNode, *agent.Agent, *AgentLog) Hctx, -) ([]fsedsl.Binding, error) { +func buildAgentBindings(s *SessionNode, rs *RootState) ([]fsedsl.Binding, error) { agents := s.Session.Agents() var out []fsedsl.Binding @@ -281,8 +209,6 @@ func buildAgentBindings( wireAgentEvents(s.Session.ID, a, al) } - ac := func() Hctx { return agentCtx(s, a, al) } - out = append(out, fsedsl.Binding{ Name: a.Name(), Aliases: []string{a.ID()}, @@ -290,34 +216,34 @@ func buildAgentBindings( s.Session.RemoveAgent(a.ID()) session.PublishEvent("session."+s.ID()+".agent."+a.ID()+".kill", "") }, - Children: buildAgentChildren(a, al, s, ac), + Children: buildAgentChildren(a, al, s), }) } return out, nil } // buildAgentChildren returns the file nodes for a single agent. -func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode, ac func() Hctx) []fsedsl.FsNodeDecl { +func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode) []fsedsl.FsNodeDecl { return []fsedsl.FsNodeDecl{ fsedsl.FileNode("prompt", 0666, - fsedsl.Write(func(data []byte) error { return writeAgentPrompt(ac(), data) }), + fsedsl.Write(func(data []byte) error { return writeAgentPrompt(s, a, al, data) }), ), fsedsl.FileNode("fifo", 0666, - fsedsl.Write(func(data []byte) error { return writeAgentFifoIn(ac(), data) }), - fsedsl.Read(func() ([]byte, error) { return readAgentFifoOut(ac()) }), + fsedsl.Write(func(data []byte) error { return writeAgentFifoIn(a, data) }), + fsedsl.Read(func() ([]byte, error) { return readAgentFifoOut(a) }), ), fsedsl.FileNode("chat", 0444, fsedsl.StatOverride(statAgentChat), - fsedsl.Read(func() ([]byte, error) { return readAgentChatText(ac()) }), + fsedsl.Read(func() ([]byte, error) { return readAgentChatText(al) }), fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) { - return streamAgentChatText(ac(), cctx, base) + return streamAgentChatText(s, al, cctx, base) }), ), fsedsl.FileNode("chat.raw", 0444, fsedsl.StatOverride(statAgentChat), - fsedsl.Read(func() ([]byte, error) { return readAgentChat(ac()) }), + fsedsl.Read(func() ([]byte, error) { return readAgentChat(al) }), fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) { - return streamAgentChat(ac(), cctx, base) + return streamAgentChat(s, al, cctx, base) }), ), fsedsl.FileNode("statewait", 0444, @@ -325,68 +251,63 @@ func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode, ac func() return []byte(a.State() + "\n"), nil }), fsedsl.BlockOnce(func(cctx context.Context, base string) ([]byte, string, error) { - return blockAgentStateWait(ac(), cctx, base) + return blockAgentStateWait(s, a, cctx, base) }), ), fsedsl.FileNode("log", 0444, - fsedsl.Read(func() ([]byte, error) { return readAgentLog(ac()) }), + fsedsl.Read(func() ([]byte, error) { return readAgentLog(al) }), ), fsedsl.FileNode("plan", 0666, - fsedsl.Read(func() ([]byte, error) { return readAgentPlan(ac()) }), - fsedsl.Write(func(data []byte) error { return writeAgentPlan(ac(), data) }), + fsedsl.Read(func() ([]byte, error) { return readAgentPlan(al) }), + fsedsl.Write(func(data []byte) error { return writeAgentPlan(al, data) }), ), fsedsl.FileNode("cfg", 0666, - fsedsl.Read(func() ([]byte, error) { return readAgentCfg(ac()) }), - fsedsl.Write(func(data []byte) error { return writeAgentCfg(ac(), data) }), + fsedsl.Read(func() ([]byte, error) { return readAgentCfg(s, a, al) }), + fsedsl.Write(func(data []byte) error { return writeAgentCfg(a, data) }), ), fsedsl.FileNode("ctl", 0666, - fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentCtl(ac(), data) }), + fsedsl.Request(agentCtlHandler(s, a)), ), fsedsl.FileNode("stats", 0444, - fsedsl.Read(func() ([]byte, error) { return readAgentStats(ac()) }), + fsedsl.Read(func() ([]byte, error) { return readAgentStats(a) }), ), fsedsl.FileNode("tools", 0666, - fsedsl.Read(func() ([]byte, error) { return readAgentTools(ac()) }), - fsedsl.Write(func(data []byte) error { return writeAgentTools(ac(), data) }), + fsedsl.Read(func() ([]byte, error) { return readAgentTools(a) }), + fsedsl.Write(func(data []byte) error { return writeAgentTools(a, data) }), ), fsedsl.FileNode("name", 0666, - fsedsl.Read(func() ([]byte, error) { return readAgentName(ac()) }), - fsedsl.Write(func(data []byte) error { return writeAgentName(ac(), data) }), + fsedsl.Read(func() ([]byte, error) { return readAgentName(a) }), + fsedsl.Write(func(data []byte) error { return writeAgentName(s, a, data) }), ), fsedsl.FileNode("id", 0444, - fsedsl.Read(func() ([]byte, error) { return readAgentID(ac()) }), + fsedsl.Read(func() ([]byte, error) { return readAgentID(a) }), ), fsedsl.DirNode("proc", fsedsl.Each("{pid}", func() ([]fsedsl.Binding, error) { - return adaptProcessBindings(ac()) + return adaptProcessBindings(s) }), ), } } -// adaptBypassBindings converts old-style bypass bindings to new fsedsl.Binding. -func adaptBypassBindings(ctx Hctx) ([]fsedsl.Binding, error) { - oldBindings, err := bypassBindings(ctx) - if err != nil { - return nil, err +// adaptBypassBindings returns bindings for pending bypass requests. +func adaptBypassBindings(broker *bypass.Broker) ([]fsedsl.Binding, error) { + if broker == nil { + return nil, errNoBypass } + pending := broker.Pending() var out []fsedsl.Binding - for _, ob := range oldBindings { - b := ob - // Bypass requests are file-like (read/write), not directories + for _, r := range pending { + req := r out = append(out, fsedsl.Binding{ - Name: b.Name, + Name: req.ID, Children: []fsedsl.FsNodeDecl{ fsedsl.FileNode("request", 0666, fsedsl.Read(func() ([]byte, error) { - c := ctx - c.Data = b.data - return readBypassRequest(c) + return readBypassRequest(req) }), fsedsl.Write(func(data []byte) error { - c := ctx - c.Data = b.data - return writeBypassRequest(c, data) + return writeBypassRequest(broker, req, data) }), ), }, @@ -395,41 +316,44 @@ func adaptBypassBindings(ctx Hctx) ([]fsedsl.Binding, error) { return out, nil } -// adaptProcessBindings converts old-style process bindings to new fsedsl.Binding. -func adaptProcessBindings(ctx Hctx) ([]fsedsl.Binding, error) { - oldBindings, err := processBindings(ctx) - if err != nil { - return nil, err +// adaptProcessBindings returns bindings for detached processes. +func adaptProcessBindings(s *SessionNode) ([]fsedsl.Binding, error) { + if s == nil { + return nil, nil } + conn := s.DialToolServer() + if conn == nil { + return nil, nil + } + defer conn.Close() + raw := conn.ListDetachedRaw() var out []fsedsl.Binding - for _, ob := range oldBindings { - b := ob - out = append(out, fsedsl.Binding{ - Name: b.Name, - Children: []fsedsl.FsNodeDecl{ - fsedsl.FileNode("out", 0444, - fsedsl.Read(func() ([]byte, error) { - c := ctx - c.Data = b.data - return readProcess(c) - }), - ), - fsedsl.FileNode("ctl", 0222, - fsedsl.Write(func(data []byte) error { - c := ctx - c.Data = b.data - return writeProcessCtl(c, data) - }), - ), - fsedsl.FileNode("status", 0444, - fsedsl.Read(func() ([]byte, error) { - c := ctx - c.Data = b.data - return readProcessStatus(c) - }), - ), - }, - }) + for _, r := range raw { + if m, ok := r.(map[string]any); ok { + if id, ok := m["pid"].(int); ok { + pid := id + out = append(out, fsedsl.Binding{ + Name: strconv.Itoa(pid), + Children: []fsedsl.FsNodeDecl{ + fsedsl.FileNode("out", 0444, + fsedsl.Read(func() ([]byte, error) { + return readProcess(s, pid) + }), + ), + fsedsl.FileNode("ctl", 0222, + fsedsl.Write(func(data []byte) error { + return writeProcessCtl(s, pid, data) + }), + ), + fsedsl.FileNode("status", 0444, + fsedsl.Read(func() ([]byte, error) { + return readProcessStatus(s, pid) + }), + ), + }, + }) + } + } } return out, nil }