55 "fmt"
66 "io/fs"
77 "os"
8+ "path"
89 "path/filepath"
910 "reflect"
1011 "slices"
@@ -26,6 +27,80 @@ type moduleRequest struct {
2627
2728const moduleKeySeparator = "::"
2829
30+ func normalizeModulePolicyPattern (pattern string ) string {
31+ normalized := strings .TrimSpace (pattern )
32+ normalized = strings .ReplaceAll (normalized , "\\ " , "/" )
33+ normalized = strings .TrimPrefix (normalized , "./" )
34+ normalized = strings .TrimSuffix (normalized , ".vibe" )
35+ normalized = path .Clean (normalized )
36+ if normalized == "." {
37+ return ""
38+ }
39+ return normalized
40+ }
41+
42+ func normalizeModulePolicyModuleName (relative string ) string {
43+ normalized := filepath .ToSlash (filepath .Clean (relative ))
44+ normalized = strings .TrimPrefix (normalized , "./" )
45+ normalized = strings .TrimSuffix (normalized , ".vibe" )
46+ if normalized == "." {
47+ return ""
48+ }
49+ return normalized
50+ }
51+
52+ func validateModulePolicyPatterns (patterns []string , label string ) error {
53+ for _ , raw := range patterns {
54+ pattern := normalizeModulePolicyPattern (raw )
55+ if pattern == "" {
56+ return fmt .Errorf ("vibes: module %s-list pattern cannot be empty" , label )
57+ }
58+ if _ , err := path .Match (pattern , "probe" ); err != nil {
59+ return fmt .Errorf ("vibes: invalid module %s-list pattern %q: %w" , label , raw , err )
60+ }
61+ }
62+ return nil
63+ }
64+
65+ func modulePolicyMatch (pattern string , module string ) bool {
66+ matched , err := path .Match (pattern , module )
67+ if err != nil {
68+ return false
69+ }
70+ return matched
71+ }
72+
73+ func (e * Engine ) enforceModulePolicy (relative string ) error {
74+ module := normalizeModulePolicyModuleName (relative )
75+ if module == "" {
76+ return nil
77+ }
78+
79+ for _ , raw := range e .config .ModuleDenyList {
80+ pattern := normalizeModulePolicyPattern (raw )
81+ if pattern == "" {
82+ continue
83+ }
84+ if modulePolicyMatch (pattern , module ) {
85+ return fmt .Errorf ("require: module %q denied by policy" , module )
86+ }
87+ }
88+
89+ if len (e .config .ModuleAllowList ) == 0 {
90+ return nil
91+ }
92+ for _ , raw := range e .config .ModuleAllowList {
93+ pattern := normalizeModulePolicyPattern (raw )
94+ if pattern == "" {
95+ continue
96+ }
97+ if modulePolicyMatch (pattern , module ) {
98+ return nil
99+ }
100+ }
101+ return fmt .Errorf ("require: module %q not allowed by policy" , module )
102+ }
103+
29104func parseModuleRequest (name string ) (moduleRequest , error ) {
30105 trimmed := strings .TrimSpace (name )
31106 if trimmed == "" {
@@ -271,6 +346,10 @@ func (e *Engine) loadSearchPathModule(request moduleRequest) (moduleEntry, error
271346}
272347
273348func (e * Engine ) compileAndCacheModule (key , root , relative , fullPath string , content []byte ) (moduleEntry , error ) {
349+ if err := e .enforceModulePolicy (relative ); err != nil {
350+ return moduleEntry {}, err
351+ }
352+
274353 script , err := e .Compile (string (content ))
275354 if err != nil {
276355 return moduleEntry {}, fmt .Errorf ("require: compiling %s failed: %w" , fullPath , err )
0 commit comments