package fs import ( "context" "os" "strings" "sync" "ollie/agent" "ollie/backend" "ollie/elevate" olog "ollie/log" "ollie/skills" "ollie/toolsrv" ) // ModelCache caches the model list from all backends. // Created by NewRoot and used by the /models handler. // Also invalidated by root-level /ctl. type ModelCache struct { mu sync.Mutex data []byte fetched bool } func NewModelCache() *ModelCache { return &ModelCache{} } func (c *ModelCache) Get() []byte { c.mu.Lock() if !c.fetched { c.mu.Unlock() c.refresh() c.mu.Lock() } defer c.mu.Unlock() return c.data } func (c *ModelCache) Invalidate() { c.mu.Lock() c.fetched = false c.data = nil c.mu.Unlock() } func (c *ModelCache) refresh() { var sb strings.Builder for _, name := range backend.Backends() { b, err := backend.NewWithName(name) if err != nil { continue } for _, m := range b.Models(context.Background()) { sb.WriteString(name) sb.WriteByte('\t') sb.WriteString(m) sb.WriteByte('\n') } } c.mu.Lock() c.data = []byte(sb.String()) c.fetched = true c.mu.Unlock() } // HandlerCtx 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. type HandlerCtx struct { // Root-level (always available). Root *Tree Log *olog.Logger ToolReg *toolsrv.Registry Skills *skills.Registry Models *ModelCache Elevate *elevate.Broker Invalidate func() // called by root ctl "invalidate" Shutdown func() // called by root ctl "kill" // Session scope. Session *Session Kill func() Rename func(string) error SaveFn func([]byte) error // Agent scope. Agent *agent.Agent AgentLog *AgentLog } // BlockFn is the signature for blocking/streaming reads. type BlockFn func(ctx context.Context, base string) ([]byte, string, error) // Structural permissions — used by Tree for mounted children and synthetic files. const ( PermChildDir os.FileMode = 0555 PermIdx os.FileMode = 0444 PermMkdir os.FileMode = 0755 PermMkdirPrivate os.FileMode = 0700 ) // ListFn resolves dynamic directory children at runtime. type ListFn func(ctx HandlerCtx) ([]FsNodeDecl, error) // FsNodeDecl describes one node in the 9P namespace. // The entire filesystem is a single recursive tree of these. type FsNodeDecl struct { // Identity. Name string UID string // "" = inherit from parent GID string // "" = inherit from parent Mode os.FileMode // 0 = inherit parent default // Tstat — metadata override (non-nil bypasses Name/UID/GID/Mode/size defaulting). Stat func() os.FileInfo // Twalk — children (implies directory; at most one set). Children []FsNodeDecl // static, known at compile time List func(HandlerCtx) ([]FsNodeDecl, error) // dynamic, resolved at runtime // Tread (non-blocking) + Twrite. Read func(ctx HandlerCtx) ([]byte, error) Write func(ctx HandlerCtx, data []byte) error // Tread — blocking variants (at most one set). BlockOnce func(ctx HandlerCtx, cctx context.Context, base string) ([]byte, string, error) Stream func(ctx HandlerCtx, cctx context.Context, base string) ([]byte, string, error) // Request — rdwr: write triggers result, read returns it once. Request func(ctx HandlerCtx, data []byte) ([]byte, error) // Tcreate + Tremove + Trename (directory ops). Create func(ctx HandlerCtx, name string) error Remove func(ctx HandlerCtx) error Rename func(newName string) error } // NodeOption configures a FsNodeDecl at construction time. type NodeOption func(*FsNodeDecl) // ── EDSL constructors ──────────────────────────────────────────── // Dir declares a static directory node, optionally with NodeOptions (GID, UID). func Dir(name string, args ...any) FsNodeDecl { d := FsNodeDecl{Name: name} for _, a := range args { switch v := a.(type) { case FsNodeDecl: d.Children = append(d.Children, v) case NodeOption: v(&d) } } return d } // TemplateDir declares a dynamic directory node whose children are // resolved at runtime by list. The list function is a factory that // returns fully-built subtrees — no blueprint children needed here. func TemplateDir(name string, list ListFn) FsNodeDecl { return FsNodeDecl{Name: name, List: list} } // Leaf declares a file node (non-directory). func Leaf(name string, mode os.FileMode, opts ...NodeOption) FsNodeDecl { d := FsNodeDecl{Name: name, Mode: mode} for _, o := range opts { o(&d) } return d } // ── Options ────────────────────────────────────────────────────── func Read(fn func(HandlerCtx) ([]byte, error)) NodeOption { return func(d *FsNodeDecl) { d.Read = fn } } func Write(fn func(HandlerCtx, []byte) error) NodeOption { return func(d *FsNodeDecl) { d.Write = fn } } func Stream(fn func(HandlerCtx, context.Context, string) ([]byte, string, error)) NodeOption { return func(d *FsNodeDecl) { d.Stream = fn } } func BlockOnce(fn func(HandlerCtx, context.Context, string) ([]byte, string, error)) NodeOption { return func(d *FsNodeDecl) { d.BlockOnce = fn } } func Request(fn func(HandlerCtx, []byte) ([]byte, error)) NodeOption { return func(d *FsNodeDecl) { d.Request = fn } } func CreateFile(fn func(HandlerCtx, string) error) NodeOption { return func(d *FsNodeDecl) { d.Create = fn } } func Remove(fn func(HandlerCtx) error) NodeOption { return func(d *FsNodeDecl) { d.Remove = fn } } func Stat(fn func() os.FileInfo) NodeOption { return func(d *FsNodeDecl) { d.Stat = fn } } func UID(uid string) NodeOption { return func(d *FsNodeDecl) { d.UID = uid } } func GID(gid string) NodeOption { return func(d *FsNodeDecl) { d.GID = gid } }