Files
bzzz/workspace/hcfs_manager.go
anthonyrawlins 5978a0b8f5 WIP: Save agent roles integration work before CHORUS rebrand
- Agent roles and coordination features
- Chat API integration testing
- New configuration and workspace management

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-01 02:21:11 +10:00

292 lines
9.2 KiB
Go

package workspace
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/anthonyrawlins/bzzz/pkg/config"
)
// HCFSWorkspaceManager manages agent workspaces using HCFS as the backing store
type HCFSWorkspaceManager struct {
hcfsAPIURL string
mountPath string
client *http.Client
activeSpaces map[string]*HCFSWorkspace
}
// HCFSWorkspace represents an HCFS-backed workspace for an agent
type HCFSWorkspace struct {
ID string `json:"id"`
AgentID string `json:"agent_id"`
TaskID string `json:"task_id"`
HCFSPath string `json:"hcfs_path"`
LocalMount string `json:"local_mount"`
CreatedAt time.Time `json:"created_at"`
LastUsed time.Time `json:"last_used"`
}
// HCFSContextRequest represents a request to create context in HCFS
type HCFSContextRequest struct {
Path string `json:"path"`
Content string `json:"content"`
Summary string `json:"summary"`
Metadata map[string]interface{} `json:"metadata"`
}
// HCFSContextResponse represents the response from HCFS context creation
type HCFSContextResponse struct {
ID string `json:"id"`
Path string `json:"path"`
CreatedAt string `json:"created_at"`
Metadata map[string]interface{} `json:"metadata"`
}
// NewHCFSWorkspaceManager creates a new HCFS workspace manager
func NewHCFSWorkspaceManager(hcfsAPIURL string, mountPath string) *HCFSWorkspaceManager {
return &HCFSWorkspaceManager{
hcfsAPIURL: hcfsAPIURL,
mountPath: mountPath,
client: &http.Client{Timeout: 30 * time.Second},
activeSpaces: make(map[string]*HCFSWorkspace),
}
}
// CreateWorkspace creates a new HCFS-backed workspace for an agent
func (m *HCFSWorkspaceManager) CreateWorkspace(ctx context.Context, agentID, taskID string, agentConfig *config.AgentConfig) (*HCFSWorkspace, error) {
// Generate unique workspace ID
workspaceID := fmt.Sprintf("bzzz-workspace-%s-%d", agentID, time.Now().Unix())
// Create HCFS path for the workspace
hcfsPath := fmt.Sprintf("/agents/%s/workspaces/%s", agentID, workspaceID)
// Create workspace context in HCFS
contextReq := HCFSContextRequest{
Path: hcfsPath,
Content: fmt.Sprintf("Workspace for agent %s, task %s", agentID, taskID),
Summary: fmt.Sprintf("Agent workspace: %s", workspaceID),
Metadata: map[string]interface{}{
"agent_id": agentID,
"task_id": taskID,
"workspace_id": workspaceID,
"created_by": "bzzz_sandbox",
"workspace_type": "agent_sandbox",
},
}
contextID, err := m.createHCFSContext(ctx, contextReq)
if err != nil {
return nil, fmt.Errorf("failed to create HCFS context: %w", err)
}
// Create local mount point using HCFS FUSE
localMount := filepath.Join(m.mountPath, workspaceID)
if err := m.createHCFSMount(ctx, hcfsPath, localMount); err != nil {
return nil, fmt.Errorf("failed to create HCFS mount: %w", err)
}
workspace := &HCFSWorkspace{
ID: contextID,
AgentID: agentID,
TaskID: taskID,
HCFSPath: hcfsPath,
LocalMount: localMount,
CreatedAt: time.Now(),
LastUsed: time.Now(),
}
// Track active workspace
m.activeSpaces[workspaceID] = workspace
fmt.Printf("✅ HCFS workspace created: %s -> %s\n", hcfsPath, localMount)
return workspace, nil
}
// DestroyWorkspace cleans up an HCFS workspace
func (m *HCFSWorkspaceManager) DestroyWorkspace(ctx context.Context, workspace *HCFSWorkspace) error {
if workspace == nil {
return nil
}
// Unmount HCFS FUSE mount
if err := m.unmountHCFS(workspace.LocalMount); err != nil {
fmt.Printf("⚠️ Warning: failed to unmount HCFS: %v\n", err)
}
// Update HCFS context with final state
if err := m.finalizeWorkspaceContext(ctx, workspace); err != nil {
fmt.Printf("⚠️ Warning: failed to finalize workspace context: %v\n", err)
}
// Remove from active workspaces
delete(m.activeSpaces, workspace.ID)
fmt.Printf("✅ HCFS workspace destroyed: %s\n", workspace.HCFSPath)
return nil
}
// GetWorkspacePath returns the local path for container mounting
func (m *HCFSWorkspaceManager) GetWorkspacePath(workspace *HCFSWorkspace) string {
if workspace == nil {
return ""
}
return workspace.LocalMount
}
// UpdateWorkspaceUsage updates the last used timestamp
func (m *HCFSWorkspaceManager) UpdateWorkspaceUsage(workspace *HCFSWorkspace) {
if workspace != nil {
workspace.LastUsed = time.Now()
}
}
// StoreWorkspaceArtifacts stores final workspace artifacts in HCFS
func (m *HCFSWorkspaceManager) StoreWorkspaceArtifacts(ctx context.Context, workspace *HCFSWorkspace, artifacts map[string]string) error {
if workspace == nil {
return fmt.Errorf("workspace is nil")
}
// Store each artifact as a separate context in HCFS
for artifactName, content := range artifacts {
artifactPath := fmt.Sprintf("%s/artifacts/%s", workspace.HCFSPath, artifactName)
artifactReq := HCFSContextRequest{
Path: artifactPath,
Content: content,
Summary: fmt.Sprintf("Artifact from workspace %s: %s", workspace.ID, artifactName),
Metadata: map[string]interface{}{
"workspace_id": workspace.ID,
"agent_id": workspace.AgentID,
"task_id": workspace.TaskID,
"artifact_name": artifactName,
"artifact_type": "workspace_output",
"created_by": "bzzz_sandbox",
},
}
_, err := m.createHCFSContext(ctx, artifactReq)
if err != nil {
return fmt.Errorf("failed to store artifact %s: %w", artifactName, err)
}
}
return nil
}
// createHCFSContext creates a new context in HCFS
func (m *HCFSWorkspaceManager) createHCFSContext(ctx context.Context, req HCFSContextRequest) (string, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", m.hcfsAPIURL+"/contexts", strings.NewReader(string(reqBody)))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := m.client.Do(httpReq)
if err != nil {
return "", fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("HCFS API returned status %d: %s", resp.StatusCode, string(body))
}
var contextResp HCFSContextResponse
if err := json.NewDecoder(resp.Body).Decode(&contextResp); err != nil {
return "", fmt.Errorf("failed to decode response: %w", err)
}
return contextResp.ID, nil
}
// createHCFSMount creates a FUSE mount for the HCFS path
func (m *HCFSWorkspaceManager) createHCFSMount(ctx context.Context, hcfsPath, localPath string) error {
// Ensure the mount directory exists
if err := os.MkdirAll(localPath, 0755); err != nil {
return fmt.Errorf("failed to create mount directory: %w", err)
}
// TODO: Implement HCFS FUSE mount
// For now, we'll create a placeholder directory structure
// In a full implementation, this would mount the HCFS path using FUSE
fmt.Printf("🔗 Creating HCFS FUSE mount: %s -> %s\n", hcfsPath, localPath)
// Create basic workspace structure
subdirs := []string{"src", "build", "output", "logs"}
for _, subdir := range subdirs {
if err := os.MkdirAll(filepath.Join(localPath, subdir), 0755); err != nil {
return fmt.Errorf("failed to create workspace subdir %s: %w", subdir, err)
}
}
return nil
}
// unmountHCFS unmounts the HCFS FUSE mount
func (m *HCFSWorkspaceManager) unmountHCFS(localPath string) error {
// TODO: Implement HCFS FUSE unmount
// For now, we'll just remove the directory
fmt.Printf("🔌 Unmounting HCFS: %s\n", localPath)
return os.RemoveAll(localPath)
}
// finalizeWorkspaceContext updates the workspace context with final metadata
func (m *HCFSWorkspaceManager) finalizeWorkspaceContext(ctx context.Context, workspace *HCFSWorkspace) error {
// Update workspace context with completion metadata
finalReq := HCFSContextRequest{
Path: workspace.HCFSPath + "/completion",
Content: fmt.Sprintf("Workspace %s completed at %s", workspace.ID, time.Now().Format(time.RFC3339)),
Summary: fmt.Sprintf("Completion record for workspace %s", workspace.ID),
Metadata: map[string]interface{}{
"workspace_id": workspace.ID,
"agent_id": workspace.AgentID,
"task_id": workspace.TaskID,
"completed_at": time.Now().Format(time.RFC3339),
"total_duration": time.Since(workspace.CreatedAt).Seconds(),
"record_type": "workspace_completion",
},
}
_, err := m.createHCFSContext(ctx, finalReq)
return err
}
// GetActiveWorkspaces returns all currently active workspaces
func (m *HCFSWorkspaceManager) GetActiveWorkspaces() map[string]*HCFSWorkspace {
return m.activeSpaces
}
// CleanupIdleWorkspaces removes workspaces that haven't been used recently
func (m *HCFSWorkspaceManager) CleanupIdleWorkspaces(ctx context.Context, maxIdleTime time.Duration) error {
now := time.Now()
var toCleanup []*HCFSWorkspace
for _, workspace := range m.activeSpaces {
if now.Sub(workspace.LastUsed) > maxIdleTime {
toCleanup = append(toCleanup, workspace)
}
}
for _, workspace := range toCleanup {
if err := m.DestroyWorkspace(ctx, workspace); err != nil {
fmt.Printf("⚠️ Failed to cleanup idle workspace %s: %v\n", workspace.ID, err)
}
}
return nil
}