Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,16 @@ profiler:
address: localhost:6060

lua:
proto_cache_size: 60000
main_cache_size: 10000
type_system:
enabled: true
strict: false
cache:
enabled: true
dir: .wippy/cache/lua
mode: readwrite # off | readonly | readwrite
max_bytes: 1073741824
max_entries: 20000
prune_interval: 256
compile:
enabled: true
typecheck:
Expand Down
36 changes: 36 additions & 0 deletions api/registry/dependency_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MPL-2.0

package registry

import "context"

// DependencyAccess controls external dependency access.
type DependencyAccess uint8

const (
// DependencyAccessUnspecified delegates policy selection to the caller.
DependencyAccessUnspecified DependencyAccess = iota
// DependencyAccessOnline permits external resolution and artifact download.
DependencyAccessOnline
// DependencyAccessVerifiedOffline forbids external dependency access.
DependencyAccessVerifiedOffline
)

type dependencyAccessContextKey struct{}

// WithDependencyAccess returns a request-scoped dependency access policy.
func WithDependencyAccess(ctx context.Context, access DependencyAccess) context.Context {
return context.WithValue(ctx, dependencyAccessContextKey{}, access)
}

// DependencyAccessFromContext returns the request-scoped policy.
func DependencyAccessFromContext(ctx context.Context) DependencyAccess {
if ctx == nil {
return DependencyAccessUnspecified
}
access, ok := ctx.Value(dependencyAccessContextKey{}).(DependencyAccess)
if !ok || access > DependencyAccessVerifiedOffline {
return DependencyAccessUnspecified
}
return access
}
18 changes: 18 additions & 0 deletions api/registry/dependency_access_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MPL-2.0

package registry

import (
"context"
"testing"

"github.com/stretchr/testify/require"
)

