Changeset 0.38.0 (#233)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-25 09:20:06 +00:00
committed by xcaliber
parent be67feaa8e
commit 10acadc9d0
13 changed files with 556 additions and 49 deletions

View File

@@ -22,6 +22,9 @@ import (
"database/sql"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"go.starlark.net/starlark"
@@ -43,12 +46,13 @@ type RunContext struct {
// Runner executes Starlark package scripts with permission-gated modules.
type Runner struct {
sandbox *Sandbox
stores store.Stores
notifier NotificationSender // nil = notifications module unavailable
resolver ProviderResolver // nil = provider module unavailable
db *sql.DB // nil = db module unavailable
dbPostgres bool // true = use $N placeholders; false = use ?
sandbox *Sandbox
stores store.Stores
packagesDir string // v0.38.0: disk path for load() support
notifier NotificationSender // nil = notifications module unavailable
resolver ProviderResolver // nil = provider module unavailable
db *sql.DB // nil = db module unavailable
dbPostgres bool // true = use $N placeholders; false = use ?
}
// NewRunner creates a runner with the given sandbox and dependencies.
@@ -77,8 +81,17 @@ func (r *Runner) SetDB(db *sql.DB, isPostgres bool) {
r.dbPostgres = isPostgres
}
// ExecPackage loads a package's script from its manifest and executes it
// with modules gated by the package's granted permissions.
// SetPackagesDir sets the disk path where package archives are extracted.
// Required for load() support — scripts are read from disk at runtime.
// v0.38.0.
func (r *Runner) SetPackagesDir(dir string) {
r.packagesDir = dir
}
// ExecPackage loads a package's script from disk (primary) or manifest
// (legacy) and executes it with modules gated by granted permissions.
//
// v0.38.0: Disk-based loading + package-scoped load() support.
//
// The RunContext carries per-invocation state (user_id for provider
// resolution). Pass nil if no per-invocation context is needed.
@@ -94,10 +107,10 @@ func (r *Runner) ExecPackage(ctx context.Context, pkg *store.PackageRegistration
return nil, fmt.Errorf("package %q is tier %s, not starlark", pkg.ID, pkg.Tier)
}
// Extract script from manifest
script, ok := pkg.Manifest["_starlark_script"].(string)
if !ok || script == "" {
return nil, fmt.Errorf("package %q has no _starlark_script in manifest", pkg.ID)
// Load script from disk (primary) or manifest (legacy)
script, err := r.loadScript(pkg)
if err != nil {
return nil, err
}
// Build modules based on granted permissions
@@ -106,9 +119,105 @@ func (r *Runner) ExecPackage(ctx context.Context, pkg *store.PackageRegistration
return nil, fmt.Errorf("failed to build modules for %q: %w", pkg.ID, err)
}
// Build package-scoped load callback
loader := r.packageLoader(pkg.ID, modules)
log.Printf(" 🔧 runner: exec %s (%d modules granted)", pkg.ID, len(modules))
return r.sandbox.Exec(ctx, pkg.ID+".star", script, modules)
return r.sandbox.ExecWithLoader(ctx, pkg.ID+"/script.star", script, modules, loader)
}
// loadScript reads the entry point script from disk (primary path)
// or falls back to the legacy _starlark_script manifest field.
func (r *Runner) loadScript(pkg *store.PackageRegistration) (string, error) {
// Primary: read from disk
if r.packagesDir != "" {
entryPoint := "script.star"
if ep, ok := pkg.Manifest["entry_point"].(string); ok && ep != "" {
entryPoint = ep
}
path := filepath.Join(r.packagesDir, pkg.ID, entryPoint)
data, err := os.ReadFile(path)
if err == nil && len(data) > 0 {
return string(data), nil
}
// Fall through to legacy
}
// Legacy: inline in manifest
script, ok := pkg.Manifest["_starlark_script"].(string)
if ok && script != "" {
return script, nil
}
return "", fmt.Errorf("package %q: no script.star on disk and no _starlark_script in manifest", pkg.ID)
}
// loadEntry tracks cache and cycle detection for the package loader.
type loadEntry struct {
loading bool
globals starlark.StringDict
}
// packageLoader builds a LoadFunc scoped to a package's directory.
// Returns nil if no packages directory is configured.
func (r *Runner) packageLoader(pkgID string, modules map[string]starlark.Value) LoadFunc {
if r.packagesDir == "" {
return nil // no disk = no load support
}
pkgDir := filepath.Join(r.packagesDir, pkgID)
cache := make(map[string]*loadEntry) // dedup + cycle detection
return func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
// Security: reject path traversal
if strings.Contains(module, "..") || filepath.IsAbs(module) {
return nil, fmt.Errorf("load: path traversal not allowed: %q", module)
}
// Resolve to package directory
resolved := filepath.Join(pkgDir, module)
if !strings.HasPrefix(filepath.Clean(resolved), filepath.Clean(pkgDir)) {
return nil, fmt.Errorf("load: path escapes package directory: %q", module)
}
// Must be a .star file
if !strings.HasSuffix(resolved, ".star") {
return nil, fmt.Errorf("load: only .star files can be loaded: %q", module)
}
// Cache / cycle detection
if entry, ok := cache[module]; ok {
if entry.loading {
return nil, fmt.Errorf("load: circular dependency: %q", module)
}
return entry.globals, nil
}
entry := &loadEntry{loading: true}
cache[module] = entry
// Read and execute
data, err := os.ReadFile(resolved)
if err != nil {
return nil, fmt.Errorf("load: %q not found in package %q", module, pkgID)
}
// Build predeclared with same modules as entry point
predeclared := make(starlark.StringDict, len(modules)+1)
for k, v := range modules {
predeclared[k] = v
}
globals, err := starlark.ExecFile(thread, module, string(data), predeclared)
if err != nil {
return nil, fmt.Errorf("load: error in %q: %w", module, err)
}
entry.globals = globals
entry.loading = false
return globals, nil
}
}
// CallEntryPoint executes a package script and calls a named function.

View File

@@ -79,18 +79,28 @@ func New(cfg Config) *Sandbox {
return &Sandbox{config: cfg}
}
// LoadFunc resolves load("path") calls to Starlark source.
// Returns the module's globals. The sandbox caches results per
// thread (Starlark handles this via thread.Load dedup).
type LoadFunc func(thread *starlark.Thread, module string) (starlark.StringDict, error)
// Exec executes a Starlark script within the sandbox.
// load() is disabled — all dependencies must be provided via modules.
// For load() support, use ExecWithLoader.
func (s *Sandbox) Exec(ctx context.Context, filename, source string, modules map[string]starlark.Value) (*Result, error) {
return s.ExecWithLoader(ctx, filename, source, modules, nil)
}
// ExecWithLoader executes a Starlark script within the sandbox with
// an optional load callback. If loader is nil, load() returns an error.
//
// Parameters:
// - ctx: context for timeout/cancellation
// - filename: used in error messages and stack traces
// - source: Starlark source code
// - modules: predeclared modules injected into the script's namespace
// (e.g., {"secrets": secretsModule, "notifications": notifModule})
//
// The script cannot use load() — all dependencies must be provided
// via the modules parameter.
func (s *Sandbox) Exec(ctx context.Context, filename, source string, modules map[string]starlark.Value) (*Result, error) {
// - loader: resolves load() calls; nil = load() disabled
func (s *Sandbox) ExecWithLoader(ctx context.Context, filename, source string, modules map[string]starlark.Value, loader LoadFunc) (*Result, error) {
var output strings.Builder
var outputMu sync.Mutex
@@ -101,6 +111,14 @@ func (s *Sandbox) Exec(ctx context.Context, filename, source string, modules map
// json.encode / json.decode — universal, no permissions required (pure computation)
predeclared["json"] = starlarkjson.Module
// Default: load() disabled. If loader provided, delegate to it.
loadFn := func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
return nil, fmt.Errorf("load() is disabled in sandbox (requested %q)", module)
}
if loader != nil {
loadFn = loader
}
thread := &starlark.Thread{
Name: s.config.Name,
Print: func(_ *starlark.Thread, msg string) {
@@ -109,9 +127,7 @@ func (s *Sandbox) Exec(ctx context.Context, filename, source string, modules map
output.WriteString(msg)
output.WriteByte('\n')
},
Load: func(_ *starlark.Thread, module string) (starlark.StringDict, error) {
return nil, fmt.Errorf("load() is disabled in sandbox (requested %q)", module)
},
Load: loadFn,
}
if s.config.MaxSteps > 0 {