InitSessionWorkspace creates the per-session workspace and registers its subdirs with the engine + read validators. Idempotent: calling it twice returns the existing workspace. The returned workspace should have Cleanup() called at CLI shutdown.
(logger *zap.Logger)
| 98 | // |
| 99 | // The returned workspace should have Cleanup() called at CLI shutdown. |
| 100 | func InitSessionWorkspace(logger *zap.Logger) (*SessionWorkspace, error) { |
| 101 | sessionWorkspaceMu.Lock() |
| 102 | defer sessionWorkspaceMu.Unlock() |
| 103 | |
| 104 | if sessionWorkspace != nil { |
| 105 | return sessionWorkspace, nil |
| 106 | } |
| 107 | |
| 108 | root, err := os.MkdirTemp("", "chatcli-agent-") |
| 109 | if err != nil { |
| 110 | return nil, fmt.Errorf("creating session tmpdir: %w", err) |
| 111 | } |
| 112 | |
| 113 | scratch := filepath.Join(root, "scratch") |
| 114 | if err := os.MkdirAll(scratch, 0o700); err != nil { |
| 115 | _ = os.RemoveAll(root) |
| 116 | return nil, fmt.Errorf("creating scratch dir: %w", err) |
| 117 | } |
| 118 | |
| 119 | toolResults := filepath.Join(root, "tool-results") |
| 120 | if err := os.MkdirAll(toolResults, 0o700); err != nil { |
| 121 | _ = os.RemoveAll(root) |
| 122 | return nil, fmt.Errorf("creating tool-results dir: %w", err) |
| 123 | } |
| 124 | |
| 125 | keep := strings.EqualFold(os.Getenv("CHATCLI_AGENT_KEEP_TMPDIR"), "true") |
| 126 | |
| 127 | ws := &SessionWorkspace{ |
| 128 | Root: root, |
| 129 | ScratchDir: scratch, |
| 130 | ToolResultsDir: toolResults, |
| 131 | keep: keep, |
| 132 | logger: logger, |
| 133 | } |
| 134 | |
| 135 | // Export env var so child processes (run_command, exec) can use it. |
| 136 | _ = os.Setenv("CHATCLI_AGENT_TMPDIR", scratch) |
| 137 | |
| 138 | // Register with read validator (agent package). |
| 139 | RegisterAuxReadPath(root) |
| 140 | |
| 141 | // Register with engine write/exec validator. |
| 142 | engine.RegisterAuxPath(root) |
| 143 | |
| 144 | // System temp directories are allowlisted by default. Every major |
| 145 | // model defaults to writing throwaway scripts under /tmp ("cat > |
| 146 | // /tmp/check.sh && bash /tmp/check.sh"); blocking that forces the |
| 147 | // model to guess a safe path and usually fails. The risk is low — |
| 148 | // these dirs are user-owned on single-user machines. Users who |
| 149 | // need strict sandboxing can set CHATCLI_BLOCK_TMP_WRITES=true. |
| 150 | if !strings.EqualFold(os.Getenv("CHATCLI_BLOCK_TMP_WRITES"), "true") { |
| 151 | tmpPaths := []string{os.TempDir()} |
| 152 | // On macOS os.TempDir() returns a per-user /var/folders/... path, |
| 153 | // but models emit /tmp verbatim. On Linux they're the same. Add |
| 154 | // /tmp explicitly when it exists and differs. |
| 155 | if _, err := os.Stat("/tmp"); err == nil { |
| 156 | tmpPaths = append(tmpPaths, "/tmp") |
| 157 | } |