move t/ into s/{sid}/t/; add allowTools filtering at 9P layer
This commit is contained in:
parent
393460f767
commit
c075964a1f
|
|
@ -63,11 +63,11 @@ ollie/
|
||||||
systemprompt read: fully rendered system prompt for this session
|
systemprompt read: fully rendered system prompt for this session
|
||||||
tail exec: exec tail -f chat
|
tail exec: exec tail -f chat
|
||||||
usage read: token counts (input, output, requests; [estimated] if not reported by backend)
|
usage read: token counts (input, output, requests; [estimated] if not reported by backend)
|
||||||
|
t/ dir: tool scripts (r/w, backed by ~/.config/ollie/tools/)
|
||||||
|
idx read: tool index (name, description, args — synthesized at read time)
|
||||||
|
<script> r/w: tool script content
|
||||||
sk/ dir: skills (r/w, from OLLIE_SKILLS_PATH or ~/.config/ollie/skills/)
|
sk/ dir: skills (r/w, from OLLIE_SKILLS_PATH or ~/.config/ollie/skills/)
|
||||||
<name>.md r/w: skill SKILL.md content
|
<name>.md r/w: skill SKILL.md content
|
||||||
t/ dir: tool scripts (r/w, backed by ~/.config/ollie/tools/)
|
|
||||||
idx read: tool index (name, description, args — synthesized at read time)
|
|
||||||
<script> r/w: tool script content
|
|
||||||
tr/ dir: saved transcripts (read, backed by OLLIE_TRANSCRIPT_PATH or ~/.config/ollie/transcript/)
|
tr/ dir: saved transcripts (read, backed by OLLIE_TRANSCRIPT_PATH or ~/.config/ollie/transcript/)
|
||||||
<timestamp>-chat.md read: transcript saved at session end
|
<timestamp>-chat.md read: transcript saved at session end
|
||||||
tmp/ dir: temporary files (r/w, backed by OLLIE_TMP_PATH or $XDG_DATA_HOME/ollie/tmp/)
|
tmp/ dir: temporary files (r/w, backed by OLLIE_TMP_PATH or $XDG_DATA_HOME/ollie/tmp/)
|
||||||
|
|
@ -90,7 +90,6 @@ Each 9P directory endpoint is backed by a named store implementing the `Store` i
|
||||||
| `/m` | `FlatDirStore` | `Store` |
|
| `/m` | `FlatDirStore` | `Store` |
|
||||||
| `/p` | `FlatDirStore` | `Store` |
|
| `/p` | `FlatDirStore` | `Store` |
|
||||||
| `/sk` | `SkillStore` | `Store` |
|
| `/sk` | `SkillStore` | `Store` |
|
||||||
| `/t` | `ToolStore` | `Store` |
|
|
||||||
| `/tr` | `FlatDirStore` | `Store` |
|
| `/tr` | `FlatDirStore` | `Store` |
|
||||||
| `/tmp`| `FlatDirStore` | `Store` |
|
| `/tmp`| `FlatDirStore` | `Store` |
|
||||||
| `/u` | `UtilStore` | `Store` |
|
| `/u` | `UtilStore` | `Store` |
|
||||||
|
|
|
||||||
314
server.go
314
server.go
|
|
@ -327,6 +327,35 @@ func (s *Server) sessionFileStore(sessID string) (store.RunnableStore, bool) {
|
||||||
return st, true
|
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) {
|
||||||
e, err := s.Open(name)
|
e, err := s.Open(name)
|
||||||
|
|
@ -541,8 +570,6 @@ func (s *Server) pathType(path string) string {
|
||||||
return "dir"
|
return "dir"
|
||||||
case len(parts) == 1 && parts[0] == "sk":
|
case len(parts) == 1 && parts[0] == "sk":
|
||||||
return "dir"
|
return "dir"
|
||||||
case len(parts) == 1 && parts[0] == "t":
|
|
||||||
return "dir"
|
|
||||||
case len(parts) == 1 && parts[0] == "u":
|
case len(parts) == 1 && parts[0] == "u":
|
||||||
return "dir"
|
return "dir"
|
||||||
case len(parts) == 2 && parts[0] == "u":
|
case len(parts) == 2 && parts[0] == "u":
|
||||||
|
|
@ -588,22 +615,22 @@ func (s *Server) pathType(path string) string {
|
||||||
if _, err := s.skillStore.Stat(parts[1]); err == nil {
|
if _, err := s.skillStore.Stat(parts[1]); err == nil {
|
||||||
return "file"
|
return "file"
|
||||||
}
|
}
|
||||||
case len(parts) == 2 && parts[0] == "t":
|
|
||||||
if info, err := s.toolStore.Stat(parts[1]); err == nil {
|
|
||||||
if info.IsDir() {
|
|
||||||
return "dir"
|
|
||||||
}
|
|
||||||
return "file"
|
|
||||||
}
|
|
||||||
case len(parts) == 3 && parts[0] == "t":
|
|
||||||
if info, err := s.toolStore.Stat(parts[1] + "/" + parts[2]); err == nil {
|
|
||||||
if info.IsDir() {
|
|
||||||
return "dir"
|
|
||||||
}
|
|
||||||
return "file"
|
|
||||||
}
|
|
||||||
case len(parts) == 3 && parts[0] == "s":
|
case len(parts) == 3 && parts[0] == "s":
|
||||||
if sfs, ok := s.sessionFileStore(parts[1]); ok {
|
if parts[2] == "t" {
|
||||||
|
// /s/{sid}/t is the tools directory
|
||||||
|
if s.sessionStore.Session(parts[1]) != nil {
|
||||||
|
return "dir"
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(parts[2], "t/") {
|
||||||
|
// /s/{sid}/t/{rest} — tool file or subdir
|
||||||
|
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 {
|
if _, err := sfs.Stat(parts[2]); err == nil {
|
||||||
return "file"
|
return "file"
|
||||||
}
|
}
|
||||||
|
|
@ -731,23 +758,27 @@ func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
return errFcall(fc, "bad fid")
|
return errFcall(fc, "bad fid")
|
||||||
}
|
}
|
||||||
if fc.Perm&plan9.DMDIR != 0 {
|
if fc.Perm&plan9.DMDIR != 0 {
|
||||||
if f.path == "/t" || strings.HasPrefix(f.path, "/t/") {
|
// mkdir under session tool directory: /s/{sid}/t or /s/{sid}/t/...
|
||||||
rel := strings.TrimPrefix(f.path, "/t")
|
if strings.HasPrefix(f.path, "/s/") {
|
||||||
if rel == "" {
|
parts := strings.SplitN(strings.TrimPrefix(f.path, "/s/"), "/", 3)
|
||||||
rel = fc.Name
|
if (len(parts) == 2 && parts[1] == "t") || (len(parts) == 3 && parts[1] == "t") {
|
||||||
} else {
|
var rel string
|
||||||
rel = rel[1:] + "/" + fc.Name // strip leading /
|
if len(parts) == 2 {
|
||||||
|
rel = fc.Name
|
||||||
|
} else {
|
||||||
|
rel = parts[2] + "/" + fc.Name
|
||||||
|
}
|
||||||
|
dir := filepath.Join(s.toolStore.dir, rel)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return errFcall(fc, err.Error())
|
||||||
|
}
|
||||||
|
newPath := pathJoin(f.path, fc.Name)
|
||||||
|
qid := plan9.Qid{Type: QTDir, Path: qidPath(newPath)}
|
||||||
|
f.path = newPath
|
||||||
|
f.qid = qid
|
||||||
|
f.mode = fc.Mode
|
||||||
|
return &plan9.Fcall{Type: plan9.Rcreate, Tag: fc.Tag, Qid: qid}
|
||||||
}
|
}
|
||||||
dir := filepath.Join(s.toolStore.dir, rel)
|
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
||||||
return errFcall(fc, err.Error())
|
|
||||||
}
|
|
||||||
newPath := pathJoin(f.path, fc.Name)
|
|
||||||
qid := plan9.Qid{Type: QTDir, Path: qidPath(newPath)}
|
|
||||||
f.path = newPath
|
|
||||||
f.qid = qid
|
|
||||||
f.mode = fc.Mode
|
|
||||||
return &plan9.Fcall{Type: plan9.Rcreate, Tag: fc.Tag, Qid: qid}
|
|
||||||
}
|
}
|
||||||
return errFcall(fc, "mkdir not supported")
|
return errFcall(fc, "mkdir not supported")
|
||||||
}
|
}
|
||||||
|
|
@ -764,10 +795,6 @@ func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
if err := s.memStore.Create(fc.Name); err != nil {
|
if err := s.memStore.Create(fc.Name); err != nil {
|
||||||
return errFcall(fc, err.Error())
|
return errFcall(fc, err.Error())
|
||||||
}
|
}
|
||||||
case "/t":
|
|
||||||
if err := s.toolStore.Create(fc.Name); err != nil {
|
|
||||||
return errFcall(fc, err.Error())
|
|
||||||
}
|
|
||||||
case "/tmp":
|
case "/tmp":
|
||||||
if err := s.tmpStore.Create(fc.Name); err != nil {
|
if err := s.tmpStore.Create(fc.Name); err != nil {
|
||||||
return errFcall(fc, err.Error())
|
return errFcall(fc, err.Error())
|
||||||
|
|
@ -777,10 +804,19 @@ func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
return errFcall(fc, err.Error())
|
return errFcall(fc, err.Error())
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if strings.HasPrefix(f.path, "/t/") {
|
// Tool creates under session: /s/{sid}/t or /s/{sid}/t/...
|
||||||
rel := strings.TrimPrefix(f.path, "/t/") + "/" + fc.Name
|
if strings.HasPrefix(f.path, "/s/") {
|
||||||
if err := s.toolStore.Create(rel); err != nil {
|
parts := strings.SplitN(strings.TrimPrefix(f.path, "/s/"), "/", 3)
|
||||||
return errFcall(fc, err.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
|
||||||
|
}
|
||||||
|
if err := s.toolStore.Create(rel); err != nil {
|
||||||
|
return errFcall(fc, err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -880,16 +916,6 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan
|
||||||
return s.readSlice(fc, content)
|
return s.readSlice(fc, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool files are served from the tool store.
|
|
||||||
if strings.HasPrefix(path, "/t/") {
|
|
||||||
s.log.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
|
|
||||||
content, err := storeReadCtx(ctx, s.toolStore, strings.TrimPrefix(path, "/t/"))
|
|
||||||
if err != nil {
|
|
||||||
return errFcall(fc, err.Error())
|
|
||||||
}
|
|
||||||
return s.readSlice(fc, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Util files are served from the util store.
|
// Util files are served from the util store.
|
||||||
if strings.HasPrefix(path, "/u/") {
|
if strings.HasPrefix(path, "/u/") {
|
||||||
s.log.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
|
s.log.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
|
||||||
|
|
@ -934,6 +960,25 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan
|
||||||
if strings.HasPrefix(path, "/s/") {
|
if strings.HasPrefix(path, "/s/") {
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
||||||
if len(parts) == 3 {
|
if len(parts) == 3 {
|
||||||
|
// Tool files under session: /s/{id}/t/{tool}
|
||||||
|
if parts[2] == "t" || strings.HasPrefix(parts[2], "t/") {
|
||||||
|
rel := strings.TrimPrefix(parts[2], "t/")
|
||||||
|
if rel == "" {
|
||||||
|
// reading the directory itself as a file — shouldn't happen
|
||||||
|
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0}
|
||||||
|
}
|
||||||
|
s.log.Debug("Tread tool path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
|
||||||
|
content, err := storeReadCtx(ctx, s.toolStore, rel)
|
||||||
|
if err != nil {
|
||||||
|
return errFcall(fc, err.Error())
|
||||||
|
}
|
||||||
|
if rel == "idx" {
|
||||||
|
if allowed := s.sessionAllowTools(parts[1]); allowed != nil {
|
||||||
|
content = filterToolIndex(content, allowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.readSlice(fc, content)
|
||||||
|
}
|
||||||
s.log.Debug("Tread session file path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
|
s.log.Debug("Tread session file path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
|
||||||
sfs, ok := s.sessionFileStore(parts[1])
|
sfs, ok := s.sessionFileStore(parts[1])
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -1093,26 +1138,25 @@ func (s *Server) wstat(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
f.qid.Path = qidPath(f.path)
|
f.qid.Path = qidPath(f.path)
|
||||||
cs.mu.Unlock()
|
cs.mu.Unlock()
|
||||||
|
|
||||||
case strings.HasPrefix(f.path, "/t/"):
|
case strings.HasPrefix(f.path, "/s/"):
|
||||||
oldRel := strings.TrimPrefix(f.path, "/t/")
|
parts := strings.SplitN(strings.TrimPrefix(f.path, "/s/"), "/", 3)
|
||||||
parentRel := oldRel[:len(oldRel)-len(oldName)]
|
if len(parts) == 3 && parts[1] == "t" {
|
||||||
newRel := parentRel + newDir.Name
|
// Tool rename under session: /s/{sid}/t/{rel}
|
||||||
if err := s.toolStore.Rename(oldRel, newRel); err != nil {
|
oldRel := parts[2]
|
||||||
return errFcall(fc, err.Error())
|
parentRel := oldRel[:len(oldRel)-len(oldName)]
|
||||||
}
|
newRel := parentRel + newDir.Name
|
||||||
cs.mu.Lock()
|
if err := s.toolStore.Rename(oldRel, newRel); err != nil {
|
||||||
f.path = "/t/" + newRel
|
return errFcall(fc, err.Error())
|
||||||
f.qid.Path = qidPath(f.path)
|
}
|
||||||
cs.mu.Unlock()
|
cs.mu.Lock()
|
||||||
|
f.path = "/s/" + parts[0] + "/t/" + newRel
|
||||||
case strings.HasPrefix(f.path, "/s/") && f.path != "/s/new":
|
f.qid.Path = qidPath(f.path)
|
||||||
// Session directory rename: /s/{old} -> /s/{new}
|
cs.mu.Unlock()
|
||||||
parts := strings.SplitN(strings.TrimPrefix(f.path, "/"), "/", 3)
|
} else if len(parts) == 1 && f.path != "/s/new" {
|
||||||
if len(parts) != 2 || parts[0] != "s" {
|
// Session directory rename: /s/{old} -> /s/{new}
|
||||||
return errFcall(fc, "rename not supported")
|
if err := s.sessionStore.Rename(parts[0], newDir.Name); err != nil {
|
||||||
}
|
return errFcall(fc, err.Error())
|
||||||
if err := s.sessionStore.Rename(parts[1], newDir.Name); err != nil {
|
}
|
||||||
return errFcall(fc, err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
@ -1187,18 +1231,28 @@ func (s *Server) remove(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
err = s.memStore.Delete(pathBase(path))
|
err = s.memStore.Delete(pathBase(path))
|
||||||
case strings.HasPrefix(path, "/sk/"):
|
case strings.HasPrefix(path, "/sk/"):
|
||||||
err = s.skillStore.Delete(pathBase(path))
|
err = s.skillStore.Delete(pathBase(path))
|
||||||
case strings.HasPrefix(path, "/t/"):
|
|
||||||
err = s.toolStore.Delete(strings.TrimPrefix(path, "/t/"))
|
|
||||||
case strings.HasPrefix(path, "/tmp/"):
|
case strings.HasPrefix(path, "/tmp/"):
|
||||||
err = s.tmpStore.Delete(pathBase(path))
|
err = s.tmpStore.Delete(pathBase(path))
|
||||||
case strings.HasPrefix(path, "/tr/"):
|
case strings.HasPrefix(path, "/tr/"):
|
||||||
err = s.transcriptStore.Delete(pathBase(path))
|
err = s.transcriptStore.Delete(pathBase(path))
|
||||||
case strings.HasPrefix(path, "/s/") && path != "/s/new":
|
case strings.HasPrefix(path, "/s/") && path != "/s/new":
|
||||||
// If path is /s/{id}/{file}, it's a synthetic session file — no-op so
|
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 3)
|
||||||
// that "rm -r s/{id}" can proceed to remove the directory itself, which
|
if len(parts) >= 2 && (parts[1] == "t" || strings.HasPrefix(parts[1], "t/")) {
|
||||||
// triggers the actual session kill via Delete({id}).
|
// Tool remove: /s/{sid}/t/{tool}
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 2)
|
// Reconstruct: parts[0]=sid, rest starts with t/...
|
||||||
if len(parts) == 2 {
|
// Actually with SplitN(..., 3): parts = [sid, "t", rest] or [sid, "t"]
|
||||||
|
// But we split on /s/ prefix first, so path="/s/sid/t/file" -> "sid/t/file" split into ["sid","t","file"] or ["sid","t/file"]
|
||||||
|
// Wait — SplitN("sid/t/file", "/", 3) = ["sid", "t", "file"]
|
||||||
|
// SplitN("sid/t/sub/file", "/", 3) = ["sid", "t", "sub/file"]
|
||||||
|
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 {
|
||||||
|
// Session file remove
|
||||||
err = nil // synthetic file; let rm -r continue
|
err = nil // synthetic file; let rm -r continue
|
||||||
} else {
|
} else {
|
||||||
err = s.sessionStore.Delete(parts[0])
|
err = s.sessionStore.Delete(parts[0])
|
||||||
|
|
@ -1240,11 +1294,6 @@ func (s *Server) handleWrite(path, input string) error {
|
||||||
return storeWrite(s.skillStore, pathBase(path), []byte(input))
|
return storeWrite(s.skillStore, pathBase(path), []byte(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool file writes go to the tool store.
|
|
||||||
if strings.HasPrefix(path, "/t/") {
|
|
||||||
return storeWrite(s.toolStore, strings.TrimPrefix(path, "/t/"), []byte(input))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tmp file writes go to the tmp store.
|
// Tmp file writes go to the tmp store.
|
||||||
if strings.HasPrefix(path, "/tmp/") {
|
if strings.HasPrefix(path, "/tmp/") {
|
||||||
return storeWrite(s.tmpStore, pathBase(path), []byte(input))
|
return storeWrite(s.tmpStore, pathBase(path), []byte(input))
|
||||||
|
|
@ -1254,6 +1303,11 @@ func (s *Server) handleWrite(path, input string) error {
|
||||||
if strings.HasPrefix(path, "/s/") {
|
if strings.HasPrefix(path, "/s/") {
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
||||||
if len(parts) == 3 {
|
if len(parts) == 3 {
|
||||||
|
// Tool file writes: /s/{id}/t/{tool}
|
||||||
|
if strings.HasPrefix(parts[2], "t/") {
|
||||||
|
rel := strings.TrimPrefix(parts[2], "t/")
|
||||||
|
return storeWrite(s.toolStore, rel, []byte(input))
|
||||||
|
}
|
||||||
sfs, ok := s.sessionFileStore(parts[1])
|
sfs, ok := s.sessionFileStore(parts[1])
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("session not found: %s", parts[1])
|
return fmt.Errorf("session not found: %s", parts[1])
|
||||||
|
|
@ -1295,7 +1349,6 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
||||||
dirs = append(dirs, makeDir("p", "/p", true, plan9.DMDIR|0555))
|
dirs = append(dirs, makeDir("p", "/p", true, plan9.DMDIR|0555))
|
||||||
dirs = append(dirs, makeDir("s", "/s", true, plan9.DMDIR|0555))
|
dirs = append(dirs, makeDir("s", "/s", true, plan9.DMDIR|0555))
|
||||||
dirs = append(dirs, makeDir("sk", "/sk", true, plan9.DMDIR|0555))
|
dirs = append(dirs, makeDir("sk", "/sk", true, plan9.DMDIR|0555))
|
||||||
dirs = append(dirs, makeDir("t", "/t", true, plan9.DMDIR|0777))
|
|
||||||
dirs = append(dirs, makeDir("tmp", "/tmp", true, plan9.DMDIR|0755))
|
dirs = append(dirs, makeDir("tmp", "/tmp", true, plan9.DMDIR|0755))
|
||||||
dirs = append(dirs, makeDir("u", "/u", true, plan9.DMDIR|0755))
|
dirs = append(dirs, makeDir("u", "/u", true, plan9.DMDIR|0755))
|
||||||
dirs = append(dirs, makeDir("x", "/x", true, plan9.DMDIR|0555))
|
dirs = append(dirs, makeDir("x", "/x", true, plan9.DMDIR|0555))
|
||||||
|
|
@ -1335,21 +1388,6 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
||||||
}
|
}
|
||||||
dirs = append(dirs, makeDir(e.Name(), "/sk/"+e.Name(), false, mode))
|
dirs = append(dirs, makeDir(e.Name(), "/sk/"+e.Name(), false, mode))
|
||||||
}
|
}
|
||||||
} else if path == "/t" {
|
|
||||||
entries, _ := s.toolStore.List()
|
|
||||||
for _, e := range entries {
|
|
||||||
mode := plan9.Perm(0777)
|
|
||||||
if e.Name() == "idx" {
|
|
||||||
mode = 0444
|
|
||||||
}
|
|
||||||
dirs = append(dirs, makeDir(e.Name(), "/t/"+e.Name(), e.IsDir(), mode|plan9.Perm(boolToDir(e.IsDir()))))
|
|
||||||
}
|
|
||||||
} else if strings.HasPrefix(path, "/t/") {
|
|
||||||
rel := strings.TrimPrefix(path, "/t/")
|
|
||||||
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 path == "/tmp" {
|
} else if path == "/tmp" {
|
||||||
entries, _ := s.tmpStore.List()
|
entries, _ := s.tmpStore.List()
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
|
|
@ -1395,15 +1433,46 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
||||||
}
|
}
|
||||||
dirs = append(dirs, makeDir(e.Name(), "/s/"+e.Name(), e.IsDir(), perm))
|
dirs = append(dirs, makeDir(e.Name(), "/s/"+e.Name(), e.IsDir(), perm))
|
||||||
}
|
}
|
||||||
} else {
|
} else if strings.HasPrefix(path, "/s/") {
|
||||||
// Session subdirectory: /s/{sessid}
|
// /s/{sid}/t — list tools
|
||||||
sessID := pathBase(path)
|
// /s/{sid}/t/{subdir} — list tool subdirectory
|
||||||
if sfs, ok := s.sessionFileStore(sessID); ok {
|
// /s/{sid} — list session files + t/
|
||||||
entries, _ := sfs.List()
|
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 3)
|
||||||
for _, e := range entries {
|
if len(parts) >= 2 && parts[1] == "t" {
|
||||||
info, _ := e.Info()
|
if len(parts) == 2 {
|
||||||
dirs = append(dirs, makeDir(e.Name(), path+"/"+e.Name(), false, plan9.Perm(info.Mode())))
|
// /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|0777))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1454,8 +1523,13 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
||||||
var mode plan9.Perm
|
var mode plan9.Perm
|
||||||
if isDir {
|
if isDir {
|
||||||
qid.Type = QTDir
|
qid.Type = QTDir
|
||||||
if path == "/t" || strings.HasPrefix(path, "/t/") {
|
if strings.HasPrefix(path, "/s/") {
|
||||||
mode = plan9.DMDIR | 0777
|
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 3)
|
||||||
|
if (len(parts) == 2 && parts[1] == "t") || (len(parts) == 3 && parts[1] == "t") {
|
||||||
|
mode = plan9.DMDIR | 0777
|
||||||
|
} else {
|
||||||
|
mode = plan9.DMDIR | 0555
|
||||||
|
}
|
||||||
} else if path == "/a" || path == "/m" || path == "/tmp" || path == "/u" {
|
} else if path == "/a" || path == "/m" || path == "/tmp" || path == "/u" {
|
||||||
mode = plan9.DMDIR | 0755
|
mode = plan9.DMDIR | 0755
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1487,10 +1561,15 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
||||||
mode = 0444
|
mode = 0444
|
||||||
} else if strings.HasPrefix(path, "/sk/") {
|
} else if strings.HasPrefix(path, "/sk/") {
|
||||||
mode = 0666
|
mode = 0666
|
||||||
} else if path == "/t/idx" {
|
} else if strings.HasPrefix(path, "/s/") {
|
||||||
mode = 0444
|
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 3)
|
||||||
} else if strings.HasPrefix(path, "/t/") {
|
if len(parts) == 3 && parts[1] == "t" && parts[2] == "idx" {
|
||||||
mode = 0777
|
mode = 0444
|
||||||
|
} else if len(parts) == 3 && parts[1] == "t" {
|
||||||
|
mode = 0777
|
||||||
|
} else {
|
||||||
|
mode = 0444
|
||||||
|
}
|
||||||
} else if strings.HasPrefix(path, "/u/") {
|
} else if strings.HasPrefix(path, "/u/") {
|
||||||
mode = 0555
|
mode = 0555
|
||||||
} else if strings.HasPrefix(path, "/x/") {
|
} else if strings.HasPrefix(path, "/x/") {
|
||||||
|
|
@ -1561,9 +1640,12 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
||||||
if content, err := storeRead(s.skillStore, base); err == nil {
|
if content, err := storeRead(s.skillStore, base); err == nil {
|
||||||
dir.Length = uint64(len(content))
|
dir.Length = uint64(len(content))
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(path, "/t/"):
|
case strings.HasPrefix(path, "/s/") && strings.Contains(path, "/t/"):
|
||||||
if content, err := storeRead(s.toolStore, strings.TrimPrefix(path, "/t/")); err == nil {
|
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 3)
|
||||||
dir.Length = uint64(len(content))
|
if len(parts) == 3 && parts[1] == "t" {
|
||||||
|
if content, err := storeRead(s.toolStore, parts[2]); err == nil {
|
||||||
|
dir.Length = uint64(len(content))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(path, "/u/"):
|
case strings.HasPrefix(path, "/u/"):
|
||||||
if content, err := storeRead(s.utilStore, base); err == nil {
|
if content, err := storeRead(s.utilStore, base); err == nil {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ type Session struct {
|
||||||
ChatOffset int
|
ChatOffset int
|
||||||
plan []byte
|
plan []byte
|
||||||
prevPrompt []byte // last submitted prompt; overwritten on each new submission
|
prevPrompt []byte // last submitted prompt; overwritten on each new submission
|
||||||
|
AllowTools []string // if non-empty, only these tools are visible/executable
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSession(id string, core agent.Core, ctx context.Context, cancel context.CancelFunc) *Session {
|
func NewSession(id string, core agent.Core, ctx context.Context, cancel context.CancelFunc) *Session {
|
||||||
|
|
@ -356,6 +357,7 @@ func (s *SessionStore) createSession(args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var core agent.Core
|
var core agent.Core
|
||||||
|
var allowTools []string
|
||||||
if s.cfg.NewCore != nil {
|
if s.cfg.NewCore != nil {
|
||||||
var err error
|
var err error
|
||||||
core, err = s.cfg.NewCore(sessID, agentName, cwd)
|
core, err = s.cfg.NewCore(sessID, agentName, cwd)
|
||||||
|
|
@ -380,6 +382,9 @@ func (s *SessionStore) createSession(args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := LoadAgentConfig(s.cfg.AgentsDir, agentName, nil)
|
cfg := LoadAgentConfig(s.cfg.AgentsDir, agentName, nil)
|
||||||
|
if cfg != nil {
|
||||||
|
allowTools = cfg.AllowTools
|
||||||
|
}
|
||||||
|
|
||||||
var execOpts []execute.Option
|
var execOpts []execute.Option
|
||||||
if s.cfg.Strict {
|
if s.cfg.Strict {
|
||||||
|
|
@ -388,6 +393,12 @@ func (s *SessionStore) createSession(args []string) error {
|
||||||
if s.cfg.Yolo {
|
if s.cfg.Yolo {
|
||||||
execOpts = append(execOpts, execute.WithYolo())
|
execOpts = append(execOpts, execute.WithYolo())
|
||||||
}
|
}
|
||||||
|
if cfg != nil && len(cfg.AllowExecutors) > 0 {
|
||||||
|
execOpts = append(execOpts, execute.WithAllowExecutors(cfg.AllowExecutors))
|
||||||
|
}
|
||||||
|
if cfg != nil && len(cfg.AllowTools) > 0 {
|
||||||
|
execOpts = append(execOpts, execute.WithAllowTools(cfg.AllowTools))
|
||||||
|
}
|
||||||
newDisp := tools.NewDispatcherFunc(map[string]func() tools.Server{
|
newDisp := tools.NewDispatcherFunc(map[string]func() tools.Server{
|
||||||
"execute": execute.Decl(cwd, execOpts...),
|
"execute": execute.Decl(cwd, execOpts...),
|
||||||
})
|
})
|
||||||
|
|
@ -409,6 +420,7 @@ func (s *SessionStore) createSession(args []string) error {
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
sess := NewSession(sessID, core, ctx, cancel)
|
sess := NewSession(sessID, core, ctx, cancel)
|
||||||
|
sess.AllowTools = allowTools
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.sessions[sessID] = sess
|
s.sessions[sessID] = sess
|
||||||
|
|
|
||||||
Reference in New Issue