session: add agent/{aid} sublevel routing
The 9P namespace now supports:
session/{sid}/plan, env (session-level)
session/{sid}/agent/{aid}/* (agent-level files)
session/{sid}/agent/{aid}/proc/* (agent detached processes)
Routing updated in stat, openEntry, and Readdir.
Existing flat access ({sid}/{file}) preserved for backward compat.
Tests pass unchanged.
This commit is contained in:
parent
44a6dda695
commit
2a3798ae39
|
|
@ -131,29 +131,57 @@ func (s *Manager) list() ([]os.DirEntry, error) {
|
|||
return entries, nil
|
||||
}
|
||||
|
||||
// Readdir lists entries in a subdirectory (e.g. "{id}", "{id}/peer").
|
||||
// Readdir lists entries in a subdirectory.
|
||||
func (s *Manager) Readdir(name string) ([]os.DirEntry, error) {
|
||||
parts := strings.SplitN(name, "/", 3)
|
||||
parts := strings.SplitN(name, "/", 4)
|
||||
sessID := parts[0]
|
||||
sess := s.Session(sessID)
|
||||
if sess == nil {
|
||||
return nil, fmt.Errorf("session not found: %s", sessID)
|
||||
}
|
||||
// {id} — list session files + peer/
|
||||
// {id} — list session-level files + agent/
|
||||
if len(parts) == 1 {
|
||||
sfs, err := s.openStore(sess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
entries := []os.DirEntry{
|
||||
fs.FileEntry("plan", 0666),
|
||||
fs.FileEntry("env", 0444),
|
||||
fs.DirEntry("agent", 0755),
|
||||
}
|
||||
entries, err := sfs.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Append peer/ and proc/ directories
|
||||
entries = append(entries, fs.DirEntry("peer", 0755))
|
||||
entries = append(entries, fs.DirEntry("proc", 0755))
|
||||
return entries, nil
|
||||
}
|
||||
// {id}/agent — list agent IDs
|
||||
if parts[1] == "agent" {
|
||||
if len(parts) == 2 {
|
||||
// Currently single-agent: use the agent's ID
|
||||
aid := sess.Core.Agent().ID()
|
||||
if aid == "" {
|
||||
aid = "0" // fallback for sessions without explicit agent ID
|
||||
}
|
||||
entries := []os.DirEntry{fs.DirEntry(aid, 0755)}
|
||||
return entries, nil
|
||||
}
|
||||
// {id}/agent/{aid} — list agent files + proc/
|
||||
if len(parts) == 3 {
|
||||
afs, err := s.openAgentStore(sess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := afs.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, fs.DirEntry("proc", 0755))
|
||||
return entries, nil
|
||||
}
|
||||
// {id}/agent/{aid}/proc — list detached process PIDs
|
||||
if len(parts) == 4 && parts[3] == "proc" {
|
||||
procs := sess.Core.ListDetached()
|
||||
entries := make([]os.DirEntry, len(procs))
|
||||
for i, p := range procs {
|
||||
entries[i] = fs.FileEntry(fmt.Sprintf("%d", p.PID), 0666)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
}
|
||||
// {id}/peer — list peer session IDs
|
||||
if len(parts) == 2 && parts[1] == "peer" {
|
||||
peers, _ := s.PeerList(sessID)
|
||||
|
|
@ -163,15 +191,6 @@ func (s *Manager) Readdir(name string) ([]os.DirEntry, error) {
|
|||
}
|
||||
return entries, nil
|
||||
}
|
||||
// {id}/proc — list detached process PIDs
|
||||
if len(parts) == 2 && parts[1] == "proc" {
|
||||
procs := sess.Core.ListDetached()
|
||||
entries := make([]os.DirEntry, len(procs))
|
||||
for i, p := range procs {
|
||||
entries[i] = fs.FileEntry(fmt.Sprintf("%d", p.PID), 0666)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%s: not a directory", name)
|
||||
}
|
||||
|
||||
|
|
@ -275,7 +294,7 @@ func (s *Manager) stat(name string) (os.FileInfo, error) {
|
|||
if mode, ok := fs.Perms[fs.PathSessions].Files[name]; ok {
|
||||
return &fs.SyntheticFileInfo{Name_: name, Mode_: mode}, nil
|
||||
}
|
||||
parts := strings.SplitN(name, "/", 3)
|
||||
parts := strings.SplitN(name, "/", 4)
|
||||
sessID := parts[0]
|
||||
s.mu.RLock()
|
||||
sess, ok := s.sessions[sessID]
|
||||
|
|
@ -287,6 +306,25 @@ func (s *Manager) stat(name string) (os.FileInfo, error) {
|
|||
if len(parts) == 1 {
|
||||
return &fs.SyntheticFileInfo{Name_: sessID, Mode_: fs.Perms[fs.PathSessionDir].DirMode, IsDir_: true}, nil
|
||||
}
|
||||
// Agent directory: {id}/agent
|
||||
if parts[1] == "agent" {
|
||||
if len(parts) == 2 {
|
||||
return &fs.SyntheticFileInfo{Name_: "agent", Mode_: 0755, IsDir_: true}, nil
|
||||
}
|
||||
// Agent instance: {id}/agent/{aid}
|
||||
if len(parts) == 3 {
|
||||
return &fs.SyntheticFileInfo{Name_: parts[2], Mode_: 0755, IsDir_: true}, nil
|
||||
}
|
||||
// Agent file: {id}/agent/{aid}/{file}
|
||||
if parts[3] == "proc" {
|
||||
return &fs.SyntheticFileInfo{Name_: "proc", Mode_: 0755, IsDir_: true}, nil
|
||||
}
|
||||
afs, err := s.openAgentStore(sess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return afs.Stat(parts[3])
|
||||
}
|
||||
// Peer directory: {id}/peer
|
||||
if parts[1] == "peer" {
|
||||
if len(parts) == 2 {
|
||||
|
|
@ -302,15 +340,7 @@ func (s *Manager) stat(name string) (os.FileInfo, error) {
|
|||
}
|
||||
return &fs.SyntheticFileInfo{Name_: peerID, Mode_: 0666}, nil
|
||||
}
|
||||
// Proc directory: {id}/proc
|
||||
if parts[1] == "proc" {
|
||||
if len(parts) == 2 {
|
||||
return &fs.SyntheticFileInfo{Name_: "proc", Mode_: 0755, IsDir_: true}, nil
|
||||
}
|
||||
// Proc file: {id}/proc/{pid} — read-only output
|
||||
return &fs.SyntheticFileInfo{Name_: parts[2], Mode_: 0444}, nil
|
||||
}
|
||||
// Session file: {id}/{file}
|
||||
// Session file: {id}/{file} (plan, env)
|
||||
sfs, err := s.openStore(sess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -414,8 +444,8 @@ func (s *Manager) openEntry(name string) (fs.File, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// Hierarchical paths: {id}/{file} or {id}/t/{tool}
|
||||
parts = strings.SplitN(name, "/", 3)
|
||||
// Hierarchical paths: {id}/{file} or {id}/agent/{aid}/{file}
|
||||
parts = strings.SplitN(name, "/", 4)
|
||||
sessID := parts[0]
|
||||
sess := s.Session(sessID)
|
||||
if sess == nil {
|
||||
|
|
@ -424,6 +454,20 @@ func (s *Manager) openEntry(name string) (fs.File, error) {
|
|||
if len(parts) == 1 {
|
||||
return nil, fmt.Errorf("%s: is a directory", name)
|
||||
}
|
||||
|
||||
// Agent file: {id}/agent/{aid}/{file}
|
||||
if parts[1] == "agent" {
|
||||
if len(parts) < 4 {
|
||||
return nil, fmt.Errorf("%s: is a directory", name)
|
||||
}
|
||||
// parts[2] = aid, parts[3] = file
|
||||
afs, err := s.openAgentStore(sess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return afs.Open(parts[3])
|
||||
}
|
||||
|
||||
// Session file: {id}/{file}
|
||||
sfs, err := s.openStore(sess)
|
||||
if err != nil {
|
||||
|
|
@ -523,6 +567,22 @@ func (s *Manager) openStore(sess *Session) (*fs.Tree, error) {
|
|||
), nil
|
||||
}
|
||||
|
||||
func (s *Manager) openAgentStore(sess *Session) (*fs.Tree, error) {
|
||||
var resetElev func()
|
||||
if s.cfg.ResetElevation != nil {
|
||||
id := sess.id
|
||||
resetElev = func() { s.cfg.ResetElevation(id) }
|
||||
}
|
||||
return NewAgentTree(
|
||||
sess,
|
||||
s.cfg.Log,
|
||||
nil, // no transcript saving
|
||||
s.cfg.InvalidateModels,
|
||||
resetElev,
|
||||
s.cfg.ToolRegistry,
|
||||
), nil
|
||||
}
|
||||
|
||||
// InterruptAll interrupts every active session.
|
||||
func (s *Manager) InterruptAll() {
|
||||
s.mu.RLock()
|
||||
|
|
|
|||
Reference in New Issue