Files
yuan-lab-llm--clawmanager/backend/internal/services/skill_runtime_paths.go
T
heranran 71c7672545 feat: Skill Hub hardening with session usage tracking and egress governance (#163)
* feat(skill-hub): add Skill Hub catalog, publish flow, and lite materialize pipeline

Introduce Skill Hub for browsing, importing, publishing, and installing skills, with lite instance package materialization and runtime sync support.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): remove token-governance hooks from Skill Hub PR

Strip validateManagedRuntimeEnvironmentOverrides, network lock policy sync,
and egress proxy audit wiring that belong to the upcoming token-usage work,
so the Skill Hub branch compiles independently.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): update migration number in materialize docs

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): make RuntimeAgentClient test stub and hub tests compile-safe

Add ResyncInstanceSkills to the runtime pool handler fake client, and harden
skill hub payload helpers/tests against nil storage/instance repos so go test passes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add Skill Hub hardening with session usage tracking and egress governance

Unify Skill Hub runtime sync improvements with session-token observability,
egress network policy, and admin/instance usage reporting for reopenable PR.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): repair CI tests and nested skill install

* fix(ci): restore release deployment configuration

---------

Co-authored-by: heshengran <heshengran@ieisystem.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 12:37:01 +08:00

172 lines
4.7 KiB
Go

package services
import (
"os"
"path/filepath"
"strings"
"clawreef/internal/models"
)
const runtimeSkillDiscoveryMaxDepth = 2
type runtimeSkillDiscovery struct {
RelativePath string
SkillRoot string
}
func runtimeSkillInstallRoot(instance *models.Instance) string {
if instance == nil || instance.WorkspacePath == nil || strings.TrimSpace(*instance.WorkspacePath) == "" {
return ""
}
workspacePath := filepath.Clean(strings.TrimSpace(*instance.WorkspacePath))
if isLiteRuntimeInstance(instance) {
if strings.EqualFold(strings.TrimSpace(instance.Type), RuntimeTypeHermes) {
return filepath.Join(workspacePath, "home", ".hermes", "skills")
}
return filepath.Join(workspacePath, "home", ".openclaw", "workspace", "skills")
}
if strings.EqualFold(strings.TrimSpace(instance.Type), RuntimeTypeHermes) {
return filepath.Join(workspacePath, ".hermes", "skills")
}
return filepath.Join(workspacePath, "home", ".openclaw", "workspace", "skills")
}
func liteSkillInstallRoot(instance *models.Instance) string {
return runtimeSkillInstallRoot(instance)
}
func sanitizeWorkspaceRelativePath(value string) string {
value = strings.TrimSpace(value)
value = strings.ReplaceAll(value, "\\", "/")
value = strings.Trim(value, "/")
if value == "" || strings.Contains(value, "..") {
return ""
}
parts := make([]string, 0, strings.Count(value, "/")+1)
for _, part := range strings.Split(value, "/") {
part = strings.TrimSpace(part)
if part == "" || part == "." || part == ".." || strings.HasPrefix(part, ".") {
return ""
}
parts = append(parts, part)
}
if len(parts) == 0 || len(parts) > runtimeSkillDiscoveryMaxDepth {
return ""
}
return strings.Join(parts, "/")
}
func skillKeyFromRelativePath(relativePath string) string {
relativePath = sanitizeWorkspaceRelativePath(relativePath)
if relativePath == "" {
return ""
}
parts := strings.Split(relativePath, "/")
return sanitizeSkillKey(parts[len(parts)-1])
}
func runtimeSkillInstallRelativePath(instance *models.Instance, relativePath string) string {
relativePath = sanitizeWorkspaceRelativePath(relativePath)
if relativePath == "" {
return ""
}
if instance == nil || instance.WorkspacePath == nil {
return relativePath
}
workspacePath := filepath.Clean(strings.TrimSpace(*instance.WorkspacePath))
target := filepath.Join(runtimeSkillInstallRoot(instance), filepath.FromSlash(relativePath))
rel, err := filepath.Rel(workspacePath, target)
if err != nil {
return filepath.ToSlash(relativePath)
}
return filepath.ToSlash(rel)
}
func joinRuntimeSkillPath(root, relativePath string) (string, error) {
root = filepath.Clean(strings.TrimSpace(root))
relativePath = sanitizeWorkspaceRelativePath(relativePath)
if root == "" || relativePath == "" {
return "", filepath.ErrBadPattern
}
target := filepath.Join(root, filepath.FromSlash(relativePath))
if !isPathWithin(root, target) {
return "", filepath.ErrBadPattern
}
return target, nil
}
func discoverRuntimeSkillDirectories(root string, maxDepth int) ([]runtimeSkillDiscovery, error) {
root = filepath.Clean(strings.TrimSpace(root))
if root == "" {
return nil, nil
}
if maxDepth <= 0 {
maxDepth = runtimeSkillDiscoveryMaxDepth
}
result := make([]runtimeSkillDiscovery, 0)
var walk func(currentRoot, relativePrefix string, depth int) error
walk = func(currentRoot, relativePrefix string, depth int) error {
entries, err := readDirNames(currentRoot)
if err != nil {
return err
}
for _, entry := range entries {
name := strings.TrimSpace(entry.Name)
if name == "" || strings.HasPrefix(name, ".") || name == ".tmp" {
continue
}
if !entry.IsDir {
continue
}
skillRoot := filepath.Join(currentRoot, name)
relativePath := name
if relativePrefix != "" {
relativePath = relativePrefix + "/" + name
}
relativePath = sanitizeWorkspaceRelativePath(relativePath)
if relativePath == "" {
continue
}
files, err := collectLiteSkillDirectoryFiles(skillRoot)
if err != nil {
return err
}
if len(files) > 0 {
result = append(result, runtimeSkillDiscovery{
RelativePath: relativePath,
SkillRoot: skillRoot,
})
continue
}
if depth < maxDepth {
if err := walk(skillRoot, relativePath, depth+1); err != nil {
return err
}
}
}
return nil
}
if err := walk(root, "", 1); err != nil {
return nil, err
}
return result, nil
}
func readDirNames(path string) ([]dirEntryName, error) {
entries, err := os.ReadDir(path)
if err != nil {
return nil, err
}
result := make([]dirEntryName, 0, len(entries))
for _, entry := range entries {
result = append(result, dirEntryName{Name: entry.Name(), IsDir: entry.IsDir()})
}
return result, nil
}
type dirEntryName struct {
Name string
IsDir bool
}