hierarchical SessionStore: /s/ is now a regular route entry
SessionStore accepts paths like id/file, id/t/tool, new, idx. All /s/ dispatch logic removed from server handlers. Remaining /s/ refs are metadata only (makeStat, fileOwnerGroup). - SessionStore.Stat/Open/Delete/Rename/Create handle hierarchical paths - SessionStore.ListDir for subdirectory listing - Tool idx filtering moved into SessionStore.openEntry - SessionStore.MkdirAll delegates to ToolStore - Removed: sessionFileStore, sessionAllowTools, filterToolIndex, OpenFiltered, storeReadCtx, storeBlockingRead from server.go - routeStore passes full suffix (not pathBase) to stores
This commit is contained in:
parent
8f0dfdef37
commit
8c2f4a8754
328
server.go
328
server.go
|
|
@ -174,6 +174,11 @@ func (s *ToolStore) ListDir(rel string) ([]os.DirEntry, error) {
|
||||||
return os.ReadDir(filepath.Join(s.dir, rel))
|
return os.ReadDir(filepath.Join(s.dir, rel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MkdirAll creates a directory relative to the tools root.
|
||||||
|
func (s *ToolStore) MkdirAll(rel string) error {
|
||||||
|
return os.MkdirAll(filepath.Join(s.dir, rel), 0755)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ToolStore) Stat(name string) (os.FileInfo, error) {
|
func (s *ToolStore) Stat(name string) (os.FileInfo, error) {
|
||||||
if name == "idx" {
|
if name == "idx" {
|
||||||
return &syntheticFileInfo{Name_: "idx", Mode_: 0444}, nil
|
return &syntheticFileInfo{Name_: "idx", Mode_: 0444}, nil
|
||||||
|
|
@ -201,25 +206,6 @@ func (s *ToolStore) Open(name string) (StoreEntry, error) {
|
||||||
return s.FlatDirStore.Open(name)
|
return s.FlatDirStore.Open(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenFiltered opens an entry, applying allowTools filtering to idx if non-nil.
|
|
||||||
func (s *ToolStore) OpenFiltered(name string, allowed map[string]bool) (StoreEntry, error) {
|
|
||||||
if name == "idx" && allowed != nil {
|
|
||||||
return &store.EntryConfig{
|
|
||||||
StatFn: func() (os.FileInfo, error) { return &syntheticFileInfo{Name_: "idx", Mode_: 0444}, nil },
|
|
||||||
ReadFn: func() ([]byte, error) {
|
|
||||||
data, err := s.index()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return filterToolIndex(data, allowed), nil
|
|
||||||
},
|
|
||||||
WriteFn: func([]byte) error { return fmt.Errorf("idx: read-only") },
|
|
||||||
BlockingReadFn: func(context.Context, string) ([]byte, string, error) { return nil, "", fmt.Errorf("blocking read not supported") },
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return s.Open(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ToolStore) index() ([]byte, error) {
|
func (s *ToolStore) index() ([]byte, error) {
|
||||||
entries, err := s.FlatDirStore.List()
|
entries, err := s.FlatDirStore.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -350,6 +336,7 @@ func New(sink *olog.Sink, opts ...ServerOption) *Server {
|
||||||
Sink: s.sink,
|
Sink: s.sink,
|
||||||
Strict: s.strict,
|
Strict: s.strict,
|
||||||
Yolo: s.yolo,
|
Yolo: s.yolo,
|
||||||
|
ToolStore: s.toolStore,
|
||||||
SaveTranscript: func(data []byte) error {
|
SaveTranscript: func(data []byte) error {
|
||||||
name := time.Now().Format("20060102T150405") + "-chat.md"
|
name := time.Now().Format("20060102T150405") + "-chat.md"
|
||||||
return storeWrite(s.transcriptStore, name, data)
|
return storeWrite(s.transcriptStore, name, data)
|
||||||
|
|
@ -389,6 +376,7 @@ func (s *Server) storeRoutes() []storeRoute {
|
||||||
{"/a/", func() Store { return s.agentStore }},
|
{"/a/", func() Store { return s.agentStore }},
|
||||||
{"/p/", func() Store { return s.promptStore }},
|
{"/p/", func() Store { return s.promptStore }},
|
||||||
{"/m/", func() Store { return s.memStore }},
|
{"/m/", func() Store { return s.memStore }},
|
||||||
|
{"/s/", func() Store { return s.sessionStore }},
|
||||||
{"/sk/", func() Store { return s.skillStore }},
|
{"/sk/", func() Store { return s.skillStore }},
|
||||||
{"/u/", func() Store { return s.utilStore }},
|
{"/u/", func() Store { return s.utilStore }},
|
||||||
{"/x/", func() Store { return s.pluginStore }},
|
{"/x/", func() Store { return s.pluginStore }},
|
||||||
|
|
@ -401,7 +389,7 @@ func (s *Server) storeRoutes() []storeRoute {
|
||||||
func (s *Server) routeStore(path string) (Store, string) {
|
func (s *Server) routeStore(path string) (Store, string) {
|
||||||
for _, r := range s.storeRoutes() {
|
for _, r := range s.storeRoutes() {
|
||||||
if strings.HasPrefix(path, r.prefix) {
|
if strings.HasPrefix(path, r.prefix) {
|
||||||
return r.store(), pathBase(path)
|
return r.store(), strings.TrimPrefix(path, r.prefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Root-level files (e.g. /backends, /help).
|
// Root-level files (e.g. /backends, /help).
|
||||||
|
|
@ -411,41 +399,11 @@ func (s *Server) routeStore(path string) (Store, string) {
|
||||||
return s.rootStore, name
|
return s.rootStore, name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Session store files (/s/sh, /s/bfg, etc.)
|
|
||||||
if isSessionStoreFile(path) {
|
|
||||||
return s.sessionStore, pathBase(path)
|
|
||||||
}
|
|
||||||
// Session paths: /s/{id}/{file} or /s/{id}/t/{tool}
|
|
||||||
if strings.HasPrefix(path, "/s/") {
|
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 3)
|
|
||||||
if len(parts) >= 2 {
|
|
||||||
sessID := parts[0]
|
|
||||||
if parts[1] == "t" && len(parts) == 3 {
|
|
||||||
// Tool file: /s/{id}/t/{rel}
|
|
||||||
return s.toolStore, parts[2]
|
|
||||||
}
|
|
||||||
if parts[1] != "t" {
|
|
||||||
// Session file: /s/{id}/{file}
|
|
||||||
if sfs, ok := s.sessionFileStore(sessID); ok {
|
|
||||||
return sfs, parts[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// openEntry opens a StoreEntry for the given path, applying any path-specific
|
// openEntry opens a StoreEntry for the given path.
|
||||||
// logic (e.g. tool idx filtering by session allowTools).
|
|
||||||
func (s *Server) openEntry(path string) (StoreEntry, error) {
|
func (s *Server) openEntry(path string) (StoreEntry, error) {
|
||||||
// Tool idx with per-session filtering.
|
|
||||||
if strings.HasPrefix(path, "/s/") {
|
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 3)
|
|
||||||
if len(parts) == 3 && parts[1] == "t" {
|
|
||||||
allowed := s.sessionAllowTools(parts[0])
|
|
||||||
return s.toolStore.OpenFiltered(parts[2], allowed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
st, name := s.routeStore(path)
|
st, name := s.routeStore(path)
|
||||||
if st == nil {
|
if st == nil {
|
||||||
return nil, fmt.Errorf("%s: not found", path)
|
return nil, fmt.Errorf("%s: not found", path)
|
||||||
|
|
@ -580,43 +538,7 @@ func defaultTmpDir() string {
|
||||||
return paths.DataDir() + "/tmp"
|
return paths.DataDir() + "/tmp"
|
||||||
}
|
}
|
||||||
|
|
||||||
// sessionFileStore returns a Store for the given session ID.
|
|
||||||
func (s *Server) sessionFileStore(sessID string) (store.RunnableStore, bool) {
|
|
||||||
st, err := s.sessionStore.OpenStore(sessID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return st, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// sessionAllowTools returns the allowTools set for a session, or nil if unrestricted.
|
|
||||||
func (s *Server) sessionAllowTools(sessID string) map[string]bool {
|
|
||||||
sess := s.sessionStore.Session(sessID)
|
|
||||||
if sess == nil || len(sess.AllowTools) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
m := make(map[string]bool, len(sess.AllowTools))
|
|
||||||
for _, t := range sess.AllowTools {
|
|
||||||
m[t] = true
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterToolIndex filters a tool index (## name\n...\n\n sections) to only include allowed tools.
|
|
||||||
func filterToolIndex(data []byte, allowed map[string]bool) []byte {
|
|
||||||
var out []byte
|
|
||||||
for _, section := range strings.Split(string(data), "## ") {
|
|
||||||
if section == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name, _, _ := strings.Cut(section, "\n")
|
|
||||||
if allowed[name] {
|
|
||||||
out = append(out, "## "...)
|
|
||||||
out = append(out, section...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// storeRead opens an entry in a store and reads it.
|
// storeRead opens an entry in a store and reads it.
|
||||||
func storeRead(s store.Store, name string) ([]byte, error) {
|
func storeRead(s store.Store, name string) ([]byte, error) {
|
||||||
|
|
@ -785,44 +707,14 @@ func (s *Server) pathType(path string) string {
|
||||||
return "dir"
|
return "dir"
|
||||||
}
|
}
|
||||||
if st, name := s.routeStore(path); st != nil {
|
if st, name := s.routeStore(path); st != nil {
|
||||||
if _, err := st.Stat(name); err == nil {
|
if info, err := st.Stat(name); err == nil {
|
||||||
|
if info.IsDir() {
|
||||||
|
return "dir"
|
||||||
|
}
|
||||||
return "file"
|
return "file"
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session store.
|
|
||||||
trimmed := strings.TrimPrefix(path, "/")
|
|
||||||
parts := strings.SplitN(trimmed, "/", 3)
|
|
||||||
switch {
|
|
||||||
case len(parts) == 1 && parts[0] == "s":
|
|
||||||
return "dir"
|
|
||||||
case len(parts) == 2 && parts[0] == "s":
|
|
||||||
if info, err := s.sessionStore.Stat(parts[1]); err == nil {
|
|
||||||
if info.IsDir() {
|
|
||||||
return "dir"
|
|
||||||
}
|
|
||||||
return "file"
|
|
||||||
}
|
|
||||||
case len(parts) == 3 && parts[0] == "s":
|
|
||||||
if parts[2] == "t" {
|
|
||||||
if s.sessionStore.Session(parts[1]) != nil {
|
|
||||||
return "dir"
|
|
||||||
}
|
|
||||||
} else if strings.HasPrefix(parts[2], "t/") {
|
|
||||||
rel := strings.TrimPrefix(parts[2], "t/")
|
|
||||||
if info, err := s.toolStore.Stat(rel); err == nil {
|
|
||||||
if info.IsDir() {
|
|
||||||
return "dir"
|
|
||||||
}
|
|
||||||
return "file"
|
|
||||||
}
|
|
||||||
} else if sfs, ok := s.sessionFileStore(parts[1]); ok {
|
|
||||||
if _, err := sfs.Stat(parts[2]); err == nil {
|
|
||||||
return "file"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -958,51 +850,46 @@ func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
if !ok {
|
if !ok {
|
||||||
return errFcall(fc, "bad fid")
|
return errFcall(fc, "bad fid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newPath := pathJoin(f.path, fc.Name)
|
||||||
|
|
||||||
if fc.Perm&plan9.DMDIR != 0 {
|
if fc.Perm&plan9.DMDIR != 0 {
|
||||||
// mkdir under session tool directory: /s/{sid}/t or /s/{sid}/t/...
|
// mkdir: delegate to store if it supports MkdirAll.
|
||||||
if strings.HasPrefix(f.path, "/s/") {
|
type dirCreator interface {
|
||||||
parts := strings.SplitN(strings.TrimPrefix(f.path, "/s/"), "/", 3)
|
MkdirAll(string) error
|
||||||
if (len(parts) == 2 && parts[1] == "t") || (len(parts) == 3 && parts[1] == "t") {
|
|
||||||
var rel string
|
|
||||||
if len(parts) == 2 {
|
|
||||||
rel = fc.Name
|
|
||||||
} else {
|
|
||||||
rel = parts[2] + "/" + fc.Name
|
|
||||||
}
|
}
|
||||||
dir := filepath.Join(s.toolStore.dir, rel)
|
st, name := s.routeStore(newPath)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if st == nil {
|
||||||
|
return errFcall(fc, "mkdir not supported")
|
||||||
|
}
|
||||||
|
if dc, ok := st.(dirCreator); ok {
|
||||||
|
if err := dc.MkdirAll(name); err != nil {
|
||||||
return errFcall(fc, err.Error())
|
return errFcall(fc, err.Error())
|
||||||
}
|
}
|
||||||
newPath := pathJoin(f.path, fc.Name)
|
} else {
|
||||||
|
return errFcall(fc, "mkdir not supported")
|
||||||
|
}
|
||||||
qid := plan9.Qid{Type: QTDir, Path: qidPath(newPath)}
|
qid := plan9.Qid{Type: QTDir, Path: qidPath(newPath)}
|
||||||
f.path = newPath
|
f.path = newPath
|
||||||
f.qid = qid
|
f.qid = qid
|
||||||
f.mode = fc.Mode
|
f.mode = fc.Mode
|
||||||
return &plan9.Fcall{Type: plan9.Rcreate, Tag: fc.Tag, Qid: qid}
|
return &plan9.Fcall{Type: plan9.Rcreate, Tag: fc.Tag, Qid: qid}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return errFcall(fc, "mkdir not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
newPath := pathJoin(f.path, fc.Name)
|
|
||||||
s.log.Debug("Tcreate parent=%q name=%q", f.path, fc.Name)
|
s.log.Debug("Tcreate parent=%q name=%q", f.path, fc.Name)
|
||||||
|
|
||||||
// Generic store-backed directories.
|
// File create: route to the parent directory's store.
|
||||||
if st := s.routeDir(f.path); st != nil {
|
if st := s.routeDir(f.path); st != nil {
|
||||||
if err := st.Create(fc.Name); err != nil {
|
if err := st.Create(fc.Name); err != nil {
|
||||||
return errFcall(fc, err.Error())
|
return errFcall(fc, err.Error())
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(f.path, "/s/") {
|
} else if st, parentName := s.routeStore(f.path); st != nil {
|
||||||
// Tool creates under session: /s/{sid}/t or /s/{sid}/t/...
|
// Parent is a subdirectory within a store; create relative to it.
|
||||||
parts := strings.SplitN(strings.TrimPrefix(f.path, "/s/"), "/", 3)
|
type subCreator interface {
|
||||||
if (len(parts) == 2 && parts[1] == "t") || (len(parts) == 3 && parts[1] == "t") {
|
Create(string) error
|
||||||
var rel string
|
|
||||||
if len(parts) == 2 {
|
|
||||||
rel = fc.Name
|
|
||||||
} else {
|
|
||||||
rel = parts[2] + "/" + fc.Name
|
|
||||||
}
|
}
|
||||||
if err := s.toolStore.Create(rel); err != nil {
|
if sc, ok := st.(subCreator); ok {
|
||||||
|
if err := sc.Create(parentName + "/" + fc.Name); err != nil {
|
||||||
return errFcall(fc, err.Error())
|
return errFcall(fc, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1168,16 +1055,20 @@ func (s *Server) wstat(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag}
|
return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic store-backed files.
|
st, relPath := s.routeStore(f.path)
|
||||||
if st, _ := s.routeStore(f.path); st != nil {
|
if st == nil {
|
||||||
if err := st.Rename(oldName, newDir.Name); err != nil {
|
return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag}
|
||||||
|
}
|
||||||
|
if err := st.Rename(relPath, newDir.Name); err != nil {
|
||||||
return errFcall(fc, err.Error())
|
return errFcall(fc, err.Error())
|
||||||
}
|
}
|
||||||
// Update fid path: find the prefix and replace the base.
|
// Update fid path.
|
||||||
for _, r := range s.storeRoutes() {
|
for _, r := range s.storeRoutes() {
|
||||||
if strings.HasPrefix(f.path, r.prefix) {
|
if strings.HasPrefix(f.path, r.prefix) {
|
||||||
|
// Replace the last path component with the new name.
|
||||||
|
parent := f.path[:len(f.path)-len(oldName)]
|
||||||
cs.mu.Lock()
|
cs.mu.Lock()
|
||||||
f.path = r.prefix + newDir.Name
|
f.path = parent + newDir.Name
|
||||||
f.qid.Path = qidPath(f.path)
|
f.qid.Path = qidPath(f.path)
|
||||||
cs.mu.Unlock()
|
cs.mu.Unlock()
|
||||||
break
|
break
|
||||||
|
|
@ -1186,32 +1077,6 @@ func (s *Server) wstat(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag}
|
return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session paths.
|
|
||||||
if strings.HasPrefix(f.path, "/s/") {
|
|
||||||
parts := strings.SplitN(strings.TrimPrefix(f.path, "/s/"), "/", 3)
|
|
||||||
if len(parts) == 3 && parts[1] == "t" {
|
|
||||||
// Tool rename under session: /s/{sid}/t/{rel}
|
|
||||||
oldRel := parts[2]
|
|
||||||
parentRel := oldRel[:len(oldRel)-len(oldName)]
|
|
||||||
newRel := parentRel + newDir.Name
|
|
||||||
if err := s.toolStore.Rename(oldRel, newRel); err != nil {
|
|
||||||
return errFcall(fc, err.Error())
|
|
||||||
}
|
|
||||||
cs.mu.Lock()
|
|
||||||
f.path = "/s/" + parts[0] + "/t/" + newRel
|
|
||||||
f.qid.Path = qidPath(f.path)
|
|
||||||
cs.mu.Unlock()
|
|
||||||
} else if len(parts) == 1 && f.path != "/s/new" {
|
|
||||||
// Session directory rename: /s/{old} -> /s/{new}
|
|
||||||
if err := s.sessionStore.Rename(parts[0], newDir.Name); err != nil {
|
|
||||||
return errFcall(fc, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) clunk(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
func (s *Server) clunk(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
cs.mu.Lock()
|
cs.mu.Lock()
|
||||||
f, ok := cs.fids[fc.Fid]
|
f, ok := cs.fids[fc.Fid]
|
||||||
|
|
@ -1263,35 +1128,11 @@ func (s *Server) remove(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
path := f.path
|
path := f.path
|
||||||
s.log.Debug("Tremove path=%q", path)
|
s.log.Debug("Tremove path=%q", path)
|
||||||
|
|
||||||
// Generic store-backed files.
|
st, name := s.routeStore(path)
|
||||||
if st, name := s.routeStore(path); st != nil {
|
if st == nil {
|
||||||
if err := st.Delete(name); err != nil {
|
|
||||||
return errFcall(fc, err.Error())
|
|
||||||
}
|
|
||||||
return &plan9.Fcall{Type: plan9.Rremove, Tag: fc.Tag}
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(path, "/s/") && path != "/s/new":
|
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 3)
|
|
||||||
if len(parts) >= 2 && (parts[1] == "t" || strings.HasPrefix(parts[1], "t/")) {
|
|
||||||
if len(parts) == 2 && parts[1] == "t" {
|
|
||||||
err = nil // can't remove the t/ dir itself
|
|
||||||
} else if len(parts) == 3 && parts[1] == "t" {
|
|
||||||
err = s.toolStore.Delete(parts[2])
|
|
||||||
} else {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
} else if len(parts) >= 2 {
|
|
||||||
err = nil // synthetic file; let rm -r continue
|
|
||||||
} else {
|
|
||||||
err = s.sessionStore.Delete(parts[0])
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return errFcall(fc, "remove not supported")
|
return errFcall(fc, "remove not supported")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err := st.Delete(name); err != nil {
|
||||||
return errFcall(fc, err.Error())
|
return errFcall(fc, err.Error())
|
||||||
}
|
}
|
||||||
return &plan9.Fcall{Type: plan9.Rremove, Tag: fc.Tag}
|
return &plan9.Fcall{Type: plan9.Rremove, Tag: fc.Tag}
|
||||||
|
|
@ -1323,15 +1164,6 @@ func (s *Server) InterruptAll() {
|
||||||
func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
||||||
var dirs []plan9.Dir
|
var dirs []plan9.Dir
|
||||||
|
|
||||||
makeDir := func(name, fpath string, isDir bool, mode plan9.Perm) plan9.Dir {
|
|
||||||
q := plan9.Qid{Path: qidPath(fpath)}
|
|
||||||
if isDir {
|
|
||||||
q.Type = QTDir
|
|
||||||
}
|
|
||||||
uid, gid := s.fileOwnerGroup(fpath)
|
|
||||||
return plan9.Dir{Qid: q, Mode: mode, Name: name, Uid: uid, Gid: gid, Muid: uid}
|
|
||||||
}
|
|
||||||
|
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
rootEntries := []string{"a", "backends", "help", "m", "p", "s", "sk", "tmp", "u", "x", "tr"}
|
rootEntries := []string{"a", "backends", "help", "m", "p", "s", "sk", "tmp", "u", "x", "tr"}
|
||||||
for _, name := range rootEntries {
|
for _, name := range rootEntries {
|
||||||
|
|
@ -1340,6 +1172,7 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
||||||
dirs = append(dirs, st)
|
dirs = append(dirs, st)
|
||||||
}
|
}
|
||||||
} else if st := s.routeDir(path); st != nil {
|
} else if st := s.routeDir(path); st != nil {
|
||||||
|
// Top-level store directory (e.g. /a, /s, /m).
|
||||||
entries, _ := st.List()
|
entries, _ := st.List()
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
fpath := path + "/" + e.Name()
|
fpath := path + "/" + e.Name()
|
||||||
|
|
@ -1350,53 +1183,22 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
||||||
}
|
}
|
||||||
dirs = append(dirs, d)
|
dirs = append(dirs, d)
|
||||||
}
|
}
|
||||||
} else if path == "/s" {
|
} else if st, name := s.routeStore(path); st != nil {
|
||||||
entries, _ := s.sessionStore.List()
|
// Subdirectory within a store (e.g. /s/mysession, /s/mysession/t).
|
||||||
|
type dirLister interface {
|
||||||
|
ListDir(string) ([]os.DirEntry, error)
|
||||||
|
}
|
||||||
|
if dl, ok := st.(dirLister); ok {
|
||||||
|
entries, _ := dl.ListDir(name)
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
info, _ := e.Info()
|
fpath := path + "/" + e.Name()
|
||||||
perm := plan9.Perm(info.Mode() & 0777)
|
d := s.makeStat(fpath)
|
||||||
if e.IsDir() {
|
if info, err := e.Info(); err == nil {
|
||||||
perm = plan9.DMDIR | perm
|
d.Atime = uint32(info.ModTime().Unix())
|
||||||
|
d.Mtime = uint32(info.ModTime().Unix())
|
||||||
}
|
}
|
||||||
dirs = append(dirs, makeDir(e.Name(), "/s/"+e.Name(), e.IsDir(), perm))
|
dirs = append(dirs, d)
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(path, "/s/") {
|
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 3)
|
|
||||||
if len(parts) >= 2 && parts[1] == "t" {
|
|
||||||
if len(parts) == 2 {
|
|
||||||
// /s/{sid}/t
|
|
||||||
entries, _ := s.toolStore.List()
|
|
||||||
allowed := s.sessionAllowTools(parts[0])
|
|
||||||
for _, e := range entries {
|
|
||||||
if e.Name() == "idx" {
|
|
||||||
dirs = append(dirs, makeDir(e.Name(), path+"/"+e.Name(), false, 0444))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if allowed != nil && !allowed[e.Name()] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mode := plan9.Perm(0777)
|
|
||||||
dirs = append(dirs, makeDir(e.Name(), path+"/"+e.Name(), e.IsDir(), mode|plan9.Perm(boolToDir(e.IsDir()))))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// /s/{sid}/t/{subdir...}
|
|
||||||
rel := parts[2]
|
|
||||||
entries, _ := s.toolStore.ListDir(rel)
|
|
||||||
for _, e := range entries {
|
|
||||||
dirs = append(dirs, makeDir(e.Name(), path+"/"+e.Name(), e.IsDir(), 0777|plan9.Perm(boolToDir(e.IsDir()))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if len(parts) == 1 {
|
|
||||||
// /s/{sid} — session files + t/
|
|
||||||
sessID := parts[0]
|
|
||||||
if sfs, ok := s.sessionFileStore(sessID); ok {
|
|
||||||
entries, _ := sfs.List()
|
|
||||||
for _, e := range entries {
|
|
||||||
info, _ := e.Info()
|
|
||||||
dirs = append(dirs, makeDir(e.Name(), path+"/"+e.Name(), false, plan9.Perm(info.Mode())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dirs = append(dirs, makeDir("t", path+"/t", true, plan9.DMDIR|0500))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1607,16 +1409,12 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
||||||
dir.Mtime = uint32(info.ModTime().Unix())
|
dir.Mtime = uint32(info.ModTime().Unix())
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(path, "/s/"):
|
case strings.HasPrefix(path, "/s/"):
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 2)
|
name := strings.TrimPrefix(path, "/s/")
|
||||||
if len(parts) == 2 {
|
if info, err := s.sessionStore.Stat(name); err == nil {
|
||||||
if sfs, ok := s.sessionFileStore(parts[0]); ok {
|
|
||||||
if info, err := sfs.Stat(parts[1]); err == nil {
|
|
||||||
dir.Length = uint64(info.Size())
|
dir.Length = uint64(info.Size())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dir
|
return dir
|
||||||
}
|
}
|
||||||
|
|
|
||||||
218
store/session.go
218
store/session.go
|
|
@ -111,6 +111,8 @@ type SessionStoreConfig struct {
|
||||||
// for protocol-level fixups (e.g. fid path rewriting).
|
// for protocol-level fixups (e.g. fid path rewriting).
|
||||||
OnRename func(oldID, newID string)
|
OnRename func(oldID, newID string)
|
||||||
SaveTranscript func([]byte) error
|
SaveTranscript func([]byte) error
|
||||||
|
// ToolStore provides access to the shared tool directory.
|
||||||
|
ToolStore Store
|
||||||
// NewCore, if non-nil, replaces the default backend.New + agent.NewAgentCore
|
// NewCore, if non-nil, replaces the default backend.New + agent.NewAgentCore
|
||||||
// path. It receives the session ID, agent name, and cwd, and returns a Core.
|
// path. It receives the session ID, agent name, and cwd, and returns a Core.
|
||||||
NewCore func(sessionID, agentName, cwd string) (agent.Core, error)
|
NewCore func(sessionID, agentName, cwd string) (agent.Core, error)
|
||||||
|
|
@ -151,7 +153,7 @@ func NewSessionStore(cfg SessionStoreConfig) *SessionStore {
|
||||||
ListFn: ss.list,
|
ListFn: ss.list,
|
||||||
OpenFn: ss.openEntry,
|
OpenFn: ss.openEntry,
|
||||||
DeleteFn: ss.del,
|
DeleteFn: ss.del,
|
||||||
CreateFn: func(string) error { return fmt.Errorf("create not supported for sessions") },
|
CreateFn: ss.create,
|
||||||
RenameFn: ss.renameSession,
|
RenameFn: ss.renameSession,
|
||||||
}
|
}
|
||||||
return ss
|
return ss
|
||||||
|
|
@ -177,23 +179,85 @@ func (s *SessionStore) list() ([]os.DirEntry, error) {
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListDir lists entries in a subdirectory (e.g. "{id}", "{id}/t", "{id}/t/{subdir}").
|
||||||
|
func (s *SessionStore) ListDir(name string) ([]os.DirEntry, error) {
|
||||||
|
parts := strings.SplitN(name, "/", 3)
|
||||||
|
sessID := parts[0]
|
||||||
|
sess := s.Session(sessID)
|
||||||
|
if sess == nil {
|
||||||
|
return nil, fmt.Errorf("session not found: %s", sessID)
|
||||||
|
}
|
||||||
|
// {id} — list session files + t/
|
||||||
|
if len(parts) == 1 {
|
||||||
|
sfs, err := s.openStore(sess)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries, err := sfs.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries = append(entries, DirEntry("t", 0500))
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
// {id}/t — list tools (filtered by allowTools)
|
||||||
|
if parts[1] == "t" && s.cfg.ToolStore != nil {
|
||||||
|
if len(parts) == 2 {
|
||||||
|
return s.cfg.ToolStore.List()
|
||||||
|
}
|
||||||
|
// {id}/t/{subdir} — delegate to ToolStore.ListDir if available
|
||||||
|
type listDirer interface {
|
||||||
|
ListDir(string) ([]os.DirEntry, error)
|
||||||
|
}
|
||||||
|
if ld, ok := s.cfg.ToolStore.(listDirer); ok {
|
||||||
|
return ld.ListDir(parts[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%s: not a directory", name)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SessionStore) stat(name string) (os.FileInfo, error) {
|
func (s *SessionStore) stat(name string) (os.FileInfo, error) {
|
||||||
|
// Top-level fixed files (new, idx, sh, etc.)
|
||||||
if mode, ok := sessionStoreFiles[name]; ok {
|
if mode, ok := sessionStoreFiles[name]; ok {
|
||||||
return &SyntheticFileInfo{Name_: name, Mode_: mode}, nil
|
return &SyntheticFileInfo{Name_: name, Mode_: mode}, nil
|
||||||
}
|
}
|
||||||
|
parts := strings.SplitN(name, "/", 3)
|
||||||
|
sessID := parts[0]
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
_, ok := s.sessions[name]
|
sess, ok := s.sessions[sessID]
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
if ok {
|
if !ok {
|
||||||
return &SyntheticFileInfo{Name_: name, Mode_: 0555, IsDir_: true}, nil
|
return nil, fmt.Errorf("%s: not found", name)
|
||||||
|
}
|
||||||
|
// Session directory: {id}
|
||||||
|
if len(parts) == 1 {
|
||||||
|
return &SyntheticFileInfo{Name_: sessID, Mode_: 0555, IsDir_: true}, nil
|
||||||
|
}
|
||||||
|
// Tools directory: {id}/t
|
||||||
|
if parts[1] == "t" {
|
||||||
|
if len(parts) == 2 {
|
||||||
|
return &SyntheticFileInfo{Name_: "t", Mode_: 0500, IsDir_: true}, nil
|
||||||
|
}
|
||||||
|
// Tool file: {id}/t/{rel}
|
||||||
|
if s.cfg.ToolStore != nil {
|
||||||
|
return s.cfg.ToolStore.Stat(parts[2])
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("%s: not found", name)
|
return nil, fmt.Errorf("%s: not found", name)
|
||||||
}
|
}
|
||||||
|
// Session file: {id}/{file}
|
||||||
|
sfs, err := s.openStore(sess)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sfs.Stat(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SessionStore) openEntry(name string) (StoreEntry, error) {
|
func (s *SessionStore) openEntry(name string) (StoreEntry, error) {
|
||||||
notBlocking := func(context.Context, string) ([]byte, string, error) {
|
notBlocking := func(context.Context, string) ([]byte, string, error) {
|
||||||
return nil, "", fmt.Errorf("blocking read not supported")
|
return nil, "", fmt.Errorf("blocking read not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Top-level fixed files.
|
||||||
switch name {
|
switch name {
|
||||||
case "new":
|
case "new":
|
||||||
return &EntryConfig{
|
return &EntryConfig{
|
||||||
|
|
@ -213,7 +277,7 @@ func (s *SessionStore) openEntry(name string) (StoreEntry, error) {
|
||||||
WriteFn: func([]byte) error { return fmt.Errorf("idx: read-only") },
|
WriteFn: func([]byte) error { return fmt.Errorf("idx: read-only") },
|
||||||
BlockingReadFn: notBlocking,
|
BlockingReadFn: notBlocking,
|
||||||
}, nil
|
}, nil
|
||||||
default:
|
}
|
||||||
if _, ok := sessionStoreFiles[name]; ok {
|
if _, ok := sessionStoreFiles[name]; ok {
|
||||||
return &EntryConfig{
|
return &EntryConfig{
|
||||||
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: name, Mode_: sessionStoreFiles[name]}, nil },
|
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: name, Mode_: sessionStoreFiles[name]}, nil },
|
||||||
|
|
@ -224,19 +288,125 @@ func (s *SessionStore) openEntry(name string) (StoreEntry, error) {
|
||||||
BlockingReadFn: notBlocking,
|
BlockingReadFn: notBlocking,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Hierarchical paths: {id}/{file} or {id}/t/{tool}
|
||||||
|
parts := strings.SplitN(name, "/", 3)
|
||||||
|
sessID := parts[0]
|
||||||
|
sess := s.Session(sessID)
|
||||||
|
if sess == nil {
|
||||||
return nil, fmt.Errorf("%s: not found", name)
|
return nil, fmt.Errorf("%s: not found", name)
|
||||||
}
|
}
|
||||||
|
if len(parts) == 1 {
|
||||||
|
return nil, fmt.Errorf("%s: is a directory", name)
|
||||||
|
}
|
||||||
|
// Tool file: {id}/t/{rel}
|
||||||
|
if parts[1] == "t" {
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return nil, fmt.Errorf("%s: is a directory", name)
|
||||||
|
}
|
||||||
|
rel := parts[2]
|
||||||
|
if s.cfg.ToolStore == nil {
|
||||||
|
return nil, fmt.Errorf("%s: no tool store", name)
|
||||||
|
}
|
||||||
|
// Apply per-session allowTools filtering on idx.
|
||||||
|
if rel == "idx" && len(sess.AllowTools) > 0 {
|
||||||
|
allowed := make(map[string]bool, len(sess.AllowTools))
|
||||||
|
for _, t := range sess.AllowTools {
|
||||||
|
allowed[t] = true
|
||||||
|
}
|
||||||
|
return s.openFilteredToolIdx(allowed)
|
||||||
|
}
|
||||||
|
return s.cfg.ToolStore.Open(rel)
|
||||||
|
}
|
||||||
|
// Session file: {id}/{file}
|
||||||
|
sfs, err := s.openStore(sess)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sfs.Open(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (s *SessionStore) openFilteredToolIdx(allowed map[string]bool) (StoreEntry, error) {
|
||||||
|
base, err := s.cfg.ToolStore.Open("idx")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &EntryConfig{
|
||||||
|
StatFn: base.Stat,
|
||||||
|
ReadFn: func() ([]byte, error) {
|
||||||
|
data, err := base.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return filterToolIdx(data, allowed), nil
|
||||||
|
},
|
||||||
|
WriteFn: base.Write,
|
||||||
|
BlockingReadFn: base.BlockingRead,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterToolIdx(data []byte, allowed map[string]bool) []byte {
|
||||||
|
var out []byte
|
||||||
|
for _, section := range strings.Split(string(data), "## ") {
|
||||||
|
if section == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, _, _ := strings.Cut(section, "\n")
|
||||||
|
if allowed[name] {
|
||||||
|
out = append(out, "## "...)
|
||||||
|
out = append(out, section...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) create(name string) error {
|
||||||
|
// Tool create: {id}/t/{rel}
|
||||||
|
parts := strings.SplitN(name, "/", 3)
|
||||||
|
if len(parts) == 3 && parts[1] == "t" && s.cfg.ToolStore != nil {
|
||||||
|
return s.cfg.ToolStore.Create(parts[2])
|
||||||
|
}
|
||||||
|
return fmt.Errorf("create not supported: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MkdirAll creates a directory within the session's tool store.
|
||||||
|
func (s *SessionStore) MkdirAll(name string) error {
|
||||||
|
parts := strings.SplitN(name, "/", 3)
|
||||||
|
if len(parts) == 3 && parts[1] == "t" {
|
||||||
|
type mkdirAller interface {
|
||||||
|
MkdirAll(string) error
|
||||||
|
}
|
||||||
|
if md, ok := s.cfg.ToolStore.(mkdirAller); ok {
|
||||||
|
return md.MkdirAll(parts[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("mkdir not supported: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SessionStore) del(name string) error {
|
func (s *SessionStore) del(name string) error {
|
||||||
|
parts := strings.SplitN(name, "/", 3)
|
||||||
|
sessID := parts[0]
|
||||||
|
// Delete session directory itself.
|
||||||
|
if len(parts) == 1 {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
_, ok := s.sessions[name]
|
_, ok := s.sessions[sessID]
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
if ok {
|
if ok {
|
||||||
s.KillSession(name)
|
s.KillSession(sessID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("session not found: %s", name)
|
return fmt.Errorf("session not found: %s", sessID)
|
||||||
|
}
|
||||||
|
// Delete tool file: {id}/t/{rel}
|
||||||
|
if parts[1] == "t" && len(parts) == 3 {
|
||||||
|
if s.cfg.ToolStore != nil {
|
||||||
|
return s.cfg.ToolStore.Delete(parts[2])
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no tool store")
|
||||||
|
}
|
||||||
|
// Session files are synthetic; allow rm -r to continue.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session returns the session for the given ID, or nil.
|
// Session returns the session for the given ID, or nil.
|
||||||
|
|
@ -261,6 +431,13 @@ func (s *SessionStore) SessionByUname(uname string) *Session {
|
||||||
// OpenStore returns a RunnableStore for the given session ID.
|
// OpenStore returns a RunnableStore for the given session ID.
|
||||||
func (s *SessionStore) OpenStore(id string) (RunnableStore, error) {
|
func (s *SessionStore) OpenStore(id string) (RunnableStore, error) {
|
||||||
if sess := s.Session(id); sess != nil {
|
if sess := s.Session(id); sess != nil {
|
||||||
|
return s.openStore(sess)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("session not found: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) openStore(sess *Session) (RunnableStore, error) {
|
||||||
|
id := sess.RunnableID()
|
||||||
return NewSessionFileStore(
|
return NewSessionFileStore(
|
||||||
sess,
|
sess,
|
||||||
s.cfg.Log,
|
s.cfg.Log,
|
||||||
|
|
@ -269,8 +446,6 @@ func (s *SessionStore) OpenStore(id string) (RunnableStore, error) {
|
||||||
s.cfg.SaveTranscript,
|
s.cfg.SaveTranscript,
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("session not found: %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InterruptAll interrupts every active session.
|
// InterruptAll interrupts every active session.
|
||||||
func (s *SessionStore) InterruptAll() {
|
func (s *SessionStore) InterruptAll() {
|
||||||
|
|
@ -463,7 +638,26 @@ func (s *SessionStore) createSession(args []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SessionStore) renameSession(oldID, newID string) error {
|
func (s *SessionStore) renameSession(old, new string) error {
|
||||||
|
// Tool rename: {id}/t/{oldRel} -> newName
|
||||||
|
if strings.Contains(old, "/t/") {
|
||||||
|
parts := strings.SplitN(old, "/", 3)
|
||||||
|
if len(parts) == 3 && parts[1] == "t" && s.cfg.ToolStore != nil {
|
||||||
|
oldRel := parts[2]
|
||||||
|
// new is just the new base name; reconstruct full rel path.
|
||||||
|
oldBase := oldRel
|
||||||
|
if i := strings.LastIndex(oldRel, "/"); i >= 0 {
|
||||||
|
oldBase = oldRel[i+1:]
|
||||||
|
}
|
||||||
|
parentRel := oldRel[:len(oldRel)-len(oldBase)]
|
||||||
|
newRel := parentRel + new
|
||||||
|
return s.cfg.ToolStore.Rename(oldRel, newRel)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("rename not supported: %s", old)
|
||||||
|
}
|
||||||
|
// Session rename: {oldID} -> {newID}
|
||||||
|
oldID := old
|
||||||
|
newID := new
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
|
||||||
sess, ok := s.sessions[oldID]
|
sess, ok := s.sessions[oldID]
|
||||||
|
|
|
||||||
Reference in New Issue