mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 12:14:17 +00:00
refactor(workspace): extract layered resolver pipeline for workspace path computation
Replace inline workspace path computation in loop_context.go with composable WorkspaceLayer pipeline (tenant → team → project → user/chat). Each layer is a pure function that appends a path segment or is a no-op. - New workspace_resolver.go: ResolveWorkspace, TenantLayer, TeamLayer, ProjectLayer (future), UserChatLayer, SanitizePathSegment - 16 unit tests covering all layer combinations - Migrate loop_context.go, loop_history.go, team_tasks_mutations.go - Move sanitizePathSegment from agent to tools package (exported) - Zero behavior change — identical paths for all scenarios
This commit is contained in:
1 parent
28a17c4d6b
commit
4b780bbffa
6 files changed
+323
-66
No files matched your search
@@ -0,0 +1,80 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
)
|
||||
|
||||
// WorkspaceLayer transforms a base path into a scoped path.
|
||||
// Returns base unchanged if the layer is not applicable (no-op).
|
||||
type WorkspaceLayer func(base string) string
|
||||
|
||||
// ResolveWorkspace applies layers sequentially to produce the final workspace path.
|
||||
// Each layer either appends a path segment or returns base unchanged (no-op).
|
||||
func ResolveWorkspace(base string, layers ...WorkspaceLayer) string {
|
||||
for _, layer := range layers {
|
||||
base = layer(base)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// TenantLayer scopes to tenant subdirectory.
|
||||
// Master tenant is a no-op (backward compat — returns base unchanged).
|
||||
func TenantLayer(tenantID uuid.UUID, slug string) WorkspaceLayer {
|
||||
return func(base string) string {
|
||||
return config.TenantWorkspace(base, tenantID, slug)
|
||||
}
|
||||
}
|
||||
|
||||
// TeamLayer scopes to team subdirectory: {base}/teams/{teamID}.
|
||||
// Nil teamID is a no-op.
|
||||
func TeamLayer(teamID uuid.UUID) WorkspaceLayer {
|
||||
return func(base string) string {
|
||||
if teamID == uuid.Nil {
|
||||
return base
|
||||
}
|
||||
return filepath.Join(base, "teams", teamID.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ProjectLayer scopes to project subdirectory: {base}/projects/{projectID}.
|
||||
// Nil projectID is a no-op. Reserved for future use.
|
||||
func ProjectLayer(projectID *uuid.UUID) WorkspaceLayer {
|
||||
return func(base string) string {
|
||||
if projectID == nil || *projectID == uuid.Nil {
|
||||
return base
|
||||
}
|
||||
return filepath.Join(base, "projects", projectID.String())
|
||||
}
|
||||
}
|
||||
|
||||
// UserChatLayer scopes to per-user or per-chat subdirectory: {base}/{segment}.
|
||||
// Empty segment or shared=true is a no-op.
|
||||
// The segment should already be sanitized via SanitizePathSegment if it contains user input.
|
||||
func UserChatLayer(segment string, shared bool) WorkspaceLayer {
|
||||
return func(base string) string {
|
||||
if shared || segment == "" {
|
||||
return base
|
||||
}
|
||||
return filepath.Join(base, segment)
|
||||
}
|
||||
}
|
||||
|
||||
// SanitizePathSegment makes a string safe for use as a directory name.
|
||||
// Replaces colons, spaces, and other unsafe chars with underscores.
|
||||
// Used to convert userIDs and chatIDs into safe filesystem path segments.
|
||||
func SanitizePathSegment(s string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
|
||||
b.WriteRune(r)
|
||||
} else {
|
||||
b.WriteByte('_')
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in new issue
Block a user