ollie/fs/sessionfiles.go

203 lines
5.5 KiB
Go

package fs
import (
"fmt"
"os"
"strings"
"ollie/elevate"
)
// ── listSessions — factory for per-session subtrees ─────────────
func listSessions(ctx HandlerCtx) ([]FsNodeDecl, error) {
rs := globalRootState
rs.mu.RLock()
defer rs.mu.RUnlock()
var out []FsNodeDecl
for _, sess := range rs.sessions {
s := sess // capture for closures
kill := func() { Kill(rs, s.Name()) }
rename := func(newID string) error { return Rename(rs, s.Name(), newID) }
scoped := ctx
scoped.Session = s
scoped.Kill = kill
scoped.Rename = rename
sessionDir := Dir(s.Name(),
UID(s.Uname()),
GID("agent"),
// env — session environment variables
Leaf("env", 0444, Read(func(_ HandlerCtx) ([]byte, error) {
return []byte(sessionEnvContent(s)), nil
})),
// ctl — session control
Leaf("ctl", 0666,
Read(nopRead),
Write(func(_ HandlerCtx, data []byte) error {
return handleSessionCtl(s, kill, data)
}),
),
// plan — session plan
Leaf("plan", 0666,
Read(func(_ HandlerCtx) ([]byte, error) {
al := s.AgentLog()
if al == nil {
return nil, nil
}
al.mu.RLock()
data := make([]byte, len(al.plan))
copy(data, al.plan)
al.mu.RUnlock()
return data, nil
}),
Write(func(_ HandlerCtx, data []byte) error {
al := s.AgentLog()
if al == nil {
return nil
}
al.mu.Lock()
al.plan = make([]byte, len(data))
copy(al.plan, data)
al.mu.Unlock()
return nil
}),
),
// name — mutable session name
Leaf("name", 0666,
Read(func(_ HandlerCtx) ([]byte, error) {
return []byte(s.Name()), nil
}),
Write(func(_ HandlerCtx, data []byte) error {
newName := strings.TrimSpace(string(data))
if newName == "" {
return nil
}
// Rename via the rename function which updates the sessions map
oldName := s.Name()
if newName != oldName {
if err := rename(newName); err != nil {
return err
}
}
return nil
}),
),
// id — immutable session ID
Leaf("id", 0444, Read(func(_ HandlerCtx) ([]byte, error) {
return []byte(s.RunnableID()), nil
})),
// elevate — session elevation policy
Leaf("elevate", 0666,
Read(func(ctx HandlerCtx) ([]byte, error) {
if ctx.Elevate == nil {
return nil, fmt.Errorf("elevate broker not available")
}
p := ctx.Elevate.SessionPolicy(s.id)
return p.Marshal()
}),
Write(func(ctx HandlerCtx, data []byte) error {
if ctx.Elevate == nil {
return fmt.Errorf("elevate broker not available")
}
var p elevate.Policy
if err := elevate.ParsePolicy(data, &p); err != nil {
return err
}
ctx.Elevate.SetSessionPolicy(s.id, p)
return nil
}),
),
// agent/ — contains per-agent subdirectories
Dir("agent",
Leaf("new", 0666, Request(func(ctx HandlerCtx, data []byte) ([]byte, error) {
args := strings.Fields(string(data))
if err := CreateAgent(rs, s.Name(), args); err != nil {
return nil, fmt.Errorf("create agent: %w", err)
}
return []byte(s.Core.Agent().ID() + "\n"), nil
})),
// {aid} — dynamic per-agent directory
TemplateDir("{aid}", func(_ HandlerCtx) ([]FsNodeDecl, error) {
return listAgents(scoped)
}),
),
)
// Dynamic session directories must expose the lifecycle rename operation.
// Without this, Tree.Rename finds the directory but reports “rename not supported”.
sessionDir.Rename = func(newName string) error { return rename(newName) }
out = append(out, sessionDir)
}
return out, nil
}
// ── helpers ──────────────────────────────────────────────────────
func sessionEnvContent(sess *Session) string {
var sb strings.Builder
fmt.Fprintf(&sb, "OLLIE_SESSION_ID=%s\n", sess.RunnableID())
for _, e := range os.Environ() {
if strings.HasPrefix(e, "OLLIE_") && !strings.HasPrefix(e, "OLLIE_SESSION_ID=") {
sb.WriteString(e)
sb.WriteByte('\n')
}
}
return sb.String()
}
func handleSessionCtl(sess *Session, kill func(), data []byte) error {
input := strings.TrimSpace(string(data))
if input == "" {
return nil
}
cmd := strings.Fields(input)
switch cmd[0] {
case "kill", ".":
kill()
case "save":
if sess.Core != nil {
sess.Core.SaveSession("")
}
case "invalidate":
sess.InvalidateModelsCache()
default:
return fmt.Errorf("unknown session ctl command: %s", cmd[0])
}
return nil
}
// ── session/idx handler ──────────────────────────────────────────
func readSessionIdx(ctx HandlerCtx) ([]byte, error) {
return buildIndex(globalRootState), nil
}
// ── requestSessionNew — creates a new session ────────────────────
func requestSessionNew(ctx HandlerCtx, data []byte) ([]byte, error) {
args := strings.Fields(string(data))
name, err := Create(globalRootState, args)
if err != nil {
return nil, err
}
return []byte(name + "\n"), nil
}
// session file handler for requestAgentNew (within a session context)
func requestAgentNew(ctx HandlerCtx, data []byte) ([]byte, error) {
// The session is in ctx.Session, but requestAgentNew is called from
// listAgents which doesn't have it directly. This function is no longer
// used as a standalone — agent creation is handled inline in the
// session's agent/new handler above.
return nil, fmt.Errorf("not available")
}