// Package sandbox — lib_module.go // // No permission required — any starlark package can use lib.require(). // // Starlark API: // gitea = lib.require("gitea-client") → struct with library's exports // // The loaded library runs with its own permission context, not the // consumer's. Results are cached per ExecPackage invocation. package sandbox import ( "context" "fmt" "log" "go.starlark.net/starlark" "go.starlark.net/starlarkstruct" "armature/models" ) // libContext carries per-invocation state for lib.require() calls. // Created once per ExecPackage and threaded through buildModules. type libContext struct { cache map[string]starlark.Value // library ID → frozen exports struct loading map[string]bool // cycle detection } func newLibContext() *libContext { return &libContext{ cache: make(map[string]starlark.Value), loading: make(map[string]bool), } } // BuildLibModule creates the "lib" module. // consumerPkgID is the package calling lib.require() — used to validate // the dependency record exists. func BuildLibModule(ctx context.Context, r *Runner, consumerPkgID string, rc *RunContext, lc *libContext) *starlarkstruct.Module { return MakeModule("lib", starlark.StringDict{ "require": starlark.NewBuiltin("lib.require", func( thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple, ) (starlark.Value, error) { var libraryID string if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &libraryID); err != nil { return nil, err } // 1. Check cache if cached, ok := lc.cache[libraryID]; ok { return cached, nil } // 2. Cycle detection if lc.loading[libraryID] { return nil, fmt.Errorf("lib.require: circular dependency: %q", libraryID) } lc.loading[libraryID] = true defer func() { delete(lc.loading, libraryID) }() // 3. Verify dependency is declared if r.stores.Dependencies == nil { return nil, fmt.Errorf("lib.require: dependency store not available") } deps, err := r.stores.Dependencies.ListByConsumer(ctx, consumerPkgID) if err != nil { return nil, fmt.Errorf("lib.require: failed to check dependencies: %w", err) } declared := false for _, dep := range deps { if dep.LibraryID == libraryID { declared = true break } } if !declared { return nil, fmt.Errorf("lib.require: package %q does not declare dependency on %q", consumerPkgID, libraryID) } // 4. Load target PackageRegistration libPkg, err := r.stores.Packages.Get(ctx, libraryID) if err != nil { return nil, fmt.Errorf("lib.require: package %q not found: %w", libraryID, err) } // Any package with exports is callable — not just type "library" exports := extractExports(libPkg.Manifest) if len(exports) == 0 { return nil, fmt.Errorf("lib.require: package %q declares no exports", libraryID) } if libPkg.Status != models.PackageStatusActive { return nil, fmt.Errorf("lib.require: package %q is %s, not active", libraryID, libPkg.Status) } if libPkg.Tier != models.ExtTierStarlark { return nil, fmt.Errorf("lib.require: package %q is tier %s, not starlark", libraryID, libPkg.Tier) } // 5. Build library's module set (library's own permissions) libModules, err := r.buildModulesWithLibCtx(ctx, libraryID, libPkg.Manifest, rc, lc) if err != nil { return nil, fmt.Errorf("lib.require: failed to build modules for library %q: %w", libraryID, err) } // 6. Load library script from disk libScript, err := r.loadScript(libPkg) if err != nil { return nil, fmt.Errorf("lib.require: %w", err) } // 7. Execute with library-scoped loader libLoader := r.packageLoader(libraryID, libModules) result, err := r.sandbox.ExecWithLoader(ctx, libraryID+"/script.star", libScript, libModules, libLoader) if err != nil { return nil, fmt.Errorf("lib.require: error executing library %q: %w", libraryID, err) } // 8. Build exported members (exports already extracted above) members := make(starlark.StringDict, len(exports)) for _, name := range exports { val, ok := result.Globals[name] if !ok { return nil, fmt.Errorf("lib.require: library %q does not export %q", libraryID, name) } members[name] = val } // 9. Freeze into struct, cache, return frozen := starlarkstruct.FromStringDict(starlark.String(libraryID), members) lc.cache[libraryID] = frozen log.Printf(" 📦 lib.require: loaded %s (%d exports) for %s", libraryID, len(exports), consumerPkgID) return frozen, nil }), }) } // extractExports reads the "exports" array from a package manifest. func extractExports(manifest map[string]any) []string { raw, ok := manifest["exports"].([]any) if !ok { return nil } exports := make([]string, 0, len(raw)) for _, v := range raw { if s, ok := v.(string); ok { exports = append(exports, s) } } return exports }