Files
Fabric/internal/plugins/template/extension_registry.go
Kayvan Sylvan ff1ef380a7 feat: add --debug flag with levels and centralized logging
CHANGES
- Add --debug flag controlling runtime logging verbosity levels
- Introduce internal/log package with Off, Basic, Detailed, Trace
- Replace ad-hoc Debugf and globals with centralized debug logger
- Wire debug level during early CLI argument parsing
- Add bash, zsh, fish completions for --debug levels
- Document debug levels in README with usage examples
- Add comprehensive STT guide covering models, flags, workflows
- Simplify splitAudioFile signature and log ffmpeg chunking operations
- Remove FABRIC_STT_DEBUG environment variable and related code
- Clean minor code paths in vendors and template modules
2025-08-19 04:23:40 -07:00

329 lines
8.2 KiB
Go

package template
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
debuglog "github.com/danielmiessler/fabric/internal/log"
"gopkg.in/yaml.v3"
)
// ExtensionDefinition represents a single extension configuration
type ExtensionDefinition struct {
// Global properties
Name string `yaml:"name"`
Executable string `yaml:"executable"`
Type string `yaml:"type"`
Timeout string `yaml:"timeout"`
Description string `yaml:"description"`
Version string `yaml:"version"`
Env []string `yaml:"env"`
// Operation-specific commands
Operations map[string]OperationConfig `yaml:"operations"`
// Additional config
Config map[string]interface{} `yaml:"config"`
}
type OperationConfig struct {
CmdTemplate string `yaml:"cmd_template"`
}
// RegistryEntry represents a registered extension
type RegistryEntry struct {
ConfigPath string `yaml:"config_path"`
ConfigHash string `yaml:"config_hash"`
ExecutableHash string `yaml:"executable_hash"`
}
type ExtensionRegistry struct {
configDir string
registry struct {
Extensions map[string]*RegistryEntry `yaml:"extensions"`
}
}
// Helper methods for Config access
func (e *ExtensionDefinition) GetOutputMethod() string {
if output, ok := e.Config["output"].(map[string]interface{}); ok {
if method, ok := output["method"].(string); ok {
return method
}
}
return "stdout" // default to stdout if not specified
}
func (e *ExtensionDefinition) GetFileConfig() map[string]interface{} {
if output, ok := e.Config["output"].(map[string]interface{}); ok {
if fileConfig, ok := output["file_config"].(map[string]interface{}); ok {
return fileConfig
}
}
return nil
}
func (e *ExtensionDefinition) IsCleanupEnabled() bool {
if fc := e.GetFileConfig(); fc != nil {
if cleanup, ok := fc["cleanup"].(bool); ok {
return cleanup
}
}
return false // default to no cleanup
}
func NewExtensionRegistry(configDir string) *ExtensionRegistry {
r := &ExtensionRegistry{
configDir: configDir,
}
r.registry.Extensions = make(map[string]*RegistryEntry)
r.ensureConfigDir()
if err := r.loadRegistry(); err != nil {
debuglog.Debug(debuglog.Basic, "Warning: could not load extension registry: %v\n", err)
}
return r
}
func (r *ExtensionRegistry) ensureConfigDir() error {
extDir := filepath.Join(r.configDir, "extensions")
return os.MkdirAll(extDir, 0755)
}
// Update the Register method in extension_registry.go
func (r *ExtensionRegistry) Register(configPath string) error {
// Read and parse the extension definition to verify it
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
var ext ExtensionDefinition
if err := yaml.Unmarshal(data, &ext); err != nil {
return fmt.Errorf("failed to parse config file: %w", err)
}
// Validate extension name
if ext.Name == "" {
return fmt.Errorf("extension name cannot be empty")
}
if strings.Contains(ext.Name, " ") {
return fmt.Errorf("extension name '%s' contains spaces - names must not contain spaces", ext.Name)
}
// Verify executable exists
if _, err := os.Stat(ext.Executable); err != nil {
return fmt.Errorf("executable not found: %w", err)
}
// Get absolute path to config
absPath, err := filepath.Abs(configPath)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
// Calculate hashes
configHash := ComputeStringHash(string(data))
executableHash, err := ComputeHash(ext.Executable)
if err != nil {
return fmt.Errorf("failed to hash executable: %w", err)
}
// Store entry
r.registry.Extensions[ext.Name] = &RegistryEntry{
ConfigPath: absPath,
ConfigHash: configHash,
ExecutableHash: executableHash,
}
return r.saveRegistry()
}
func (r *ExtensionRegistry) validateExtensionDefinition(ext *ExtensionDefinition) error {
// Validate required fields
if ext.Name == "" {
return fmt.Errorf("extension name is required")
}
if ext.Executable == "" {
return fmt.Errorf("executable path is required")
}
if ext.Type == "" {
return fmt.Errorf("extension type is required")
}
// Validate timeout format
if ext.Timeout != "" {
if _, err := time.ParseDuration(ext.Timeout); err != nil {
return fmt.Errorf("invalid timeout format: %w", err)
}
}
// Validate operations
if len(ext.Operations) == 0 {
return fmt.Errorf("at least one operation must be defined")
}
for name, op := range ext.Operations {
if op.CmdTemplate == "" {
return fmt.Errorf("command template is required for operation %s", name)
}
}
return nil
}
func (r *ExtensionRegistry) Remove(name string) error {
if _, exists := r.registry.Extensions[name]; !exists {
return fmt.Errorf("extension %s not found", name)
}
delete(r.registry.Extensions, name)
return r.saveRegistry()
}
func (r *ExtensionRegistry) Verify(name string) error {
// Get the registry entry
entry, exists := r.registry.Extensions[name]
if !exists {
return fmt.Errorf("extension %s not found", name)
}
// Load and parse the config file
data, err := os.ReadFile(entry.ConfigPath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
// Verify config hash
currentConfigHash := ComputeStringHash(string(data))
if currentConfigHash != entry.ConfigHash {
return fmt.Errorf("config file hash mismatch for %s", name)
}
// Parse to get executable path
var ext ExtensionDefinition
if err := yaml.Unmarshal(data, &ext); err != nil {
return fmt.Errorf("failed to parse config file: %w", err)
}
// Verify executable hash
currentExecutableHash, err := ComputeHash(ext.Executable)
if err != nil {
return fmt.Errorf("failed to verify executable: %w", err)
}
if currentExecutableHash != entry.ExecutableHash {
return fmt.Errorf("executable hash mismatch for %s", name)
}
return nil
}
func (r *ExtensionRegistry) GetExtension(name string) (*ExtensionDefinition, error) {
entry, exists := r.registry.Extensions[name]
if !exists {
return nil, fmt.Errorf("extension %s not found", name)
}
// Read current config file
data, err := os.ReadFile(entry.ConfigPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
// Verify config hash
currentHash := ComputeStringHash(string(data))
if currentHash != entry.ConfigHash {
return nil, fmt.Errorf("config file hash mismatch for %s", name)
}
// Parse config
var ext ExtensionDefinition
if err := yaml.Unmarshal(data, &ext); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
// Verify executable hash
currentExecHash, err := ComputeHash(ext.Executable)
if err != nil {
return nil, fmt.Errorf("failed to verify executable: %w", err)
}
if currentExecHash != entry.ExecutableHash {
return nil, fmt.Errorf("executable hash mismatch for %s", name)
}
return &ext, nil
}
func (r *ExtensionRegistry) ListExtensions() ([]*ExtensionDefinition, error) {
var exts []*ExtensionDefinition
for name := range r.registry.Extensions {
ext, err := r.GetExtension(name)
if err != nil {
// Instead of failing, we'll return nil for this extension
// The manager will handle displaying the error
exts = append(exts, nil)
continue
}
exts = append(exts, ext)
}
return exts, nil
}
func (r *ExtensionRegistry) calculateFileHash(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func (r *ExtensionRegistry) saveRegistry() error {
data, err := yaml.Marshal(r.registry)
if err != nil {
return fmt.Errorf("failed to marshal extension registry: %w", err)
}
registryPath := filepath.Join(r.configDir, "extensions", "extensions.yaml")
return os.WriteFile(registryPath, data, 0644)
}
func (r *ExtensionRegistry) loadRegistry() error {
registryPath := filepath.Join(r.configDir, "extensions", "extensions.yaml")
data, err := os.ReadFile(registryPath)
if err != nil {
if os.IsNotExist(err) {
return nil // New registry
}
return fmt.Errorf("failed to read extension registry: %w", err)
}
// Need to unmarshal the data into our registry
if err := yaml.Unmarshal(data, &r.registry); err != nil {
return fmt.Errorf("failed to parse extension registry: %w", err)
}
return nil
}