func TestDependencyAccessContext(t *testing.T) {
require.Equal(t, DependencyAccessUnspecified, DependencyAccessFromContext(context.Background()))

ctx := WithDependencyAccess(context.Background(), DependencyAccessVerifiedOffline)
require.Equal(t, DependencyAccessVerifiedOffline, DependencyAccessFromContext(ctx))
require.Equal(t, DependencyAccessUnspecified, DependencyAccessFromContext(nil))
}
10 changes: 6 additions & 4 deletions boot/components/runtime/lua/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,14 @@ func Engine() boot.Component {

func resolveEngineSettings(cfg boot.Config) code.Config {
settings := code.Config{
ProtoCacheSize: 60000,
MainCacheSize: 10000,
Cache: cache.Config{
Dir: cache.DefaultDir,
Mode: cache.ModeReadWrite,
CompileEnabled: true,
TypecheckEnabled: true,
MaxBytes: cache.DefaultMaxBytes,
MaxEntries: cache.DefaultMaxEntries,
PruneInterval: cache.DefaultPruneInterval,
},
InvalidationWaitTimeout: code.DefaultInvalidationWaitTimeout,
}
Expand All @@ -133,8 +134,6 @@ func resolveEngineSettings(cfg boot.Config) code.Config {
)

luaCfg := cfg.Sub("lua")
settings.ProtoCacheSize = luaCfg.GetInt("proto_cache_size", settings.ProtoCacheSize)
settings.MainCacheSize = luaCfg.GetInt("main_cache_size", settings.MainCacheSize)
settings.InvalidationWaitTimeout = luaCfg.GetDuration("invalidation_wait_timeout", settings.InvalidationWaitTimeout)

typeSystemCfg := luaCfg.Sub("type_system")
Expand All @@ -153,6 +152,9 @@ func resolveEngineSettings(cfg boot.Config) code.Config {
settings.Cache.Mode = cache.ParseMode(luaCfg.GetString("cache.mode", string(settings.Cache.Mode)))
settings.Cache.CompileEnabled = luaCfg.GetBool("cache.compile.enabled", settings.Cache.CompileEnabled)
settings.Cache.TypecheckEnabled = luaCfg.GetBool("cache.typecheck.enabled", settings.Cache.TypecheckEnabled)
settings.Cache.MaxBytes = int64(luaCfg.GetInt("cache.max_bytes", int(settings.Cache.MaxBytes)))
settings.Cache.MaxEntries = luaCfg.GetInt("cache.max_entries", settings.Cache.MaxEntries)
settings.Cache.PruneInterval = luaCfg.GetInt("cache.prune_interval", settings.Cache.PruneInterval)
return settings
}

Expand Down
6 changes: 3 additions & 3 deletions boot/components/runtime/lua/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,6 @@ func TestL04EngineLifecycleIdempotent(t *testing.T) {

func TestL05EngineSettingsDefaults(t *testing.T) {
settings := resolveEngineSettings(nil)
if settings.ProtoCacheSize != 60000 || settings.MainCacheSize != 10000 {
t.Fatalf("cache sizes = (%d, %d), want (60000, 10000)", settings.ProtoCacheSize, settings.MainCacheSize)
}
if settings.TypeCheck.Enabled || settings.TypeCheck.Strict {
t.Fatalf("type check defaults = %#v, want disabled and non-strict", settings.TypeCheck)
}
Expand All @@ -210,6 +207,9 @@ func TestL05EngineSettingsDefaults(t *testing.T) {
if !settings.Cache.CompileEnabled || !settings.Cache.TypecheckEnabled {
t.Fatalf("cache stage defaults = %#v, want both enabled", settings.Cache)
}
if settings.Cache.MaxBytes <= 0 || settings.Cache.MaxEntries <= 0 || settings.Cache.PruneInterval <= 0 {
t.Fatalf("cache retention defaults = %#v, want bounded positive limits", settings.Cache)
}
if settings.InvalidationWaitTimeout != 30*time.Second {
t.Fatalf("invalidation timeout = %v, want 30s", settings.InvalidationWaitTimeout)
}
Expand Down
69 changes: 39 additions & 30 deletions boot/deps/hub/dependency_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,9 @@ func (h *DependencyHandler) resolveModules(ctx context.Context, deps []Dependenc
provider = h.manifestCache
}
lockedDigests := h.lockedModuleDigests()
if regapi.DependencyAccessFromContext(ctx) == regapi.DependencyAccessVerifiedOffline {
provider = newLockedManifestProvider(h, lockedVersions, lockedDigests)
}
provider = &replacementManifestProvider{
base: provider,
handler: h,
Expand All @@ -1329,6 +1332,10 @@ func (h *DependencyHandler) resolveModules(ctx context.Context, deps []Dependenc
if h.logger != nil {
h.logger.Error("dependency resolution failed", zap.String("errors", formatResolutionErrors(result.Errors)))
}
if regapi.DependencyAccessFromContext(ctx) == regapi.DependencyAccessVerifiedOffline {
module := result.Errors[0].Org + "/" + result.Errors[0].Name
return nil, NewDependencyOfflineError("resolve", strings.Trim(module, "/"))
}
return nil, NewDependencyResolutionErrors(result.Errors)
}
for _, mod := range result.Modules {
Expand Down Expand Up @@ -1357,6 +1364,17 @@ func (h *DependencyHandler) resolveEffectiveModules(
deps []DependencyDefinition,
lockedVersions map[string]string,
) ([]ResolvedModule, error) {
if regapi.DependencyAccessFromContext(ctx) == regapi.DependencyAccessVerifiedOffline {
if resolved, ok := h.lockedResolution(deps, lockedVersions); ok {
if h.logger != nil {
h.logger.Debug("using locked dependency resolution",
zap.Int("modules", len(resolved)),
zap.Int("roots", len(deps)))
}
return resolved, nil
}
}

resolved, err := h.resolveModules(ctx, deps, lockedVersions)
if err != nil {
return nil, err
Expand Down Expand Up @@ -1667,36 +1685,7 @@ func (p *replacementManifestProvider) localReplacementDependencies(ctx context.C
return nil, err
}

deps := make([]ManifestDep, 0)
seen := make(map[string]struct{})
for _, entry := range entries {
if entry.Kind != regapi.NamespaceDependency {
continue
}
def, err := decodeDependency(ctx, transcoder, entry)
if err != nil {
return nil, err
}
if def.Component == "" {
return nil, NewDependencyEntryInvalidError(entry.ID.String(), "component is required", "")
}
name, err := graph.ParseName(def.Component)
if err != nil {
return nil, NewDependencyEntryInvalidError(entry.ID.String(), "invalid component", def.Component)
}

key := name.String() + "@" + def.Version
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
deps = append(deps, ManifestDep{
Org: name.Organization,
Name: name.Module,
Version: def.Version,
})
}
return deps, nil
return manifestDependenciesFromEntries(ctx, transcoder, entries)
}

func loadReplacementEntries(
Expand Down Expand Up @@ -2147,6 +2136,9 @@ func (h *DependencyHandler) ensureModuleAvailable(ctx context.Context, mod Resol
} else if !errors.Is(statErr, os.ErrNotExist) {
return "", NewDependencyDownloadError(modKey(mod), statErr)
}
if regapi.DependencyAccessFromContext(ctx) == regapi.DependencyAccessVerifiedOffline {
return "", NewDependencyOfflineError("load artifact", modKey(mod))
}

privateDir, err := os.MkdirTemp(h.vendorDir, ".artifact-download-*")
if err != nil {
Expand Down Expand Up @@ -2362,6 +2354,9 @@ func validateDownloadInfo(mod ResolvedModule, info *DownloadInfo) error {
// Used both when the resolved manifest carries no URL and to refresh a URL
// that expired before the artifact could be downloaded.
func (h *DependencyHandler) freshDownloadInfo(ctx context.Context, mod ResolvedModule) (*DownloadInfo, error) {
if regapi.DependencyAccessFromContext(ctx) == regapi.DependencyAccessVerifiedOffline {
return nil, NewDependencyOfflineError("fetch artifact metadata", modKey(mod))
}
downloadURLCtx, cancel := withOptionalTimeout(ctx, h.downloadTimeout)
defer cancel()

Expand Down Expand Up @@ -2850,6 +2845,20 @@ func NewDependencyResolutionError(cause error) apierror.Error {
return err
}

// NewDependencyOfflineError reports unavailable verified dependency evidence.
func NewDependencyOfflineError(operation, module string) apierror.Error {
details := map[string]any{
"operation": operation,
"hint": "run an explicit wippy update/install while online, then retry startup",
}
if module != "" {
details["module"] = module
}
return apierror.New(apierror.Invalid, "verified dependency evidence is unavailable during offline startup").
WithRetryable(apierror.False).
WithDetails(attrs.NewBagFrom(details))
}

func NewDependencyResolutionErrors(errs []ResolutionError) apierror.Error {
details := make([]map[string]any, 0, len(errs))
unauthenticated := false
Expand Down
1 change: 1 addition & 0 deletions boot/deps/hub/dependency_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3339,6 +3339,7 @@ replacements:

func newTestContext() context.Context {
ctx := ctxapi.NewRootContext()
ctx = regapi.WithDependencyAccess(ctx, regapi.DependencyAccessOnline)
transcoder := syspayload.NewTranscoder()
jsonpayload.Register(transcoder)
yamlpayload.Register(transcoder)
Expand Down
125 changes: 125 additions & 0 deletions boot/deps/hub/locked_manifest_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-License-Identifier: MPL-2.0

package hub

import (
"context"
"fmt"

"github.com/wippyai/runtime/api/payload"
regapi "github.com/wippyai/runtime/api/registry"
"github.com/wippyai/runtime/boot/deps/graph"
)

// lockedManifestProvider exposes only locally materialized, content-pinned
// modules. It lets the normal resolver validate a graph containing mutable
// replacements without granting that resolver any network capability.
type lockedManifestProvider struct {
handler *DependencyHandler
modules map[string]ResolvedModule
}

func newLockedManifestProvider(
handler *DependencyHandler,
materializedVersions map[string]string,
lockedDigests map[string]string,
) ManifestProvider {
provider := &lockedManifestProvider{
handler: handler,
modules: make(map[string]ResolvedModule),
}
if handler == nil || handler.lock == nil {
return provider
}
for _, locked := range handler.lock.GetModules() {
name, err := graph.ParseName(locked.Name)
if err != nil || locked.Version == "" || materializedVersions[locked.Name] != locked.Version {
continue
}
digest := lockedDigests[locked.Name+"@"+locked.Version]
if err := validateModuleArtifactIdentity(name, locked.Version, digest); err != nil || digest == "" {
continue
}
provider.modules[locked.Name] = ResolvedModule{
Org: name.Organization,
Name: name.Module,
Version: locked.Version,
VersionID: locked.Version,
Source: moduleSourceHub,
Digest: digest,
}
}
return provider
}

func (p *lockedManifestProvider) GetManifest(ctx context.Context, org, module, constraint string) (*ModuleManifest, error) {
name := org + "/" + module
mod, ok := p.modules[name]
if !ok || !storedVersionSatisfies(mod.Version, constraint) {
return nil, NewDependencyOfflineError("resolve manifest", name)
}
transcoder := payload.GetTranscoder(ctx)
if transcoder == nil {
return nil, ErrDependencyTranscoderMissing
}
entries, err := p.handler.loadEntriesForModule(ctx, transcoder, mod)
if err != nil {
return nil, err
}
deps, err := manifestDependenciesFromEntries(ctx, transcoder, entries)
if err != nil {
return nil, fmt.Errorf("read locked manifest %s@%s: %w", name, mod.Version, err)
}
return &ModuleManifest{
Org: mod.Org,
Name: mod.Name,
Version: mod.Version,
VersionID: mod.VersionID,
Digest: mod.Digest,
SizeBytes: mod.SizeBytes,
Dependencies: deps,
}, nil
}

func (p *lockedManifestProvider) ListAllVersions(_ context.Context, org, module string) ([]VersionInfo, error) {
name := org + "/" + module
mod, ok := p.modules[name]
if !ok {
return nil, NewDependencyOfflineError("list versions", name)
}
return []VersionInfo{{Version: mod.Version}}, nil
}

func manifestDependenciesFromEntries(
ctx context.Context,
transcoder payload.Transcoder,
entries []regapi.Entry,
) ([]ManifestDep, error) {
deps := make([]ManifestDep, 0)
seen := make(map[string]struct{})
for _, entry := range entries {
if entry.Kind != regapi.NamespaceDependency {
continue
}
def, err := decodeDependency(ctx, transcoder, entry)
if err != nil {
return nil, err
}
if def.Component == "" {
return nil, NewDependencyEntryInvalidError(entry.ID.String(), "component is required", "")
}
name, err := graph.ParseName(def.Component)
if err != nil {
return nil, NewDependencyEntryInvalidError(entry.ID.String(), "invalid component", def.Component)
}
key := name.String() + "@" + def.Version
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
deps = append(deps, ManifestDep{
Org: name.Organization, Name: name.Module, Version: def.Version,
})
}
return deps, nil
}
Loading