All checks were successful
CI/CD / detect-changes (pull_request) Successful in 5s
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / test-go-pg (pull_request) Successful in 2m42s
CI/CD / test-sqlite (pull_request) Successful in 2m58s
CI/CD / build-and-deploy (pull_request) Successful in 1m33s
Manifest declarations for slots (host surfaces declare injection points) and contributes (extensions declare UI contributions). lib.require() relaxed from library-only to any package with exports. Admin slots aggregation endpoint. SDK renderAll() helper. 7 new tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
152 lines
4.9 KiB
Go
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
|
|
}
|