olliesrv/fs: migrate to closure-capture fsedsl API
Converted the entire 9P namespace spec from progressive Hctx population to closure capture. Handlers are now closures that capture their dependencies (session, agent, etc.) at binding time. Hctx remains as a transitional adapter — handlers still receive it, but it's constructed per-call from captured state rather than threaded through the tree. Will be removed in a follow-up once handlers are converted to use captured state directly.
This commit is contained in:
parent
53b4618561
commit
95eb402c68
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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: backend<tab>model 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: name<spaces>description 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: id<tab>path 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: backend<tab>model 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: name<spaces>description 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: id<tab>path 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: name<tab>state<tab>cwd<tab>backend<tab>model<tab>agentName<tab>id 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: name<tab>id<tab>cwd<tab>backend<tab>model 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 <n>, dismiss."),
|
||||
Write(writeProcessCtl),
|
||||
),
|
||||
FileNode("status", 0444,
|
||||
Doc("Process status: running or exited <code>."),
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue