diff --git a/sandbox/manager.go b/sandbox/manager.go index c93c6a59..484d059e 100644 --- a/sandbox/manager.go +++ b/sandbox/manager.go @@ -648,8 +648,10 @@ func (m *Manager) Cleanup(ctx context.Context) error { name := key.(string) c := value.(*Container) + idleTime := now.Sub(c.LastUsedAt) + // Stop idle containers - if c.Status == StatusRunning && now.Sub(c.LastUsedAt) > m.config.IdleTimeout { + if c.Status == StatusRunning && idleTime > m.config.IdleTimeout { m.Stop(ctx, name) } @@ -727,6 +729,7 @@ func (m *Manager) WriteFile(ctx context.Context, name, path string, content []by } // ReadFile reads content from a file in container +// Since workspace is bind-mounted, we read directly from host for better performance func (m *Manager) ReadFile(ctx context.Context, name, path string) ([]byte, error) { c, ok := m.containers.Load(name) if !ok { @@ -734,20 +737,13 @@ func (m *Manager) ReadFile(ctx context.Context, name, path string) ([]byte, erro } cont := c.(*Container) - reader, _, err := m.dockerClient.CopyFromContainer(ctx, cont.ID, path) - if err != nil { - return nil, err - } - defer reader.Close() - - // Extract from tar - tr := tar.NewReader(reader) - _, err = tr.Next() - if err != nil { - return nil, err + // Read directly from host bind mount + hostPath := m.containerPathToHost(cont, path) + if hostPath == "" { + return nil, fmt.Errorf("path %s is not within workspace", path) } - return io.ReadAll(tr) + return os.ReadFile(hostPath) } // ListDir lists directory contents in container @@ -840,6 +836,20 @@ func (m *Manager) ensureIPCSession(ctx context.Context, userID, chatID string) { m.ipcManager.Create(ctx, sessionID, agentCtx, nil) } +// containerPathToHost converts a container path to the corresponding host path +// Returns empty string if the path is not within a bind-mounted directory +func (m *Manager) containerPathToHost(cont *Container, containerPath string) string { + // Container workspace is mounted at ContainerWorkDir (e.g., /workspace) + // Host path is WorkspaceRoot/{userID}/{chatID} + workDir := m.config.ContainerWorkDir + if strings.HasPrefix(containerPath, workDir) { + relativePath := strings.TrimPrefix(containerPath, workDir) + relativePath = strings.TrimPrefix(relativePath, "/") + return filepath.Join(m.config.WorkspaceRoot, cont.UserID, cont.ChatID, relativePath) + } + return "" +} + // fixIPCSocketPermissions fixes IPC socket permissions inside the container // This is needed because macOS Docker Desktop with gRPC-FUSE doesn't properly // preserve Unix socket permissions when bind mounting from host.