fs: centralized permission registry; FUSE mutating operations
This commit is contained in:
parent
e77c5a09f4
commit
09e4f0f678
|
|
@ -23,6 +23,7 @@ import (
|
|||
"9fans.net/go/plan9/client"
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
perms "olliesrv/fs"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -54,7 +55,7 @@ func main() {
|
|||
log.Fatalf("dial: %v", err)
|
||||
}
|
||||
|
||||
os.MkdirAll(mnt, 0755)
|
||||
os.MkdirAll(mnt, perms.PermMkdir)
|
||||
root := &p9Dir{fsys: fsys, path: ""}
|
||||
server, err := fs.Mount(mnt, root, &fs.Options{
|
||||
MountOptions: fuse.MountOptions{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
|
|
@ -29,6 +31,11 @@ type p9File struct {
|
|||
var _ = (fs.NodeGetattrer)((*p9Dir)(nil))
|
||||
var _ = (fs.NodeReaddirer)((*p9Dir)(nil))
|
||||
var _ = (fs.NodeLookuper)((*p9Dir)(nil))
|
||||
var _ = (fs.NodeCreater)((*p9Dir)(nil))
|
||||
var _ = (fs.NodeUnlinker)((*p9Dir)(nil))
|
||||
var _ = (fs.NodeMkdirer)((*p9Dir)(nil))
|
||||
var _ = (fs.NodeRmdirer)((*p9Dir)(nil))
|
||||
var _ = (fs.NodeRenamer)((*p9Dir)(nil))
|
||||
|
||||
var _ = (fs.NodeGetattrer)((*p9File)(nil))
|
||||
var _ = (fs.NodeOpener)((*p9File)(nil))
|
||||
|
|
@ -46,7 +53,17 @@ func p9path(base, name string) string {
|
|||
// --- p9Dir ---
|
||||
|
||||
func (d *p9Dir) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||
out.Mode = 0555 | syscall.S_IFDIR
|
||||
path := d.path
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
dir, err := d.fsys.Stat(path)
|
||||
if err != nil {
|
||||
return syscall.ENOENT
|
||||
}
|
||||
out.Mode = uint32(dir.Mode)&0777 | syscall.S_IFDIR
|
||||
out.Uid = lookupUid(dir.Uid)
|
||||
out.Gid = lookupGid(dir.Gid)
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -81,6 +98,8 @@ func (d *p9Dir) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*f
|
|||
if err != nil {
|
||||
return nil, syscall.ENOENT
|
||||
}
|
||||
out.Uid = lookupUid(dir.Uid)
|
||||
out.Gid = lookupGid(dir.Gid)
|
||||
if dir.Mode&plan9.DMDIR != 0 {
|
||||
out.Mode = uint32(dir.Mode)&0777 | syscall.S_IFDIR
|
||||
node := &p9Dir{fsys: d.fsys, path: child}
|
||||
|
|
@ -92,6 +111,58 @@ func (d *p9Dir) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*f
|
|||
return d.NewInode(ctx, node, fs.StableAttr{}), 0
|
||||
}
|
||||
|
||||
func (d *p9Dir) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (*fs.Inode, fs.FileHandle, uint32, syscall.Errno) {
|
||||
child := p9path(d.path, name)
|
||||
fid, err := d.fsys.Create(child, plan9.OWRITE, plan9.Perm(mode&0777))
|
||||
if err != nil {
|
||||
return nil, nil, 0, syscall.EIO
|
||||
}
|
||||
fid.Close()
|
||||
out.Mode = mode & 0777
|
||||
node := &p9File{fsys: d.fsys, path: child}
|
||||
inode := d.NewInode(ctx, node, fs.StableAttr{})
|
||||
return inode, nil, fuse.FOPEN_DIRECT_IO, 0
|
||||
}
|
||||
|
||||
func (d *p9Dir) Unlink(ctx context.Context, name string) syscall.Errno {
|
||||
child := p9path(d.path, name)
|
||||
if err := d.fsys.Remove(child); err != nil {
|
||||
return syscall.EIO
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (d *p9Dir) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
|
||||
child := p9path(d.path, name)
|
||||
fid, err := d.fsys.Create(child, plan9.OREAD, plan9.DMDIR|plan9.Perm(mode&0777))
|
||||
if err != nil {
|
||||
return nil, syscall.EIO
|
||||
}
|
||||
fid.Close()
|
||||
out.Mode = mode&0777 | syscall.S_IFDIR
|
||||
node := &p9Dir{fsys: d.fsys, path: child}
|
||||
return d.NewInode(ctx, node, fs.StableAttr{Mode: syscall.S_IFDIR}), 0
|
||||
}
|
||||
|
||||
func (d *p9Dir) Rmdir(ctx context.Context, name string) syscall.Errno {
|
||||
child := p9path(d.path, name)
|
||||
if err := d.fsys.Remove(child); err != nil {
|
||||
return syscall.EIO
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (d *p9Dir) Rename(ctx context.Context, name string, newParent fs.InodeEmbedder, newName string, flags uint32) syscall.Errno {
|
||||
child := p9path(d.path, name)
|
||||
var dir plan9.Dir
|
||||
dir.Null()
|
||||
dir.Name = newName
|
||||
if err := d.fsys.Wstat(child, &dir); err != nil {
|
||||
return syscall.EIO
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// --- p9File ---
|
||||
|
||||
func (f *p9File) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||
|
|
@ -101,6 +172,8 @@ func (f *p9File) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOu
|
|||
}
|
||||
out.Mode = uint32(dir.Mode) & 0777
|
||||
out.Size = dir.Length
|
||||
out.Uid = lookupUid(dir.Uid)
|
||||
out.Gid = lookupGid(dir.Gid)
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -126,8 +199,6 @@ func (f *p9File) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off in
|
|||
return nil, syscall.EIO
|
||||
}
|
||||
defer fid.Close()
|
||||
// The 9fans client doesn't support Seek; read from offset by reading and discarding.
|
||||
// For small offsets this is fine; large seeks on big files would need a smarter approach.
|
||||
if off > 0 {
|
||||
buf := make([]byte, min(off, 8192))
|
||||
remaining := off
|
||||
|
|
@ -174,3 +245,27 @@ func min(a, b int64) int64 {
|
|||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func lookupUid(name string) uint32 {
|
||||
if id, err := strconv.ParseUint(name, 10, 32); err == nil {
|
||||
return uint32(id)
|
||||
}
|
||||
if u, err := user.Lookup(name); err == nil {
|
||||
if id, err := strconv.ParseUint(u.Uid, 10, 32); err == nil {
|
||||
return uint32(id)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupGid(name string) uint32 {
|
||||
if id, err := strconv.ParseUint(name, 10, 32); err == nil {
|
||||
return uint32(id)
|
||||
}
|
||||
if g, err := user.LookupGroup(name); err == nil {
|
||||
if id, err := strconv.ParseUint(g.Gid, 10, 32); err == nil {
|
||||
return uint32(id)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,14 +44,14 @@ func (m *MemoryStore) Stat(name string) (os.FileInfo, error) {
|
|||
if !ok {
|
||||
return nil, fmt.Errorf("%s: not found", name)
|
||||
}
|
||||
return &SyntheticFileInfo{Name_: name, Mode_: 0444, Size_: sz}, nil
|
||||
return &SyntheticFileInfo{Name_: name, Mode_: PermIdx, Size_: sz}, nil
|
||||
}
|
||||
|
||||
func (m *MemoryStore) List() ([]os.DirEntry, error) {
|
||||
m.mu.RLock()
|
||||
entries := make([]os.DirEntry, 0, len(m.entries))
|
||||
for name := range m.entries {
|
||||
entries = append(entries, FileEntry(name, 0444))
|
||||
entries = append(entries, FileEntry(name, PermIdx))
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
return entries, nil
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
// Package fs permission registry.
|
||||
// This is the SINGLE SOURCE OF TRUTH for all filesystem permissions.
|
||||
// Every file and directory permission in the 9P tree is declared here.
|
||||
// No permission literals should appear anywhere else.
|
||||
package fs
|
||||
|
||||
import "os"
|
||||
|
||||
// Path constants for the filesystem namespace.
|
||||
//
|
||||
// Paths ending in "/" are directories (used as route prefixes).
|
||||
// Paths without trailing "/" are scoped entries within a parent.
|
||||
//
|
||||
// Template variables:
|
||||
// {id} — session ID (dynamic, matches any session)
|
||||
// {file} — file name within a session directory
|
||||
const (
|
||||
PathRoot = "/"
|
||||
PathAgents = "/a/"
|
||||
PathPrompts = "/p/"
|
||||
PathMemory = "/m/"
|
||||
PathSessions = "/s/"
|
||||
PathSkills = "/sk/"
|
||||
PathUtils = "/u/"
|
||||
PathPlugins = "/x/"
|
||||
PathTmp = "/tmp/"
|
||||
PathTranscripts = "/tr/"
|
||||
PathSessionDir = "s/{id}"
|
||||
PathTools = "t/"
|
||||
PathSessionFile = "s/{id}/{file}"
|
||||
)
|
||||
|
||||
// Perm declares permissions for a filesystem node.
|
||||
// - DirMode: permission reported in 9P stat for the directory itself.
|
||||
// - FileMode: default permission for files created/listed in this directory.
|
||||
// - Files: per-file permission overrides (file name → mode).
|
||||
type Perm struct {
|
||||
DirMode os.FileMode
|
||||
FileMode os.FileMode
|
||||
Files map[string]os.FileMode
|
||||
}
|
||||
|
||||
// Perms is the permission registry. To find the permission for any path,
|
||||
// look it up here.
|
||||
var Perms = map[string]Perm{
|
||||
PathRoot: {
|
||||
DirMode: 0755,
|
||||
FileMode: 0444,
|
||||
Files: map[string]os.FileMode{
|
||||
"backends": 0444,
|
||||
"help": 0444,
|
||||
},
|
||||
},
|
||||
PathAgents: {DirMode: 0775, FileMode: 0666},
|
||||
PathPrompts: {DirMode: 0755, FileMode: 0444},
|
||||
PathMemory: {DirMode: 0775, FileMode: 0664},
|
||||
PathSessions: {
|
||||
DirMode: 0755,
|
||||
FileMode: 0444,
|
||||
Files: map[string]os.FileMode{
|
||||
"new": 0666,
|
||||
"idx": 0444,
|
||||
"ls": 0555,
|
||||
"kill": 0555,
|
||||
"sh": 0550,
|
||||
"b": 0555,
|
||||
"bfg": 0555,
|
||||
"bbg": 0555,
|
||||
"cleanup": 0555,
|
||||
},
|
||||
},
|
||||
PathSkills: {DirMode: 0775, FileMode: 0664},
|
||||
PathUtils: {DirMode: 0755, FileMode: 0555},
|
||||
PathPlugins: {DirMode: 0755, FileMode: 0555},
|
||||
PathTmp: {DirMode: 0777, FileMode: 0777},
|
||||
PathTranscripts: {DirMode: 0755, FileMode: 0444},
|
||||
PathSessionDir: {DirMode: 0755},
|
||||
PathTools: {DirMode: 0755, FileMode: 0755},
|
||||
PathSessionFile: {
|
||||
Files: map[string]os.FileMode{
|
||||
"plan": 0666,
|
||||
"ctl": 0666,
|
||||
"prompt": 0466,
|
||||
"fifo.in": 0222,
|
||||
"fifo.out": 0444,
|
||||
"chat": 0444,
|
||||
"offset": 0444,
|
||||
"cfg": 0666,
|
||||
"state": 0444,
|
||||
"statewait": 0444,
|
||||
"usage": 0444,
|
||||
"cost": 0444,
|
||||
"ctxsz": 0444,
|
||||
"models": 0444,
|
||||
"systemprompt": 0444,
|
||||
"env": 0444,
|
||||
"tail": 0555,
|
||||
"prompt.prev": 0444,
|
||||
"context": 0444,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Structural permissions — invariants of the Tree type itself.
|
||||
const (
|
||||
PermIdx os.FileMode = 0444 // synthetic index files are always read-only
|
||||
PermChildDir os.FileMode = 0555 // mounted child directories are always rx
|
||||
)
|
||||
|
||||
// OS-level directory creation mode (for os.MkdirAll of backing dirs on disk).
|
||||
const PermMkdir os.FileMode = 0755
|
||||
|
||||
// Private directory creation mode (e.g. sessions state dir).
|
||||
const PermMkdirPrivate os.FileMode = 0700
|
||||
|
|
@ -57,7 +57,7 @@ func NewSkillTreeWith(cfg SkillConfig) *Tree {
|
|||
|
||||
ss := &skillHelper{cfg: cfg}
|
||||
|
||||
return NewTree(cfg.Dirs, 0644,
|
||||
return NewTree(cfg.Dirs, Perms[PathSkills].FileMode,
|
||||
WithIndex(ss.index),
|
||||
WithResolver(ss.resolve),
|
||||
WithLister(ss.list),
|
||||
|
|
@ -121,7 +121,7 @@ func (ss *skillHelper) resolve(_ []string, name string) (string, error) {
|
|||
func (ss *skillHelper) list(_ []string) ([]os.DirEntry, error) {
|
||||
var result []os.DirEntry
|
||||
for _, m := range ss.listSkills() {
|
||||
result = append(result, FileEntry(m.Name+".md", 0666))
|
||||
result = append(result, FileEntry(m.Name+".md", Perms[PathSkills].FileMode))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -129,10 +129,10 @@ func (ss *skillHelper) list(_ []string) ([]os.DirEntry, error) {
|
|||
func (ss *skillHelper) create(_ []string, name string, _ os.FileMode) error {
|
||||
skillName := strings.TrimSuffix(name, ".md")
|
||||
dir := filepath.Join(ss.cfg.Dirs[0], skillName)
|
||||
if err := ss.cfg.MkdirAll(dir, 0755); err != nil {
|
||||
if err := ss.cfg.MkdirAll(dir, PermMkdir); err != nil {
|
||||
return err
|
||||
}
|
||||
return ss.cfg.WriteFile(filepath.Join(dir, "SKILL.md"), nil, 0644)
|
||||
return ss.cfg.WriteFile(filepath.Join(dir, "SKILL.md"), nil, Perms[PathSkills].FileMode)
|
||||
}
|
||||
|
||||
func (ss *skillHelper) del(_ []string, name string) error {
|
||||
|
|
|
|||
20
fs/tree.go
20
fs/tree.go
|
|
@ -160,12 +160,12 @@ func (d *Tree) Stat(name string) (os.FileInfo, error) {
|
|||
// Check children first.
|
||||
if child, rest, ok := d.split(name); ok {
|
||||
if rest == "" {
|
||||
return &SyntheticFileInfo{Name_: name, Mode_: 0555, IsDir_: true}, nil
|
||||
return &SyntheticFileInfo{Name_: name, Mode_: PermChildDir, IsDir_: true}, nil
|
||||
}
|
||||
return child.Stat(rest)
|
||||
}
|
||||
if name == "idx" && d.indexFn != nil {
|
||||
return &SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil
|
||||
return &SyntheticFileInfo{Name_: "idx", Mode_: PermIdx}, nil
|
||||
}
|
||||
// Custom stat hook.
|
||||
if d.statFn != nil {
|
||||
|
|
@ -201,19 +201,19 @@ func (d *Tree) List() ([]os.DirEntry, error) {
|
|||
}
|
||||
if d.indexFn != nil {
|
||||
result := make([]os.DirEntry, 0, len(entries)+1)
|
||||
result = append(result, FileEntry("idx", 0444))
|
||||
result = append(result, FileEntry("idx", PermIdx))
|
||||
result = append(result, entries...)
|
||||
entries = result
|
||||
}
|
||||
for name := range d.children {
|
||||
entries = append(entries, DirEntry(name, 0555))
|
||||
entries = append(entries, DirEntry(name, PermChildDir))
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
var result []os.DirEntry
|
||||
if d.indexFn != nil {
|
||||
result = append(result, FileEntry("idx", 0444))
|
||||
result = append(result, FileEntry("idx", PermIdx))
|
||||
seen["idx"] = true
|
||||
}
|
||||
for _, dir := range d.Tree {
|
||||
|
|
@ -230,7 +230,7 @@ func (d *Tree) List() ([]os.DirEntry, error) {
|
|||
}
|
||||
for name := range d.children {
|
||||
if !seen[name] {
|
||||
result = append(result, DirEntry(name, 0555))
|
||||
result = append(result, DirEntry(name, PermChildDir))
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
|
|
@ -249,7 +249,7 @@ func (d *Tree) Open(name string) (File, error) {
|
|||
}
|
||||
if name == "idx" && d.indexFn != nil {
|
||||
return &FileConfig{
|
||||
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil },
|
||||
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: "idx", Mode_: PermIdx}, nil },
|
||||
ReadFn: func() ([]byte, error) { return d.indexFn(d) },
|
||||
WriteFn: func([]byte) error { return fmt.Errorf("idx: read-only") },
|
||||
BlockingReadFn: notBlocking,
|
||||
|
|
@ -271,7 +271,7 @@ func (d *Tree) Open(name string) (File, error) {
|
|||
if d.readOnly {
|
||||
return fmt.Errorf("%s: read-only", name)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(wp), 0755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(wp), PermMkdir); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(wp, data, d.perm)
|
||||
|
|
@ -295,7 +295,7 @@ func (d *Tree) Create(name string) error {
|
|||
return d.createFn(d.Tree, name, d.perm)
|
||||
}
|
||||
p := d.writePath(name)
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(p), PermMkdir); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(p, nil, d.perm)
|
||||
|
|
@ -364,5 +364,5 @@ func (d *Tree) MkdirAll(rel string) error {
|
|||
if len(d.Tree) == 0 {
|
||||
return fmt.Errorf("no backing directory")
|
||||
}
|
||||
return os.MkdirAll(filepath.Join(d.Tree[0], rel), 0755)
|
||||
return os.MkdirAll(filepath.Join(d.Tree[0], rel), PermMkdir)
|
||||
}
|
||||
36
main.go
36
main.go
|
|
@ -95,7 +95,7 @@ func cmdMount() {
|
|||
home, _ := os.UserHomeDir()
|
||||
mnt = filepath.Join(home, "mnt", addr)
|
||||
}
|
||||
if err := os.MkdirAll(mnt, 0755); err != nil {
|
||||
if err := os.MkdirAll(mnt, fs.PermMkdir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot create mount dir: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
@ -167,21 +167,21 @@ func runServer(sockPath, pidPath string) {
|
|||
}
|
||||
|
||||
// Write PID file
|
||||
os.WriteFile(pidPath, []byte(fmt.Sprintf("%d", os.Getpid())), 0644) //nolint:errcheck
|
||||
os.WriteFile(pidPath, []byte(fmt.Sprintf("%d", os.Getpid())), fs.Perms[fs.PathRoot].FileMode) //nolint:errcheck
|
||||
|
||||
sink := olog.NewSink(os.Stdout, os.Stderr, olog.ParseLevel(os.Getenv("OLLIE_LOG"), olog.LevelWarn))
|
||||
|
||||
memDir := defaultMemDir()
|
||||
os.MkdirAll(memDir, 0755) //nolint:errcheck
|
||||
os.MkdirAll(memDir, fs.PermMkdir) //nolint:errcheck
|
||||
transcriptDir := defaultTranscriptDir()
|
||||
os.MkdirAll(transcriptDir, 0755) //nolint:errcheck
|
||||
os.MkdirAll(transcriptDir, fs.PermMkdir) //nolint:errcheck
|
||||
tmpDir := defaultTmpDir()
|
||||
os.MkdirAll(tmpDir, 0755) //nolint:errcheck
|
||||
os.MkdirAll(tmpDir, fs.PermMkdir) //nolint:errcheck
|
||||
|
||||
agentsDirs := agent.AgentsDirs()
|
||||
sessionsDir := paths.CfgDir() + "/sessions"
|
||||
toolTree := fs.NewTree([]string{execute.ToolsPath()}, 0755, fs.WithIndex(ToolIndex))
|
||||
transcriptStore := fs.NewTree([]string{transcriptDir}, 0444, fs.WithReadOnly())
|
||||
toolTree := fs.NewTree([]string{execute.ToolsPath()}, fs.Perms[fs.PathTools].FileMode, fs.WithIndex(ToolIndex))
|
||||
transcriptStore := fs.NewTree([]string{transcriptDir}, fs.Perms[fs.PathTranscripts].FileMode, fs.WithReadOnly())
|
||||
|
||||
mgr := session.NewManager(session.ManagerConfig{
|
||||
AgentsDir: agentsDirs[0],
|
||||
|
|
@ -207,16 +207,16 @@ func runServer(sockPath, pidPath string) {
|
|||
|
||||
srv := server.New(server.Config{
|
||||
Sink: sink,
|
||||
AgentStore: fs.NewTree(agentsDirs, 0644),
|
||||
PromptStore: fs.NewTree(agent.PromptsDirs(), 0444, fs.WithReadOnly()),
|
||||
MemStore: fs.NewTree([]string{memDir}, 0644),
|
||||
AgentStore: fs.NewTree(agentsDirs, fs.Perms[fs.PathAgents].FileMode),
|
||||
PromptStore: fs.NewTree(agent.PromptsDirs(), fs.Perms[fs.PathPrompts].FileMode, fs.WithReadOnly()),
|
||||
MemStore: fs.NewTree([]string{memDir}, fs.Perms[fs.PathMemory].FileMode),
|
||||
ToolTree: toolTree,
|
||||
UtilStore: fs.NewTree([]string{paths.CfgDir() + "/scripts/u"}, 0555, fs.WithReadOnly()),
|
||||
PluginStore: fs.NewTree([]string{execute.PluginsPath()}, 0555, fs.WithReadOnly()),
|
||||
UtilStore: fs.NewTree([]string{paths.CfgDir() + "/scripts/u"}, fs.Perms[fs.PathUtils].FileMode, fs.WithReadOnly()),
|
||||
PluginStore: fs.NewTree([]string{execute.PluginsPath()}, fs.Perms[fs.PathPlugins].FileMode, fs.WithReadOnly()),
|
||||
SkillTree: fs.NewSkillTree(),
|
||||
SessionMgr: mgr,
|
||||
TranscriptStore: transcriptStore,
|
||||
TmpStore: fs.NewTree([]string{tmpDir}, 0600),
|
||||
TmpStore: fs.NewTree([]string{tmpDir}, fs.Perms[fs.PathTmp].FileMode),
|
||||
RootStore: NewRootStore(),
|
||||
})
|
||||
|
||||
|
|
@ -267,7 +267,7 @@ func runServer(sockPath, pidPath string) {
|
|||
mnt = filepath.Join(home, "mnt", "ollie")
|
||||
}
|
||||
var fuseCmd *exec.Cmd
|
||||
if err := os.MkdirAll(mnt, 0755); err != nil {
|
||||
if err := os.MkdirAll(mnt, fs.PermMkdir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: cannot create mount dir: %v\n", err)
|
||||
} else {
|
||||
fuseCmd = exec.Command("9pfuse", sockPath, mnt)
|
||||
|
|
@ -381,7 +381,7 @@ func NewRootStore() *fs.Tree {
|
|||
}
|
||||
readOnly := func([]byte) error { return fmt.Errorf("read-only") }
|
||||
|
||||
return fs.NewTree(nil, 0444,
|
||||
return fs.NewTree(nil, fs.Perms[fs.PathRoot].FileMode,
|
||||
fs.WithReadOnly(),
|
||||
fs.WithResolver(func(_ []string, name string) (string, error) {
|
||||
if _, ok := entries[name]; ok {
|
||||
|
|
@ -391,8 +391,8 @@ func NewRootStore() *fs.Tree {
|
|||
}),
|
||||
fs.WithLister(func(_ []string) ([]os.DirEntry, error) {
|
||||
return []os.DirEntry{
|
||||
fs.FileEntry("backends", 0444),
|
||||
fs.FileEntry("help", 0444),
|
||||
fs.FileEntry("backends", fs.Perms[fs.PathRoot].FileMode),
|
||||
fs.FileEntry("help", fs.Perms[fs.PathRoot].FileMode),
|
||||
}, nil
|
||||
}),
|
||||
fs.WithOpener(func(_ []string, name string) (fs.File, error) {
|
||||
|
|
@ -401,7 +401,7 @@ func NewRootStore() *fs.Tree {
|
|||
return nil, fmt.Errorf("%s: not found", name)
|
||||
}
|
||||
return &fs.FileConfig{
|
||||
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: name, Mode_: 0444}, nil },
|
||||
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: name, Mode_: fs.Perms[fs.PathRoot].FileMode}, nil },
|
||||
ReadFn: readFn,
|
||||
WriteFn: readOnly,
|
||||
BlockingReadFn: notBlocking,
|
||||
|
|
|
|||
162
server/server.go
162
server/server.go
|
|
@ -115,24 +115,26 @@ func New(cfg Config) *Server {
|
|||
return s
|
||||
}
|
||||
|
||||
// fsRoute maps a path prefix to its backing fs.
|
||||
// fsRoute maps a path prefix to its backing fs and directory permissions.
|
||||
type fsRoute struct {
|
||||
prefix string
|
||||
tree func() FileTree
|
||||
prefix string
|
||||
dirMode os.FileMode
|
||||
tree func() FileTree
|
||||
}
|
||||
|
||||
// routes returns the route table for the file tree namespace.
|
||||
// Directory permissions come from the fs.Perms registry.
|
||||
func (s *Server) routes() []fsRoute {
|
||||
return []fsRoute{
|
||||
{"/a/", func() FileTree { return s.agentStore }},
|
||||
{"/p/", func() FileTree { return s.promptStore }},
|
||||
{"/m/", func() FileTree { return s.memStore }},
|
||||
{"/s/", func() FileTree { return s.sessionMgr.Tree() }},
|
||||
{"/sk/", func() FileTree { return s.skillTree }},
|
||||
{"/u/", func() FileTree { return s.utilStore }},
|
||||
{"/x/", func() FileTree { return s.pluginStore }},
|
||||
{"/tmp/", func() FileTree { return s.tmpStore }},
|
||||
{"/tr/", func() FileTree { return s.transcriptStore }},
|
||||
{fs.PathAgents, fs.Perms[fs.PathAgents].DirMode, func() FileTree { return s.agentStore }},
|
||||
{fs.PathPrompts, fs.Perms[fs.PathPrompts].DirMode, func() FileTree { return s.promptStore }},
|
||||
{fs.PathMemory, fs.Perms[fs.PathMemory].DirMode, func() FileTree { return s.memStore }},
|
||||
{fs.PathSessions, fs.Perms[fs.PathSessions].DirMode, func() FileTree { return s.sessionMgr.Tree() }},
|
||||
{fs.PathSkills, fs.Perms[fs.PathSkills].DirMode, func() FileTree { return s.skillTree }},
|
||||
{fs.PathUtils, fs.Perms[fs.PathUtils].DirMode, func() FileTree { return s.utilStore }},
|
||||
{fs.PathPlugins, fs.Perms[fs.PathPlugins].DirMode, func() FileTree { return s.pluginStore }},
|
||||
{fs.PathTmp, fs.Perms[fs.PathTmp].DirMode, func() FileTree { return s.tmpStore }},
|
||||
{fs.PathTranscripts, fs.Perms[fs.PathTranscripts].DirMode, func() FileTree { return s.transcriptStore }},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +174,52 @@ func (s *Server) routeDir(path string) FileTree {
|
|||
return nil
|
||||
}
|
||||
|
||||
// dirMode returns the directory permission for a path.
|
||||
// Top-level directories get their mode from the route table.
|
||||
// Subdirectories within /s/ have specific overrides for tools.
|
||||
func (s *Server) dirMode(path string) plan9.Perm {
|
||||
for _, r := range s.routes() {
|
||||
if path+"/" == r.prefix {
|
||||
return plan9.Perm(r.dirMode)
|
||||
}
|
||||
if strings.HasPrefix(path, r.prefix) {
|
||||
// Subdirectory within a route — check for /s/{id}/t
|
||||
if r.prefix == fs.PathSessions {
|
||||
rel := strings.TrimPrefix(path, fs.PathSessions)
|
||||
parts := strings.SplitN(rel, "/", 3)
|
||||
if len(parts) >= 2 && parts[1] == "t" {
|
||||
return plan9.Perm(fs.Perms[fs.PathTools].DirMode)
|
||||
}
|
||||
}
|
||||
return plan9.Perm(r.dirMode)
|
||||
}
|
||||
}
|
||||
return plan9.Perm(fs.Perms[fs.PathRoot].DirMode)
|
||||
}
|
||||
|
||||
// filePerm returns the file permission for a path from the permission registry.
|
||||
func (s *Server) filePerm(path string) os.FileMode {
|
||||
// Session files have per-file overrides.
|
||||
if strings.HasPrefix(path, fs.PathSessions) {
|
||||
base := pathBase(path)
|
||||
if m, ok := fs.Perms[fs.PathSessionFile].Files[base]; ok {
|
||||
return m
|
||||
}
|
||||
if m, ok := fs.Perms[fs.PathSessions].Files[base]; ok {
|
||||
return m
|
||||
}
|
||||
return fs.Perms[fs.PathSessions].FileMode
|
||||
}
|
||||
for _, r := range s.routes() {
|
||||
if strings.HasPrefix(path, r.prefix) {
|
||||
if p, ok := fs.Perms[r.prefix]; ok {
|
||||
return p.FileMode
|
||||
}
|
||||
}
|
||||
}
|
||||
return fs.PermIdx
|
||||
}
|
||||
|
||||
// AddGroup adds a user to a group.
|
||||
func (s *Server) AddGroup(group, user string) {
|
||||
s.mu.Lock()
|
||||
|
|
@ -205,22 +253,19 @@ func (s *Server) InGroup(group, user string) bool {
|
|||
// Session namespace (/s/{sid}/**) is owned by the agent principal with group "agent".
|
||||
// Everything else is owned by the current system user and their primary group.
|
||||
func (s *Server) fileOwnerGroup(path string) (uid, gid string) {
|
||||
if strings.HasPrefix(path, "/s/") {
|
||||
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 2)
|
||||
if strings.HasPrefix(path, fs.PathSessions) {
|
||||
parts := strings.SplitN(strings.TrimPrefix(path, fs.PathSessions), "/", 2)
|
||||
if len(parts) >= 1 && parts[0] != "new" && !isSessionFile(path) {
|
||||
if sess := s.sessionMgr.Session(parts[0]); sess != nil {
|
||||
return sess.Uname(), "agent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if u, err := user.Current(); err == nil {
|
||||
gid := u.Gid
|
||||
if g, err := user.LookupGroupId(u.Gid); err == nil {
|
||||
gid = g.Name
|
||||
}
|
||||
return u.Username, gid
|
||||
return u.Username, "agent"
|
||||
}
|
||||
return "ollie", "ollie"
|
||||
return "ollie", "agent"
|
||||
}
|
||||
|
||||
// checkPerm verifies that uname has the requested access (mode) to path.
|
||||
|
|
@ -404,7 +449,7 @@ func (s *Server) handle(cs *connState, fc *plan9.Fcall, ctx context.Context) *pl
|
|||
// isSessionFile reports whether path is a fixed file directly under /s/
|
||||
// (i.e. /s/<name> where name is in sessionStoreFiles).
|
||||
func isSessionFile(path string) bool {
|
||||
name, ok := strings.CutPrefix(path, "/s/")
|
||||
name, ok := strings.CutPrefix(path, fs.PathSessions)
|
||||
if !ok || strings.Contains(name, "/") {
|
||||
return false
|
||||
}
|
||||
|
|
@ -488,13 +533,10 @@ func (s *Server) attach(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
qid := plan9.Qid{Type: QTDir, Path: 0}
|
||||
cs.fids[fc.Fid] = &fid{path: "/", qid: qid}
|
||||
s.log.Debug("Tattach uname=%q", fc.Uname)
|
||||
// Assign group membership based on whether uname is a session principal.
|
||||
// All clients get agent group membership so they can access shared
|
||||
// directories. Owner bits handle user-vs-agent distinction.
|
||||
if fc.Uname != "" {
|
||||
if s.sessionMgr.SessionByUname(fc.Uname) != nil {
|
||||
s.AddGroup("agent", fc.Uname)
|
||||
} else {
|
||||
s.AddGroup("user", fc.Uname)
|
||||
}
|
||||
s.AddGroup("agent", fc.Uname)
|
||||
}
|
||||
return &plan9.Fcall{Type: plan9.Rattach, Tag: fc.Tag, Qid: qid}
|
||||
}
|
||||
|
|
@ -811,7 +853,7 @@ func (s *Server) wstat(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
c.mu.Unlock()
|
||||
}
|
||||
// Update group membership for session renames.
|
||||
if r.prefix == "/s/" && !strings.Contains(relPath, "/") {
|
||||
if r.prefix == fs.PathSessions && !strings.Contains(relPath, "/") {
|
||||
s.RemoveGroup("agent", oldName)
|
||||
s.AddGroup("agent", newDir.Name)
|
||||
}
|
||||
|
|
@ -993,41 +1035,11 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
|||
var mode plan9.Perm
|
||||
if isDir {
|
||||
qid.Type = QTDir
|
||||
if strings.HasPrefix(path, "/s/") {
|
||||
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 3)
|
||||
if (len(parts) == 2 && parts[1] == "t") || (len(parts) == 3 && parts[1] == "t") {
|
||||
mode = plan9.DMDIR | 0500 // rx owner only
|
||||
} else {
|
||||
mode = plan9.DMDIR | 0755
|
||||
}
|
||||
} else if path == "/s" {
|
||||
mode = plan9.DMDIR | 0755
|
||||
} else if path == "/u" || path == "/x" {
|
||||
mode = plan9.DMDIR | 0555
|
||||
} else if path == "/p" {
|
||||
mode = plan9.DMDIR | 0664
|
||||
} else if path == "/a" {
|
||||
mode = plan9.DMDIR | 0744
|
||||
} else if path == "/sk" {
|
||||
mode = plan9.DMDIR | 0754
|
||||
} else if path == "/tmp" {
|
||||
mode = plan9.DMDIR | 0777
|
||||
} else if path == "/tr" {
|
||||
mode = plan9.DMDIR | 0766
|
||||
} else {
|
||||
mode = plan9.DMDIR | 0755
|
||||
}
|
||||
mode = plan9.DMDIR | s.dirMode(path)
|
||||
} else {
|
||||
// Delegate to the backing tree for file mode.
|
||||
if tree, name := s.route(path); tree != nil {
|
||||
if info, err := tree.Stat(name); err == nil {
|
||||
mode = plan9.Perm(info.Mode())
|
||||
} else {
|
||||
mode = 0444
|
||||
}
|
||||
} else {
|
||||
mode = 0444
|
||||
}
|
||||
// Use the permission registry for file mode rather than os.Stat
|
||||
// (which is affected by umask and may not reflect intended perms).
|
||||
mode = plan9.Perm(s.filePerm(path))
|
||||
}
|
||||
|
||||
uid, gid := s.fileOwnerGroup(path)
|
||||
|
|
@ -1043,7 +1055,7 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
|||
// For chat and tailable mutable files, report actual size and
|
||||
// Qid version so polling tools (tail -f) can detect changes via stat.
|
||||
// Path format: /s/{sessid}/{file}
|
||||
if strings.HasPrefix(path, "/s/") {
|
||||
if strings.HasPrefix(path, fs.PathSessions) {
|
||||
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
||||
if len(parts) == 3 && parts[0] == "s" {
|
||||
if sess := s.sessionMgr.Session(parts[1]); sess != nil {
|
||||
|
|
@ -1057,7 +1069,7 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
|||
}
|
||||
|
||||
// For memory and plan files, report real size and timestamps from the fs.
|
||||
if strings.HasPrefix(path, "/m/") {
|
||||
if strings.HasPrefix(path, fs.PathMemory) {
|
||||
if info, err := s.memStore.Stat(base); err == nil {
|
||||
dir.Length = uint64(info.Size())
|
||||
dir.Atime = uint32(info.ModTime().Unix())
|
||||
|
|
@ -1079,47 +1091,47 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
|||
if info, err := os.Stat(s.helpPath()); err == nil {
|
||||
dir.Length = uint64(info.Size())
|
||||
}
|
||||
case strings.HasPrefix(path, "/a/"):
|
||||
case strings.HasPrefix(path, fs.PathAgents):
|
||||
if info, err := s.agentStore.Stat(base); err == nil {
|
||||
dir.Length = uint64(info.Size())
|
||||
}
|
||||
case strings.HasPrefix(path, "/p/"):
|
||||
case strings.HasPrefix(path, fs.PathPrompts):
|
||||
if info, err := s.promptStore.Stat(base); err == nil {
|
||||
dir.Length = uint64(info.Size())
|
||||
}
|
||||
case strings.HasPrefix(path, "/sk/"):
|
||||
case strings.HasPrefix(path, fs.PathSkills):
|
||||
if content, err := readFile(s.skillTree, base); err == nil {
|
||||
dir.Length = uint64(len(content))
|
||||
}
|
||||
case strings.HasPrefix(path, "/s/") && strings.Contains(path, "/t/"):
|
||||
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 3)
|
||||
case strings.HasPrefix(path, fs.PathSessions) && strings.Contains(path, "/t/"):
|
||||
parts := strings.SplitN(strings.TrimPrefix(path, fs.PathSessions), "/", 3)
|
||||
if len(parts) == 3 && parts[1] == "t" {
|
||||
if content, err := readFile(s.toolTree, parts[2]); err == nil {
|
||||
dir.Length = uint64(len(content))
|
||||
}
|
||||
}
|
||||
case strings.HasPrefix(path, "/u/"):
|
||||
case strings.HasPrefix(path, fs.PathUtils):
|
||||
if content, err := readFile(s.utilStore, base); err == nil {
|
||||
dir.Length = uint64(len(content))
|
||||
}
|
||||
case strings.HasPrefix(path, "/x/"):
|
||||
case strings.HasPrefix(path, fs.PathPlugins):
|
||||
if content, err := readFile(s.pluginStore, base); err == nil {
|
||||
dir.Length = uint64(len(content))
|
||||
}
|
||||
case strings.HasPrefix(path, "/tmp/"):
|
||||
case strings.HasPrefix(path, fs.PathTmp):
|
||||
if info, err := s.tmpStore.Stat(base); err == nil {
|
||||
dir.Length = uint64(info.Size())
|
||||
dir.Atime = uint32(info.ModTime().Unix())
|
||||
dir.Mtime = uint32(info.ModTime().Unix())
|
||||
}
|
||||
case strings.HasPrefix(path, "/tr/"):
|
||||
case strings.HasPrefix(path, fs.PathTranscripts):
|
||||
if info, err := s.transcriptStore.Stat(base); err == nil {
|
||||
dir.Length = uint64(info.Size())
|
||||
dir.Atime = uint32(info.ModTime().Unix())
|
||||
dir.Mtime = uint32(info.ModTime().Unix())
|
||||
}
|
||||
case strings.HasPrefix(path, "/s/"):
|
||||
name := strings.TrimPrefix(path, "/s/")
|
||||
case strings.HasPrefix(path, fs.PathSessions):
|
||||
name := strings.TrimPrefix(path, fs.PathSessions)
|
||||
if info, err := s.sessionMgr.Tree().Stat(name); err == nil {
|
||||
dir.Length = uint64(info.Size())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,25 +112,14 @@ func (sess *Session) LogInfo() (length int, vers uint32) {
|
|||
return len(sess.log), sess.logVers
|
||||
}
|
||||
|
||||
// sessionStoreFiles maps fixed file entries in s/ to their permissions.
|
||||
var sessionStoreFiles = map[string]os.FileMode{
|
||||
"new": 0666,
|
||||
"idx": 0444,
|
||||
"ls": 0555,
|
||||
"kill": 0555,
|
||||
"sh": 0550,
|
||||
"b": 0555,
|
||||
"bfg": 0555,
|
||||
"bbg": 0555,
|
||||
"cleanup": 0555,
|
||||
}
|
||||
|
||||
|
||||
var sessionStoreOrder = []string{"new", "idx", "ls", "kill", "sh", "b", "bfg", "bbg", "cleanup"}
|
||||
|
||||
// FileMode returns the mode for a fixed session file,
|
||||
// or 0 and false if the name is not a fixed file.
|
||||
func FileMode(name string) (os.FileMode, bool) {
|
||||
m, ok := sessionStoreFiles[name]
|
||||
m, ok := fs.Perms[fs.PathSessions].Files[name]
|
||||
return m, ok
|
||||
}
|
||||
|
||||
|
|
@ -204,11 +193,11 @@ func (s *Manager) AddSession(sess *Session) {
|
|||
func (s *Manager) list() ([]os.DirEntry, error) {
|
||||
entries := make([]os.DirEntry, 0, len(sessionStoreOrder))
|
||||
for _, name := range sessionStoreOrder {
|
||||
entries = append(entries, fs.FileEntry(name, sessionStoreFiles[name]))
|
||||
entries = append(entries, fs.FileEntry(name, fs.Perms[fs.PathSessions].Files[name]))
|
||||
}
|
||||
s.mu.RLock()
|
||||
for id := range s.sessions {
|
||||
entries = append(entries, fs.DirEntry(id, 0555))
|
||||
entries = append(entries, fs.DirEntry(id, fs.Perms[fs.PathSessionDir].DirMode))
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
return entries, nil
|
||||
|
|
@ -232,7 +221,7 @@ func (s *Manager) Readdir(name string) ([]os.DirEntry, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, fs.DirEntry("t", 0500))
|
||||
entries = append(entries, fs.DirEntry("t", fs.Perms[fs.PathTools].DirMode))
|
||||
return entries, nil
|
||||
}
|
||||
// {id}/t — list tools (filtered by allowTools)
|
||||
|
|
@ -253,7 +242,7 @@ func (s *Manager) Readdir(name string) ([]os.DirEntry, error) {
|
|||
|
||||
func (s *Manager) stat(name string) (os.FileInfo, error) {
|
||||
// Top-level fixed files (new, idx, sh, etc.)
|
||||
if mode, ok := sessionStoreFiles[name]; ok {
|
||||
if mode, ok := fs.Perms[fs.PathSessions].Files[name]; ok {
|
||||
return &fs.SyntheticFileInfo{Name_: name, Mode_: mode}, nil
|
||||
}
|
||||
parts := strings.SplitN(name, "/", 3)
|
||||
|
|
@ -266,12 +255,12 @@ func (s *Manager) stat(name string) (os.FileInfo, error) {
|
|||
}
|
||||
// Session directory: {id}
|
||||
if len(parts) == 1 {
|
||||
return &fs.SyntheticFileInfo{Name_: sessID, Mode_: 0555, IsDir_: true}, nil
|
||||
return &fs.SyntheticFileInfo{Name_: sessID, Mode_: fs.Perms[fs.PathSessionDir].DirMode, IsDir_: true}, nil
|
||||
}
|
||||
// Tools directory: {id}/t
|
||||
if parts[1] == "t" {
|
||||
if len(parts) == 2 {
|
||||
return &fs.SyntheticFileInfo{Name_: "t", Mode_: 0500, IsDir_: true}, nil
|
||||
return &fs.SyntheticFileInfo{Name_: "t", Mode_: fs.Perms[fs.PathTools].DirMode, IsDir_: true}, nil
|
||||
}
|
||||
// Tool file: {id}/t/{rel}
|
||||
if s.cfg.ToolTree != nil {
|
||||
|
|
@ -296,7 +285,7 @@ func (s *Manager) openEntry(name string) (fs.File, error) {
|
|||
switch name {
|
||||
case "new":
|
||||
return &fs.FileConfig{
|
||||
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "new", Mode_: 0666}, nil },
|
||||
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "new", Mode_: fs.Perms[fs.PathSessions].Files["new"]}, nil },
|
||||
ReadFn: func() ([]byte, error) {
|
||||
return []byte("name=\ncwd=\nbackend=\nmodel=\nagent=\nmaxTokens=\nmaxCompletionTokens=\ntemperature=\ntopP=\ntopK=\nminP=\ntopA=\nfrequencyPenalty=\npresencePenalty=\nrepetitionPenalty=\nreasoning=\nreasoningEffort=\nincludeReasoning=\nresponseFormat=\nstop=\nverbosity=\n"), nil
|
||||
},
|
||||
|
|
@ -307,15 +296,15 @@ func (s *Manager) openEntry(name string) (fs.File, error) {
|
|||
}, nil
|
||||
case "idx":
|
||||
return &fs.FileConfig{
|
||||
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil },
|
||||
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "idx", Mode_: fs.Perms[fs.PathSessions].Files["idx"]}, nil },
|
||||
ReadFn: func() ([]byte, error) { return s.index(), nil },
|
||||
WriteFn: func([]byte) error { return fmt.Errorf("idx: read-only") },
|
||||
BlockingReadFn: notBlocking,
|
||||
}, nil
|
||||
}
|
||||
if _, ok := sessionStoreFiles[name]; ok {
|
||||
if _, ok := fs.Perms[fs.PathSessions].Files[name]; ok {
|
||||
return &fs.FileConfig{
|
||||
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: name, Mode_: sessionStoreFiles[name]}, nil },
|
||||
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: name, Mode_: fs.Perms[fs.PathSessions].Files[name]}, nil },
|
||||
ReadFn: func() ([]byte, error) {
|
||||
return s.cfg.ReadFile(paths.CfgDir() + "/scripts/s/" + name)
|
||||
},
|
||||
|
|
@ -618,7 +607,7 @@ func (s *Manager) createSession(args []string) error {
|
|||
be.SetModel(modelOverride)
|
||||
}
|
||||
|
||||
if err := s.cfg.MkdirAll(s.cfg.SessionsDir, 0700); err != nil {
|
||||
if err := s.cfg.MkdirAll(s.cfg.SessionsDir, fs.PermMkdirPrivate); err != nil {
|
||||
return fmt.Errorf("sessions dir: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,39 +15,45 @@ import (
|
|||
)
|
||||
|
||||
// FileList defines the fixed set of files in a session directory.
|
||||
// Permissions come from fs.Perms[fs.PathSessionFile].Files.
|
||||
var FileList = []struct {
|
||||
Name string
|
||||
Mode os.FileMode
|
||||
OneShot bool
|
||||
Async bool
|
||||
}{
|
||||
{"plan", 0666, false, false},
|
||||
{"ctl", 0666, false, true},
|
||||
{"prompt", 0466, false, true},
|
||||
{"fifo.in", 0222, false, true},
|
||||
{"fifo.out", 0444, true, false},
|
||||
{"chat", 0444, false, false},
|
||||
{"offset", 0444, false, false},
|
||||
{"cfg", 0666, false, false},
|
||||
{"state", 0444, false, false},
|
||||
{"statewait", 0444, false, false},
|
||||
{"usage", 0444, false, false},
|
||||
{"cost", 0444, false, false},
|
||||
{"ctxsz", 0444, false, false},
|
||||
{"models", 0444, false, false},
|
||||
{"systemprompt", 0444, false, false},
|
||||
{"env", 0444, false, false},
|
||||
{"tail", 0555, false, false},
|
||||
{"prompt.prev", 0444, false, false},
|
||||
{"context", 0444, false, false},
|
||||
{"plan", false, false},
|
||||
{"ctl", false, true},
|
||||
{"prompt", false, true},
|
||||
{"fifo.in", false, true},
|
||||
{"fifo.out", true, false},
|
||||
{"chat", false, false},
|
||||
{"offset", false, false},
|
||||
{"cfg", false, false},
|
||||
{"state", false, false},
|
||||
{"statewait", false, false},
|
||||
{"usage", false, false},
|
||||
{"cost", false, false},
|
||||
{"ctxsz", false, false},
|
||||
{"models", false, false},
|
||||
{"systemprompt", false, false},
|
||||
{"env", false, false},
|
||||
{"tail", false, false},
|
||||
{"prompt.prev", false, false},
|
||||
{"context", false, false},
|
||||
}
|
||||
|
||||
// sessionFilePerms returns the permission registry for per-session files.
|
||||
func sessionFilePerms() map[string]os.FileMode {
|
||||
return fs.Perms[fs.PathSessionFile].Files
|
||||
}
|
||||
|
||||
|
||||
func NewSessionTree(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error) *fs.Tree {
|
||||
h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript}
|
||||
perms := sessionFilePerms()
|
||||
specs := make([]fs.FileSpec, len(FileList))
|
||||
for i, f := range FileList {
|
||||
specs[i] = h.fileSpec(f.Name, f.Mode)
|
||||
specs[i] = h.fileSpec(f.Name, perms[f.Name])
|
||||
specs[i].OneShot = f.OneShot
|
||||
specs[i].Async = f.Async
|
||||
}
|
||||
|
|
|
|||
Reference in New Issue