ollie/fs/fsnode.go

213 lines
5.9 KiB
Go

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 }
}