decouple session and agent files in 9P tree
Session-level files (session/{id}/): plan, env, ctl
Agent-level files (session/{id}/agent/{aid}/): cwd, prompt, log, chat,
cfg, state, statewait, usage, cost, ctxsz, models, systemprompt,
tail, prompt.prev, context, toolsrv.loaded, toolsrv.rev, tools
Key changes:
- Split sessionHelper into sessionHelper and agentHelper types
- ctl moved to session level (kill, rn, save, invalidate)
- cwd moved to agent level (read/write)
- Agent cfg no longer supports rename (use session/ctl rn)
- Permissions updated: cwd=0666, ctl=0666, env=0444 at session level
- Removed duplicate envContent method
This commit is contained in:
parent
3046afd8ed
commit
fb1d10141b
|
|
@ -0,0 +1,174 @@
|
|||
# Default execute_code sandbox configuration.
|
||||
filesystem:
|
||||
ro:
|
||||
- "{XDG_CONFIG_HOME}/git"
|
||||
- "{HOME}/.netrc"
|
||||
- "/etc/gitconfig"
|
||||
- "/etc/gitattributes"
|
||||
- "/etc/ssh/ssh_config"
|
||||
- "/etc/ssh/ssh_known_hosts"
|
||||
- "/etc/magic"
|
||||
- "/var/log"
|
||||
- "/run/systemd"
|
||||
- "/var/log/journal"
|
||||
- "/etc/systemd"
|
||||
- "/proc"
|
||||
- "/sys"
|
||||
- "{HOME}/.jenkins"
|
||||
- "{HOME}/.psqlrc"
|
||||
- "{HOME}/.pgpass"
|
||||
- "{HOME}/.aws"
|
||||
- "{HOME}/.ansible"
|
||||
- "{HOME}/.ansible.cfg"
|
||||
- "{HOME}/.jira.d"
|
||||
- "{HOME}/.my.cnf"
|
||||
- "{HOME}/.mylogin.cnf"
|
||||
- "{XDG_CONFIG_HOME}/gh"
|
||||
- "{XDG_DATA_HOME}/gh/extensions"
|
||||
- "/dev"
|
||||
- "{HOME}/img"
|
||||
- "{OLLIE_CFG_PATH}"
|
||||
rw:
|
||||
- "{OLLIE_MEMORY_PATH}"
|
||||
- "{OLLIE_PLAN_PATH}"
|
||||
- "{HOME}/.ssh/known_hosts"
|
||||
- "{HOME}/pCloudDrive"
|
||||
- "{HOME}/.pcloud"
|
||||
- "{HOME}/.git-credentials"
|
||||
- "{XDG_CONFIG_HOME}/git/credentials"
|
||||
- "{XDG_RUNTIME_DIR}/git/"
|
||||
- "{HOME}/.mcpt"
|
||||
- "{HOME}/doc"
|
||||
- "{HOME}/.cache/mise"
|
||||
- "{HOME}/.locale/state/mise"
|
||||
- "{HOME}/.gnupg"
|
||||
- "{XDG_DOCUMENTS_DIR}"
|
||||
- "{XDG_CONFIG_HOME}/.jira"
|
||||
- "{HOME}/.m2"
|
||||
- "{HOME}/.gradle"
|
||||
- "/var/run/docker.sock"
|
||||
- "{HOME}/.docker"
|
||||
- "{HOME}/.pnpm-store"
|
||||
- "{XDG_CONFIG_HOME}/yarn"
|
||||
- "{HOME}/.yarnrc.yml"
|
||||
- "{HOME}/.npmrc"
|
||||
- "{HOME}/.ansible/tmp"
|
||||
- "{XDG_RUNTIME_DIR}"
|
||||
- "{XDG_DATA_HOME}/containers"
|
||||
- "{XDG_CONFIG_HOME}/containers"
|
||||
- "{XDG_CONFIG_HOME}/pip"
|
||||
- "{HOME}/.pip"
|
||||
- "{HOME}/.cargo"
|
||||
- "{HOME}/.rustup"
|
||||
- "{XDG_CONFIG_HOME}/nvim"
|
||||
- "{XDG_CONFIG_HOME}/vim"
|
||||
- "{XDG_DATA_HOME}/nvim"
|
||||
- "{XDG_STATE_HOME}/nvim"
|
||||
- "{OLLIE_MEMORY_PATH}"
|
||||
- "{OLLIE_PLAN_PATH}"
|
||||
- "{HOME}/.vimrc"
|
||||
- "{HOME}/.vim"
|
||||
- "{HOME}/.emacs.d"
|
||||
- "{HOME}/.config/emacs"
|
||||
- "/etc"
|
||||
- "/usr/local/etc"
|
||||
- "/dev/urandom"
|
||||
- "/dev/random"
|
||||
- "/dev/null"
|
||||
- "/dev/fuse"
|
||||
- "{OLLIE_DATA_PATH}"
|
||||
- "{HOME}/notes"
|
||||
rox:
|
||||
- "/usr"
|
||||
- "/lib"
|
||||
- "/lib64"
|
||||
- "/bin"
|
||||
- "/sbin"
|
||||
- "{HOME}/go/bin"
|
||||
- "{HOME}/env"
|
||||
- "/proc/self/fd"
|
||||
- "/proc/self/cmdline"
|
||||
- "{OLLIE_TOOLS_PATH}"
|
||||
rwx:
|
||||
- "{CWD}"
|
||||
- "{HOME}/Qt"
|
||||
- "{HOME}/.local/share/pnpm"
|
||||
- "{HOME}/.local/share/pipx"
|
||||
- "{HOME}/.local/share/aqt/"
|
||||
- "{HOME}/prj/aicoder"
|
||||
- "{TMPDIR}"
|
||||
- "{PLAN9}"
|
||||
- "{HOME}/.cache/uv"
|
||||
- "{HOME}/bin"
|
||||
- "{OLLIE}"
|
||||
- "{OLLIE_CFG_PATH}"
|
||||
- "{OLLIE_DATA_PATH}"
|
||||
- "{OLLIE_MEMORY_PATH}"
|
||||
- "{OLLIE_PLAN_PATH}"
|
||||
- "{OLLIE_SKILLS_PATH}"
|
||||
- "{OLLIE_TMP_PATH}"
|
||||
- "{OLLIE_TOOLS_PATH}"
|
||||
- "{OLLIE_TRANSCRIPT_PATH}"
|
||||
- "{OLLIE_AGENTS_PATH}"
|
||||
- "{OLLIE_PROMPTS_PATH}"
|
||||
# XDG defaults for OLLIE_* paths (in case env vars are unset)
|
||||
- "{XDG_CONFIG_HOME}/ollie"
|
||||
- "{XDG_DATA_HOME}/ollie"
|
||||
- "{HOME}/.pyenv"
|
||||
- "{HOME}/.local/bin"
|
||||
- "{HOME}/.sdkman"
|
||||
- "{HOME}/go"
|
||||
- "{HOME}/.cache/go-build"
|
||||
- "{HOME}/opt"
|
||||
- "{HOME}/src"
|
||||
- "{HOME}/prj"
|
||||
- "{HOME}/.nvm"
|
||||
- "{HOME}/.please"
|
||||
- "{HOME}/.npm"
|
||||
- "{HOME}/.mcp-auth"
|
||||
- "{HOME}/.yarn"
|
||||
- "{HOME}/.local/share/mise"
|
||||
- "{PCFS}"
|
||||
- "{XDG_RUNTIME_DIR}/{WAYLAND_DISPLAY}"
|
||||
- "{HOME}/ws"
|
||||
env:
|
||||
- PATH
|
||||
- NAMESPACE
|
||||
- OLLIE
|
||||
- OLLIE_DEFAULT_AGENT
|
||||
- OLLIE_CFG_PATH
|
||||
- OLLIE_DATA_PATH
|
||||
- OLLIE_MEMORY_PATH
|
||||
- OLLIE_PLAN_PATH
|
||||
- OLLIE_SESSION_ID
|
||||
- OLLIE_SKILLS_PATH
|
||||
- OLLIE_TMP_PATH
|
||||
- OLLIE_ELEVATE_SOCKET
|
||||
- OLLIE_TOOLS_PATH
|
||||
- OLLIE_TRANSCRIPT_PATH
|
||||
- OLLIE_AGENTS_PATH
|
||||
- OLLIE_PROMPTS_PATH
|
||||
- OLLIE_UNAME
|
||||
- OLLIE_LOGSEQ_TOKEN
|
||||
- HOME
|
||||
- TMPDIR
|
||||
- PLAN9
|
||||
- JENKINS_USER_ID
|
||||
- JENKINS_API_TOKEN
|
||||
- JENKINS_MCP_AUTH
|
||||
- JIRA_API_TOKEN
|
||||
- BITBUCKET_ACCESS_TOKEN
|
||||
- BITBUCKET_TOKEN
|
||||
- BITBUCKET_USERNAME
|
||||
- PCLOUD_USER
|
||||
- EDITOR
|
||||
- GIT_SEQUENCE_EDITOR
|
||||
- DISPLAY
|
||||
- DBUS_SESSION_BUS_ADDRESS
|
||||
- PCFS
|
||||
- SSH_AUTH_SOCK
|
||||
- XDG_SESSION_TYPE
|
||||
- XDG_RUNTIME_DIR
|
||||
- WAYLAND_DISPLAY
|
||||
network:
|
||||
unrestricted: true
|
||||
|
|
@ -22,6 +22,7 @@ var SessionFileList = []struct {
|
|||
}{
|
||||
{"plan", false, false},
|
||||
{"env", false, false},
|
||||
{"ctl", false, true},
|
||||
}
|
||||
|
||||
// AgentFileList defines agent-level files.
|
||||
|
|
@ -30,7 +31,7 @@ var AgentFileList = []struct {
|
|||
OneShot bool
|
||||
Async bool
|
||||
}{
|
||||
{"ctl", false, true},
|
||||
{"cwd", false, false},
|
||||
{"prompt", false, true},
|
||||
{"fifo.in", false, true},
|
||||
{"fifo.out", true, false},
|
||||
|
|
@ -75,7 +76,7 @@ func NewSessionTree(sess *Session, log *olog.Logger, kill func(), rename func(ne
|
|||
|
||||
// NewAgentTree builds the file tree for a single agent within a session.
|
||||
func NewAgentTree(sess *Session, log *olog.Logger, saveTranscript func([]byte) error, invalidateModels func(), toolRegistry *toolsrv.Registry) *Tree {
|
||||
h := &sessionHelper{sess: sess, log: log, saveTranscript: saveTranscript, invalidateModels: invalidateModels, toolRegistry: toolRegistry}
|
||||
h := &agentHelper{sess: sess, log: log, saveTranscript: saveTranscript, invalidateModels: invalidateModels, toolRegistry: toolRegistry}
|
||||
perms := sessionFilePerms()
|
||||
specs := make([]FileSpec, len(AgentFileList))
|
||||
for i, f := range AgentFileList {
|
||||
|
|
@ -97,10 +98,18 @@ type sessionHelper struct {
|
|||
toolRegistry *toolsrv.Registry
|
||||
}
|
||||
|
||||
// agentHelper holds the dependencies needed to build agent FileSpecs.
|
||||
type agentHelper struct {
|
||||
sess *Session
|
||||
log *olog.Logger
|
||||
saveTranscript func([]byte) error
|
||||
invalidateModels func()
|
||||
toolRegistry *toolsrv.Registry
|
||||
}
|
||||
|
||||
func (h *sessionHelper) fileSpec(name string, mode os.FileMode) FileSpec {
|
||||
fs := FileSpec{Name: name, Mode: mode}
|
||||
|
||||
// Read
|
||||
switch name {
|
||||
case "plan":
|
||||
fs.Read = func() ([]byte, error) {
|
||||
|
|
@ -116,6 +125,80 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) FileSpec {
|
|||
h.sess.mu.RUnlock()
|
||||
return int64(n)
|
||||
}
|
||||
fs.Write = func(data []byte) error {
|
||||
h.sess.mu.Lock()
|
||||
h.sess.plan = make([]byte, len(data))
|
||||
copy(h.sess.plan, data)
|
||||
h.sess.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
case "env":
|
||||
fs.Read = func() ([]byte, error) { return []byte(h.envContent()), nil }
|
||||
case "ctl":
|
||||
fs.Write = func(data []byte) error {
|
||||
input := strings.TrimSpace(string(data))
|
||||
if input == "" {
|
||||
return nil
|
||||
}
|
||||
return h.handleCtl(input)
|
||||
}
|
||||
}
|
||||
|
||||
return fs
|
||||
}
|
||||
|
||||
func (h *sessionHelper) envContent() string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "OLLIE_SESSION_ID=%s\n", h.sess.RunnableID())
|
||||
for _, e := range os.Environ() {
|
||||
if strings.HasPrefix(e, "OLLIE_") && !strings.HasPrefix(e, "OLLIE_SESSION_ID=") {
|
||||
sb.WriteString(e)
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (h *sessionHelper) handleCtl(input string) error {
|
||||
cmd := strings.Fields(input)
|
||||
if len(cmd) == 0 {
|
||||
return fmt.Errorf("empty ctl command")
|
||||
}
|
||||
switch cmd[0] {
|
||||
case "kill", ".":
|
||||
h.kill()
|
||||
case "rn":
|
||||
if name := strings.TrimSpace(input[3:]); name != "" {
|
||||
if err := h.rename(name); err != nil {
|
||||
h.log.Error("rename: %v", err)
|
||||
}
|
||||
}
|
||||
case "save":
|
||||
h.sess.mu.RLock()
|
||||
data := make([]byte, len(h.sess.log))
|
||||
copy(data, h.sess.log)
|
||||
h.sess.mu.RUnlock()
|
||||
return h.saveTranscript(data)
|
||||
case "invalidate":
|
||||
if h.invalidateModels != nil {
|
||||
h.invalidateModels()
|
||||
}
|
||||
h.sess.InvalidateModelsCache()
|
||||
default:
|
||||
return fmt.Errorf("unknown session ctl command: %s", cmd[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *agentHelper) fileSpec(name string, mode os.FileMode) FileSpec {
|
||||
fs := FileSpec{Name: name, Mode: mode}
|
||||
|
||||
// Read
|
||||
switch name {
|
||||
case "cwd":
|
||||
fs.Read = func() ([]byte, error) {
|
||||
return []byte(h.sess.Core.CWD() + "\n"), nil
|
||||
}
|
||||
case "prompt.prev":
|
||||
fs.Read = func() ([]byte, error) {
|
||||
h.sess.mu.RLock()
|
||||
|
|
@ -164,13 +247,13 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) FileSpec {
|
|||
|
||||
// Write
|
||||
switch name {
|
||||
case "plan":
|
||||
case "cwd":
|
||||
fs.Write = func(data []byte) error {
|
||||
h.sess.mu.Lock()
|
||||
h.sess.plan = make([]byte, len(data))
|
||||
copy(h.sess.plan, data)
|
||||
h.sess.mu.Unlock()
|
||||
return nil
|
||||
input := strings.TrimSpace(string(data))
|
||||
if input == "" {
|
||||
return nil
|
||||
}
|
||||
return h.sess.Core.SetCWD(input)
|
||||
}
|
||||
case "log":
|
||||
fs.Write = func(data []byte) error {
|
||||
|
|
@ -212,14 +295,6 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) FileSpec {
|
|||
h.sess.Core.Agent().Queue(input)
|
||||
return nil
|
||||
}
|
||||
case "ctl":
|
||||
fs.Write = func(data []byte) error {
|
||||
input := strings.TrimSpace(string(data))
|
||||
if input == "" {
|
||||
return nil
|
||||
}
|
||||
return h.handleCtl(input)
|
||||
}
|
||||
case "cfg":
|
||||
fs.Write = func(data []byte) error {
|
||||
input := strings.TrimSpace(string(data))
|
||||
|
|
@ -283,7 +358,6 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) FileSpec {
|
|||
for len(h.sess.log) <= offset {
|
||||
if ctx.Err() != nil {
|
||||
h.sess.mu.RUnlock()
|
||||
// Context cancelled (session killed or connection closed) → EOF
|
||||
return nil, "", nil
|
||||
}
|
||||
h.sess.chatCond.Wait()
|
||||
|
|
@ -303,6 +377,16 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) FileSpec {
|
|||
}
|
||||
|
||||
func (h *sessionHelper) content(name string) string {
|
||||
h.sess.mu.RLock()
|
||||
defer h.sess.mu.RUnlock()
|
||||
switch name {
|
||||
case "env":
|
||||
return h.envContent()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *agentHelper) content(name string) string {
|
||||
h.sess.mu.RLock()
|
||||
defer h.sess.mu.RUnlock()
|
||||
switch name {
|
||||
|
|
@ -321,8 +405,6 @@ func (h *sessionHelper) content(name string) string {
|
|||
return h.sess.Core.Agent().State() + "\n"
|
||||
case "offset":
|
||||
return fmt.Sprintf("%d\n", h.sess.ChatOffset)
|
||||
case "env":
|
||||
return h.envContent()
|
||||
case "tail":
|
||||
return "#!/bin/sh\nexec tail -f \"$(dirname \"$0\")/chat\"\n"
|
||||
case "context":
|
||||
|
|
@ -372,19 +454,7 @@ func (h *sessionHelper) content(name string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (h *sessionHelper) envContent() string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "OLLIE_SESSION_ID=%s\n", h.sess.RunnableID())
|
||||
for _, e := range os.Environ() {
|
||||
if strings.HasPrefix(e, "OLLIE_") && !strings.HasPrefix(e, "OLLIE_SESSION_ID=") {
|
||||
sb.WriteString(e)
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (h *sessionHelper) cfgContent() string {
|
||||
func (h *agentHelper) cfgContent() string {
|
||||
h.sess.mu.RLock()
|
||||
defer h.sess.mu.RUnlock()
|
||||
p := h.sess.Core.Agent().GenParams()
|
||||
|
|
@ -467,7 +537,7 @@ func (h *sessionHelper) cfgContent() string {
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
func (h *sessionHelper) handleCfg(input string) error {
|
||||
func (h *agentHelper) handleCfg(input string) error {
|
||||
p := h.sess.Core.Agent().GenParams()
|
||||
hasParams := false
|
||||
var deferredBackend, deferredModel string
|
||||
|
|
@ -489,9 +559,7 @@ func (h *sessionHelper) handleCfg(input string) error {
|
|||
if h.sess.Core.Agent().IsRunning() {
|
||||
return fmt.Errorf("cannot rename while agent is running")
|
||||
}
|
||||
if err := h.rename(v); err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("rename not supported via agent cfg; use session/ctl rn")
|
||||
case "agent":
|
||||
if v == "" {
|
||||
continue
|
||||
|
|
@ -518,9 +586,7 @@ func (h *sessionHelper) handleCfg(input string) error {
|
|||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if err := h.sess.Core.SetCWD(v); err != nil {
|
||||
return err
|
||||
}
|
||||
return h.sess.Core.SetCWD(v)
|
||||
case "maxTokens":
|
||||
hasParams = true
|
||||
if v == "" {
|
||||
|
|
@ -643,7 +709,7 @@ func (h *sessionHelper) handleCfg(input string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (h *sessionHelper) handleCtl(input string) error {
|
||||
func (h *agentHelper) handleCtl(input string) error {
|
||||
cmd := strings.Fields(input)
|
||||
if len(cmd) == 0 {
|
||||
return fmt.Errorf("empty ctl command")
|
||||
|
|
@ -655,19 +721,7 @@ func (h *sessionHelper) handleCtl(input string) error {
|
|||
// FIXME: kill currently kills the entire session because we only support
|
||||
// one agent per session. When multi-agent-per-session lands, kill should
|
||||
// kill the agent only; session and chat stream persist.
|
||||
h.kill()
|
||||
case "rn":
|
||||
if name := strings.TrimSpace(input[3:]); name != "" {
|
||||
if err := h.rename(name); err != nil {
|
||||
h.log.Error("rename: %v", err)
|
||||
}
|
||||
}
|
||||
case "save":
|
||||
h.sess.mu.RLock()
|
||||
data := make([]byte, len(h.sess.log))
|
||||
copy(data, h.sess.log)
|
||||
h.sess.mu.RUnlock()
|
||||
return h.saveTranscript(data)
|
||||
h.sess.Cancel()
|
||||
case "detach":
|
||||
if !h.sess.Core.Agent().Detach() {
|
||||
return fmt.Errorf("no running process to detach")
|
||||
|
|
|
|||
|
|
@ -65,9 +65,10 @@ var Perms = map[string]Perm{
|
|||
PathTools: {DirMode: 0755, FileMode: 0755},
|
||||
PathSessionFile: {
|
||||
Files: map[string]os.FileMode{
|
||||
"elevate": 0666,
|
||||
"plan": 0666,
|
||||
"cwd": 0666,
|
||||
"ctl": 0666,
|
||||
"plan": 0666,
|
||||
"env": 0444,
|
||||
"prompt": 0666,
|
||||
"fifo.in": 0222,
|
||||
"fifo.out": 0444,
|
||||
|
|
@ -82,7 +83,6 @@ var Perms = map[string]Perm{
|
|||
"ctxsz": 0444,
|
||||
"models": 0444,
|
||||
"systemprompt": 0444,
|
||||
"env": 0444,
|
||||
"tail": 0555,
|
||||
"prompt.prev": 0444,
|
||||
"context": 0444,
|
||||
|
|
|
|||
Loading…
Reference in New Issue