V0.38.2 library packages (#235)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
155
server/sandbox/lib_module.go
Normal file
155
server/sandbox/lib_module.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Package sandbox — lib_module.go
|
||||
//
|
||||
// v0.38.2: Library loading module for Starlark extensions.
|
||||
// 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"
|
||||
|
||||
"chat-switchboard/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 library's PackageRegistration
|
||||
libPkg, err := r.stores.Packages.Get(ctx, libraryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lib.require: library %q not found: %w", libraryID, err)
|
||||
}
|
||||
if libPkg.Type != "library" {
|
||||
return nil, fmt.Errorf("lib.require: package %q is type %q, not library", libraryID, libPkg.Type)
|
||||
}
|
||||
if libPkg.Status != models.PackageStatusActive {
|
||||
return nil, fmt.Errorf("lib.require: library %q is %s, not active", libraryID, libPkg.Status)
|
||||
}
|
||||
if libPkg.Tier != models.ExtTierStarlark {
|
||||
return nil, fmt.Errorf("lib.require: library %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. Extract exports from manifest
|
||||
exports := extractExports(libPkg.Manifest)
|
||||
if len(exports) == 0 {
|
||||
return nil, fmt.Errorf("lib.require: library %q declares no exports", libraryID)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -13,6 +13,9 @@
|
||||
// at startup. db.read grants query/view/list_tables; db.write adds
|
||||
// insert/update/delete. If both are granted, db.write wins (superset).
|
||||
//
|
||||
// v0.38.2: Adds lib.load() for library packages. Libraries run with
|
||||
// their own permission context. Per-invocation lib cache + cycle detection.
|
||||
//
|
||||
// The runner is the bridge between the package/permission system
|
||||
// and the sandboxed Starlark interpreter.
|
||||
package sandbox
|
||||
@@ -119,8 +122,11 @@ func (r *Runner) ExecPackage(ctx context.Context, pkg *store.PackageRegistration
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// v0.38.2: per-invocation lib context for lib.load() caching + cycle detection
|
||||
lc := newLibContext()
|
||||
|
||||
// Build modules based on granted permissions
|
||||
modules, err := r.buildModules(ctx, pkg.ID, pkg.Manifest, rc)
|
||||
modules, err := r.buildModulesWithLibCtx(ctx, pkg.ID, pkg.Manifest, rc, lc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build modules for %q: %w", pkg.ID, err)
|
||||
}
|
||||
@@ -256,10 +262,17 @@ func (r *Runner) CallEntryPoint(ctx context.Context, pkg *store.PackageRegistrat
|
||||
}
|
||||
|
||||
// buildModules assembles the module map based on granted permissions.
|
||||
// Delegates to buildModulesWithLibCtx with a nil lib context (no lib.load support).
|
||||
func (r *Runner) buildModules(ctx context.Context, packageID string, manifest map[string]any, rc *RunContext) (map[string]starlark.Value, error) {
|
||||
return r.buildModulesWithLibCtx(ctx, packageID, manifest, rc, nil)
|
||||
}
|
||||
|
||||
// buildModulesWithLibCtx assembles the module map based on granted permissions.
|
||||
// The manifest is passed through so modules can read their config
|
||||
// (e.g., http module reads network_access, provider reads requires_provider).
|
||||
// The RunContext carries per-invocation state for user-scoped modules.
|
||||
func (r *Runner) buildModules(ctx context.Context, packageID string, manifest map[string]any, rc *RunContext) (map[string]starlark.Value, error) {
|
||||
// The libContext enables lib.load() caching and cycle detection (v0.38.2).
|
||||
func (r *Runner) buildModulesWithLibCtx(ctx context.Context, packageID string, manifest map[string]any, rc *RunContext, lc *libContext) (map[string]starlark.Value, error) {
|
||||
if r.stores.ExtPermissions == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -332,5 +345,11 @@ func (r *Runner) buildModules(ctx context.Context, packageID string, manifest ma
|
||||
}
|
||||
modules["settings"] = BuildSettingsModule(ctx, r.stores, packageID, userID)
|
||||
|
||||
// v0.38.2: lib module — always injected, no permission required.
|
||||
// Allows any starlark package to load declared library dependencies.
|
||||
if lc != nil {
|
||||
modules["lib"] = BuildLibModule(ctx, r, packageID, rc, lc)
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user