diff --git a/cmd/olliesrv/internal/fs/handlers_agent.go b/cmd/olliesrv/internal/fs/handlers_agent.go index 9e64640..fe6644a 100644 --- a/cmd/olliesrv/internal/fs/handlers_agent.go +++ b/cmd/olliesrv/internal/fs/handlers_agent.go @@ -27,41 +27,6 @@ func requestAgentNew(ctx Hctx, data []byte) ([]byte, error) { return []byte(ag.ID() + "\n"), nil } -// agentBindings returns bindings for all agents in a session. -// The Blueprint pattern clones the structure and binds ctx.Agent + ctx.AgentLog. -func agentBindings(ctx Hctx) ([]Binding, error) { - sess := ctx.Session - if sess == nil || sess.Session == nil { - return nil, nil - } - - var out []Binding - for _, ag := range sess.Session.Agents() { - a := ag // capture - al := sess.AgentLogFor(a.ID()) - if al == nil { - continue - } - out = append(out, Binding{ - Name: a.Name(), - Aliases: []string{a.ID()}, - UID: a.ID(), - GID: "agent", - Applier: func(c Hctx) Hctx { - c.Agent = a - c.AgentLog = al - return c - }, - Rename: func(newName string) error { - a.SetName(newName) - session.PersistSession(sess.Name()) - return nil - }, - }) - } - return out, nil -} - func writeAgentPrompt(ctx Hctx, data []byte) error { input := strings.TrimSpace(string(data)) if input == "" { @@ -495,7 +460,7 @@ func streamChat(al *AgentLog, cctx context.Context, base string) ([]byte, string } // processBindings returns bindings for detached processes. -func processBindings(ctx Hctx) ([]Binding, error) { +func processBindings(ctx Hctx) ([]internalBinding, error) { if ctx.Session == nil { return nil, nil } @@ -505,17 +470,13 @@ func processBindings(ctx Hctx) ([]Binding, error) { } defer conn.Close() raw := conn.ListDetachedRaw() - out := make([]Binding, 0, len(raw)) + 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 { - id := pid // capture - out = append(out, Binding{ - Name: strconv.Itoa(id), - Applier: func(c Hctx) Hctx { - c.Data = id - return c - }, + out = append(out, internalBinding{ + Name: strconv.Itoa(pid), + data: pid, }) } } diff --git a/cmd/olliesrv/internal/fs/handlers_root.go b/cmd/olliesrv/internal/fs/handlers_root.go index 07e8c32..55f3bb7 100644 --- a/cmd/olliesrv/internal/fs/handlers_root.go +++ b/cmd/olliesrv/internal/fs/handlers_root.go @@ -43,6 +43,9 @@ func readBackends(_ Hctx) ([]byte, error) { var helpFn func() string +// helpSpec stores the spec for help generation. Set during tree build. +var helpSpec *fsedsl.FsNodeDecl + func readHelp(_ Hctx) ([]byte, error) { if helpFn == nil { return []byte("help not initialized\n"), nil @@ -53,7 +56,9 @@ func readHelp(_ Hctx) ([]byte, error) { func init() { helpFn = func() string { helpOnce.Do(func() { - helpText = fsedsl.GenerateHelp(treeSpec) + if helpSpec != nil { + helpText = fsedsl.GenerateHelp(*helpSpec) + } }) return helpText } @@ -176,20 +181,16 @@ func writeBypassPolicy(ctx Hctx, data []byte) error { } // bypassBindings returns bindings for pending bypass requests. -func bypassBindings(ctx Hctx) ([]Binding, error) { +func bypassBindings(ctx Hctx) ([]internalBinding, error) { if ctx.Bypass == nil { return nil, errNoBypass } pending := ctx.Bypass.Pending() - out := make([]Binding, len(pending)) + out := make([]internalBinding, len(pending)) for i, r := range pending { - req := r // capture - out[i] = Binding{ + out[i] = internalBinding{ Name: r.ID, - Applier: func(c Hctx) Hctx { - c.Data = req - return c - }, + data: r, } } return out, nil diff --git a/cmd/olliesrv/internal/fs/handlers_session.go b/cmd/olliesrv/internal/fs/handlers_session.go index a05a977..6d8773f 100644 --- a/cmd/olliesrv/internal/fs/handlers_session.go +++ b/cmd/olliesrv/internal/fs/handlers_session.go @@ -38,53 +38,6 @@ func requestSessionNew(_ Hctx, data []byte) ([]byte, error) { return []byte(sess.Name() + "\n"), nil } -// sessionBindings returns bindings for all sessions. -// The Blueprint pattern clones the structure and binds ctx.Session. -func sessionBindings(ctx Hctx) ([]Binding, error) { - rs := ctx.RootState - sessions := session.Sessions() - - out := make([]Binding, 0, len(sessions)) - for name, sess := range sessions { - // Get or create cached SessionNode wrapper - node := rs.GetOrCreateNode(sess) - n := name // capture - out = append(out, Binding{ - Name: sess.Name(), - Aliases: []string{sess.ID}, - UID: sess.Uname, - GID: "agent", - Applier: func(c Hctx) Hctx { - c.Context = node.Ctx() - c.Session = node - c.Remove = func() { - session.Kill(n) - rs.RemoveNode(n) - } - c.Rename = func(newName string) error { - if err := session.Rename(n, newName); err != nil { - return err - } - rs.RenameNode(n, newName) - return nil - } - return c - }, - Remove: func() { - session.Kill(n) - rs.RemoveNode(n) - }, - Rename: func(newName string) error { - if err := session.Rename(n, newName); err != nil { - return err - } - rs.RenameNode(n, newName) - return nil - }, - }) - } - return out, nil -} func readSessionEnv(ctx Hctx) ([]byte, error) { s := ctx.Session diff --git a/cmd/olliesrv/internal/fs/newroot.go b/cmd/olliesrv/internal/fs/newroot.go index 5b06258..6c45d97 100644 --- a/cmd/olliesrv/internal/fs/newroot.go +++ b/cmd/olliesrv/internal/fs/newroot.go @@ -2,7 +2,6 @@ package fs import ( "context" - "os/user" "ollie/cmd/olliesrv/internal/bypass" "ollie/fsedsl" @@ -26,19 +25,7 @@ type Config struct { } // NewRoot creates the complete 9P filesystem tree from the spec. -// It validates, resolves ownership inheritance, wires handlers, -// populates root state, and returns the ready-to-serve *Tree. func NewRoot(cfg Config) *Tree { - // Resolve root ownership from the current system user. - u, err := user.Current() - treeSpecUID = "ollie" - treeSpecGID = "agent" - if err == nil && u != nil { - treeSpecUID = u.Username - } - treeSpec.UID = treeSpecUID // root node: owner is system user - treeSpec.GID = treeSpecGID // root node: group is "agent" - // Initialize session package session.Init(session.InitConfig{ Ctx: cfg.Ctx, @@ -55,21 +42,10 @@ func NewRoot(cfg Config) *Tree { SessionNodes: make(map[string]*SessionNode), } - // Build the root context with all available deps. - ctx := Hctx{ - Context: cfg.Ctx, // per-operation cancellation deadline - Root: nil, // set after tree is built - RootState: rs, - Log: cfg.Log, - Models: cfg.ModelCache, - Bypass: cfg.BypassBroker, - Shutdown: cfg.Shutdown, - Invalidate: cfg.Invalidate, - } - // Build the tree from the spec. - tree := fsedsl.BuildTree(treeSpec, ctx) - ctx.Root = tree + spec := buildTreeSpec(rs) + helpSpec = &spec + tree := fsedsl.BuildTree(spec) tree.Data = rs // Restore persisted sessions from disk. diff --git a/cmd/olliesrv/internal/fs/spec.go b/cmd/olliesrv/internal/fs/spec.go index d62ad20..3b48d52 100644 --- a/cmd/olliesrv/internal/fs/spec.go +++ b/cmd/olliesrv/internal/fs/spec.go @@ -1,318 +1,435 @@ package fs // Filesystem specification — the single source of truth for the 9P namespace. -// Every file and directory is declared here with Doc() descriptions. -// treeSpecUID/GID are set at NewRoot() time from the current system user. +// Constructed via buildTreeSpec with closure capture. import ( "context" + "os/user" "ollie/cmd/olliesrv/internal/agent" "ollie/cmd/olliesrv/internal/bypass" + "ollie/cmd/olliesrv/internal/session" "ollie/fsedsl" olog "ollie/log" ) -// Hctx carries runtime dependencies for handler functions. -// The wire step populates the correct subset per node scope. -// Handlers assert their scope by checking which pointer is non-nil. +// 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 // per-operation cancellation deadline - - // Root-level (always available). - Root *Tree + context.Context RootState *RootState Log *olog.Logger Models *ModelCache Bypass *bypass.Broker - Invalidate func() // called by root ctl "invalidate" - Shutdown func() // called by root ctl "kill" - - // Session scope. - Session *SessionNode - Remove func() - Rename func(string) error - - // Agent scope. - Agent *agent.Agent - AgentLog *AgentLog - - // Generic binding data — type depends on context. - Data any + Invalidate func() + Shutdown func() + Session *SessionNode + Remove func() + Rename func(string) error + Agent *agent.Agent + AgentLog *AgentLog + Data any } -// Type aliases from fsedsl, specialized to HandlerCtx. +// Type aliases for fsedsl types used throughout the package. type ( - FsNodeDecl = fsedsl.FsNodeDecl[Hctx] - Binding = fsedsl.Binding[Hctx] Tree = fsedsl.Tree File = fsedsl.File SyntheticFileInfo = fsedsl.SyntheticFileInfo ) -// Structural permissions — used by Tree for mounted children and synthetic files. +// Structural permissions. const ( PermChildDir = fsedsl.PermChildDir PermIdx = fsedsl.PermIdx PermMkdir = fsedsl.PermMkdir PermMkdirPrivate = fsedsl.PermMkdirPrivate - BindData = fsedsl.BindData ) -var ( - DirNode = fsedsl.DirNode[Hctx] - FileNode = fsedsl.FileNode[Hctx] - Each = fsedsl.Each[Hctx] - Doc = fsedsl.Doc[Hctx] - Read = fsedsl.Read[Hctx] - Write = fsedsl.Write[Hctx] - Stream = fsedsl.Stream[Hctx] - BlockOnce = fsedsl.BlockOnce[Hctx] - Request = fsedsl.Request[Hctx] - Remove = fsedsl.RemoveNode[Hctx] - Stat = fsedsl.StatOverride[Hctx] - UID = fsedsl.UID[Hctx] - GID = fsedsl.GID[Hctx] - Bind = fsedsl.Bind[Hctx] -) +// 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 -var treeSpec = DirNode("/", - GID("agent"), +// buildTreeSpec constructs the full 9P namespace as an fsedsl.FsNodeDecl. +// All handlers are closures that capture the root context (rs, log, etc.). +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, + } + } - FileNode("backends", 0444, - Doc("Available backend names, one per line (ollama, openai, anthropic, etc.)"), - Read(readBackends), + // 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 + } + + u, _ := user.Current() + treeSpecUID = "ollie" + treeSpecGID = "agent" + if u != nil { + treeSpecUID = u.Username + } + + return fsedsl.DirNode("/", + fsedsl.UID(treeSpecUID), + fsedsl.GID(treeSpecGID), + + fsedsl.FileNode("backends", 0444, + fsedsl.Doc("Available backend names, one per line"), + fsedsl.Read(func() ([]byte, error) { return readBackends(rootCtx()) }), + ), + fsedsl.FileNode("help", 0444, + fsedsl.Doc("Filesystem reference"), + fsedsl.Read(func() ([]byte, error) { return readHelp(rootCtx()) }), + ), + fsedsl.FileNode("models", 0444, + fsedsl.Doc("Available models. Format: backendmodel per line."), + fsedsl.Read(func() ([]byte, error) { return readModels(rootCtx()) }), + ), + fsedsl.FileNode("agents", 0444, + fsedsl.Doc("Available agent profiles, one per line"), + fsedsl.Read(func() ([]byte, error) { return readAgents(rootCtx()) }), + ), + fsedsl.FileNode("tools", 0444, + fsedsl.Doc("Available tools. Format: namedescription per line."), + fsedsl.Read(func() ([]byte, error) { return readTools(rootCtx()) }), + ), + fsedsl.FileNode("ctl", 0666, + fsedsl.Doc("Server control. Write: 'invalidate', 'kill'"), + fsedsl.Request(func(data []byte) ([]byte, error) { return requestRootCtl(rootCtx(), data) }), + ), + 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.FileNode("generate", 0666, + fsedsl.Doc("One-shot LLM generation"), + fsedsl.Request(func(data []byte) ([]byte, error) { return requestGenerate(rootCtx(), data) }), + ), + fsedsl.FileNode("aliases", 0444, + fsedsl.Doc("Alias table. Format: idpath per line."), + fsedsl.Read(func() ([]byte, error) { return readAliases(rootCtx()) }), ), - FileNode("help", 0444, - Doc("Filesystem reference (this output). Generated from spec."), - Read(readHelp), - ), - - FileNode("models", 0444, - Doc("Available models across all backends. Format: backendmodel per line."), - Read(readModels), - ), - - FileNode("agents", 0444, - Doc("Available agent profile names (from agents/*.json), one per line"), - Read(readAgents), - ), - - FileNode("tools", 0444, - Doc("Available tools and their descriptions. Format: namedescription per line."), - Read(readTools), - ), - - FileNode("ctl", 0666, - Doc("Server control. Write: 'invalidate' (refresh caches), 'kill' (shutdown server)"), - Request(requestRootCtl), - ), - - FileNode("eventwait", 0444, - Doc("Blocking read for server events. Returns next event as 'topic payload'."), - Read(readEventwait), - BlockOnce(blockEventwait), - ), - -FileNode("generate", 0666, - Doc("One-shot LLM generation. Write JSON {prompt,...} or plain text. Read returns result."), - Request(requestGenerate), + 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.Each("{reqid}", func() ([]fsedsl.Binding, error) { + ctx := rootCtx() + return adaptBypassBindings(ctx) + }), ), - FileNode("aliases", 0444, - Doc("Alias table for ID-based lookups. Format: idpath per line."), - Read(readAliases), - ), - - DirNode("bypass", - Doc("Bypass broker for sandboxed command approval"), - - FileNode("policy", 0666, - Doc("Global bypass policy. Read: current policy. Write: key=value to update."), - Read(readBypassPolicy), - Write(writeBypassPolicy), + fsedsl.DirNode("session", + fsedsl.Doc("Session management"), + fsedsl.FileNode("new", 0666, + fsedsl.Doc("Create session"), + fsedsl.Request(func(data []byte) ([]byte, error) { return requestSessionNew(rootCtx(), data) }), + ), + fsedsl.FileNode("idx", 0444, + fsedsl.Doc("Session index"), + fsedsl.Read(func() ([]byte, error) { return readSessionIdx(rootCtx()) }), + ), + fsedsl.Each("{sname}", func() ([]fsedsl.Binding, error) { + return buildSessionBindings(rs, rootCtx, sessCtx, agentCtx) + }), ), + ) +} - Each("{reqid}", bypassBindings, - Doc("Pending bypass request. Read: request summary. Write: 'approve', 'deny', or 'persist'."), - Bind(BindData), - Read(readBypassRequest), - Write(writeBypassRequest), +// 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) { + sessions := session.Sessions() + var out []fsedsl.Binding + + for name, sess := range sessions { + node := rs.GetOrCreateNode(sess) + s := node // capture + n := name // capture + + removeFn := func() { + session.Kill(n) + rs.RemoveNode(n) + } + renameFn := func(newName string) error { + if err := session.Rename(n, newName); err != nil { + return err + } + rs.RenameNode(n, newName) + return nil + } + + 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), + }) + } + return out, nil +} + +// buildSessionChildren returns the file nodes for a single session. +func buildSessionChildren( + s *SessionNode, + rs *RootState, + 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()) }), ), - ), - - DirNode("session", - Doc("Session management. Contains per-session subdirectories."), - - FileNode("new", 0666, - Doc("Create session. Write key=value args (name=, cwd=, agent=, backend=, model=). Read returns session name."), - Request(requestSessionNew), + fsedsl.FileNode("paused", 0444, + fsedsl.Read(func() ([]byte, error) { return readSessionPaused(sc()) }), ), - - FileNode("idx", 0444, - Doc("Session index. Format: namestatecwdbackendmodelagentNameid per line."), - Read(readSessionIdx), + fsedsl.FileNode("connected", 0444, + fsedsl.Read(func() ([]byte, error) { return readSessionConnected(sc()) }), ), - - Each("{sname}", sessionBindings, - Doc("Session directory. Addressable by name or immutable ID."), - - FileNode("env", 0444, - Doc("Session environment. OLLIE_SESSION_ID plus inherited OLLIE_* variables."), - Read(readSessionEnv), + fsedsl.FileNode("ctl", 0666, + fsedsl.Request(func(data []byte) ([]byte, error) { return requestSessionCtl(sc(), data) }), + ), + fsedsl.FileNode("name", 0666, + fsedsl.Read(func() ([]byte, error) { return readSessionName(sc()) }), + fsedsl.Write(func(data []byte) error { return writeSessionName(sc(), data) }), + ), + fsedsl.FileNode("id", 0444, + fsedsl.Read(func() ([]byte, error) { return readSessionID(sc()) }), + ), + fsedsl.FileNode("bypass", 0666, + fsedsl.Read(func() ([]byte, error) { return readSessionBypass(sc()) }), + fsedsl.Write(func(data []byte) error { return writeSessionBypass(sc(), data) }), + ), + fsedsl.DirNode("agent", + fsedsl.FileNode("new", 0666, + fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentNew(sc(), data) }), ), - - FileNode("paused", 0444, - Doc("Session pause status. Returns 'true' or 'false'."), - Read(readSessionPaused), + fsedsl.FileNode("idx", 0444, + fsedsl.Read(func() ([]byte, error) { return readAgentIdx(sc()) }), ), + fsedsl.Each("{aname}", func() ([]fsedsl.Binding, error) { + return buildAgentBindings(s, rs, sessCtx, agentCtx) + }), + ), + } +} - FileNode("connected", 0444, - Doc("RPC connection status to tool server. Returns 'true' or 'false'."), - Read(readSessionConnected), - ), +// 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) { + agents := s.Session.Agents() + var out []fsedsl.Binding - FileNode("ctl", 0666, - Doc("Session control: kill, save, invalidate, pause, resume."), - Request(requestSessionCtl), - ), + for _, ag := range agents { + a := ag + al := s.AgentLogFor(a.ID()) + if al == nil { + al = NewAgentLog(s.Session.Remote) + s.SetAgentLog(a.ID(), al) + wireAgentEvents(s.Session.ID, a, al) + } - FileNode("name", 0666, - Doc("Session display name. Read: current name. Write: rename session."), - Read(readSessionName), - Write(writeSessionName), - ), + ac := func() Hctx { return agentCtx(s, a, al) } - FileNode("id", 0444, - Doc("Immutable session ID (UUID). Does not change when session is renamed."), - Read(readSessionID), - ), + out = append(out, fsedsl.Binding{ + Name: a.Name(), + Aliases: []string{a.ID()}, + Remove: func() { + s.Session.RemoveAgent(a.ID()) + session.PublishEvent("session."+s.ID()+".agent."+a.ID()+".kill", "") + }, + Children: buildAgentChildren(a, al, s, ac), + }) + } + return out, nil +} - FileNode("bypass", 0666, - Doc("Session-scoped bypass policy. Overrides global policy for this session."), - Read(readSessionBypass), - Write(writeSessionBypass), - ), +// buildAgentChildren returns the file nodes for a single agent. +func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode, ac func() Hctx) []fsedsl.FsNodeDecl { + return []fsedsl.FsNodeDecl{ + fsedsl.FileNode("prompt", 0666, + fsedsl.Write(func(data []byte) error { return writeAgentPrompt(ac(), data) }), + ), + fsedsl.FileNode("fifo", 0666, + fsedsl.Write(func(data []byte) error { return writeAgentFifoIn(ac(), data) }), + fsedsl.Read(func() ([]byte, error) { return readAgentFifoOut(ac()) }), + ), + fsedsl.FileNode("chat", 0444, + fsedsl.StatOverride(statAgentChat), + fsedsl.Read(func() ([]byte, error) { return readAgentChatText(ac()) }), + fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) { + return streamAgentChatText(ac(), cctx, base) + }), + ), + fsedsl.FileNode("chat.raw", 0444, + fsedsl.StatOverride(statAgentChat), + fsedsl.Read(func() ([]byte, error) { return readAgentChat(ac()) }), + fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) { + return streamAgentChat(ac(), cctx, base) + }), + ), + fsedsl.FileNode("statewait", 0444, + fsedsl.Read(func() ([]byte, error) { + return []byte(a.State() + "\n"), nil + }), + fsedsl.BlockOnce(func(cctx context.Context, base string) ([]byte, string, error) { + return blockAgentStateWait(ac(), cctx, base) + }), + ), + fsedsl.FileNode("log", 0444, + fsedsl.Read(func() ([]byte, error) { return readAgentLog(ac()) }), + ), + fsedsl.FileNode("plan", 0666, + fsedsl.Read(func() ([]byte, error) { return readAgentPlan(ac()) }), + fsedsl.Write(func(data []byte) error { return writeAgentPlan(ac(), data) }), + ), + fsedsl.FileNode("cfg", 0666, + fsedsl.Read(func() ([]byte, error) { return readAgentCfg(ac()) }), + fsedsl.Write(func(data []byte) error { return writeAgentCfg(ac(), data) }), + ), + fsedsl.FileNode("ctl", 0666, + fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentCtl(ac(), data) }), + ), + fsedsl.FileNode("stats", 0444, + fsedsl.Read(func() ([]byte, error) { return readAgentStats(ac()) }), + ), + fsedsl.FileNode("tools", 0666, + fsedsl.Read(func() ([]byte, error) { return readAgentTools(ac()) }), + fsedsl.Write(func(data []byte) error { return writeAgentTools(ac(), data) }), + ), + fsedsl.FileNode("name", 0666, + fsedsl.Read(func() ([]byte, error) { return readAgentName(ac()) }), + fsedsl.Write(func(data []byte) error { return writeAgentName(ac(), data) }), + ), + fsedsl.FileNode("id", 0444, + fsedsl.Read(func() ([]byte, error) { return readAgentID(ac()) }), + ), + fsedsl.DirNode("proc", + fsedsl.Each("{pid}", func() ([]fsedsl.Binding, error) { + return adaptProcessBindings(ac()) + }), + ), + } +} - DirNode("agent", - Doc("Agent management within this session."), - - FileNode("new", 0666, - Doc("Create agent. Write key=value args (name=, profile=). Read returns agent ID."), - Request(requestAgentNew), - ), - - FileNode("idx", 0444, - Doc("Agent index. Format: nameidcwdbackendmodel per line."), - Read(readAgentIdx), - ), - - Each("{aname}", agentBindings, - Doc("Agent directory. Addressable by name or immutable ID."), - - FileNode("prompt", 0666, - Doc("Submit prompt to agent. Write: prompt text (triggers async execution). Special: '/invalidate' refreshes model cache."), - Write(writeAgentPrompt), - ), - - FileNode("fifo", 0666, - Doc("Prompt queue. Write: enqueue. Read: dequeue (empty if none)."), - Write(writeAgentFifoIn), - Read(readAgentFifoOut), - ), - - FileNode("chat", 0444, - Doc("Filtered text presentation of conversation. Strips markers and source fences for terminal display."), - Stat(statAgentChat), - Read(readAgentChatText), - Stream(streamAgentChatText), - ), - - FileNode("chat.raw", 0444, - Doc("Full conversation with block markers and fences. Used by GUIs."), - Stat(statAgentChat), - Read(readAgentChat), - Stream(streamAgentChat), - ), - - FileNode("statewait", 0444, - Doc("Blocking read that returns when state changes. Returns new state value."), - Read(func(ctx Hctx) ([]byte, error) { - return []byte(ctx.Agent.State() + "\n"), nil +// 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 + } + var out []fsedsl.Binding + for _, ob := range oldBindings { + b := ob + // Bypass requests are file-like (read/write), not directories + out = append(out, fsedsl.Binding{ + Name: b.Name, + Children: []fsedsl.FsNodeDecl{ + fsedsl.FileNode("request", 0666, + fsedsl.Read(func() ([]byte, error) { + c := ctx + c.Data = b.data + return readBypassRequest(c) + }), + fsedsl.Write(func(data []byte) error { + c := ctx + c.Data = b.data + return writeBypassRequest(c, data) }), - BlockOnce(blockAgentStateWait), ), + }, + }) + } + return out, nil +} - FileNode("log", 0444, - Doc("Agent activity log (last 64KB). Debug output and internal events."), - Read(readAgentLog), - ), - - FileNode("plan", 0666, - Doc("Session-scoped markdown checklist. Survives context compaction. Read/write."), - Read(readAgentPlan), - Write(writeAgentPlan), - ), - - FileNode("cfg", 0666, - Doc("Agent configuration. Read: all settings. Write: key=value to update (name=, backend=, model=)."), - Read(readAgentCfg), - Write(writeAgentCfg), - ), - - FileNode("ctl", 0666, - Doc("Agent control (rdwr). Write command, read response. Commands: stop, compact, clear, inject, agent, model, cwd, name."), - Request(requestAgentCtl), - ), - - FileNode("stats", 0444, - Doc("Agent statistics. Key=value lines: usage=, cost=, ctxsz=."), - Read(readAgentStats), - ), - - FileNode("tools", 0666, - Doc("Loaded tools. Read: list. Write: tool name to load."), - Read(readAgentTools), - Write(writeAgentTools), - ), - - FileNode("name", 0666, - Doc("Agent display name. Read: current name. Write: rename agent."), - Read(readAgentName), - Write(writeAgentName), - ), - - FileNode("id", 0444, - Doc("Immutable agent ID. Does not change when agent is renamed."), - Read(readAgentID), - ), - - DirNode("proc", - Doc("Detached background processes started by this agent."), - - Each("{pid}", processBindings, - Doc("Background process. Directory contains out, ctl, status."), - Bind(BindData), - - FileNode("out", 0444, - Doc("Process output buffer."), - Read(readProcess), - ), - FileNode("ctl", 0222, - Doc("Process control. Write: term, kill, signal , dismiss."), - Write(writeProcessCtl), - ), - FileNode("status", 0444, - Doc("Process status: running or exited ."), - Read(readProcessStatus), - ), - ), - ), +// 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 + } + 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) + }), + ), + }, + }) + } + return out, nil +}