203 lines
5.5 KiB
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")
|
|
}
|