This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/sandbox/lib_module.go
Jeffrey Smith 98fd3eb3e6
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 2m54s
CI/CD / build-and-deploy (push) Successful in 52s
Feat v0.8.5 extension composability (#72)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-03 11:33:47 +00:00

152 lines
4.9 KiB
Go

// 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
}