diff --git a/cmd/ollie-remote/sandbox/default.yaml b/cmd/ollie-remote/sandbox/default.yaml new file mode 100644 index 0000000..fed5b27 --- /dev/null +++ b/cmd/ollie-remote/sandbox/default.yaml @@ -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 diff --git a/fs/session/files.go b/fs/session/files.go index 1f1e660..8e0d6c6 100644 --- a/fs/session/files.go +++ b/fs/session/files.go @@ -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") diff --git a/fs/session/perm.go b/fs/session/perm.go index 1e1a983..b8a5b7a 100644 --- a/fs/session/perm.go +++ b/fs/session/perm.go @@ -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,