sonnetbox

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 26, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

README

sonnetbox

Wazero: this project depends on github.com/thevilledev/wazero (feat/fuel-module-path, based on wazero v1.12.0), a fork that adds the deterministic experimental/fuel metering this sandbox needs. The fork declares its own module path so that go install and ordinary module resolution work; a replace directive would be ignored for anyone depending on sonnetbox.

CI Go Reference

sonnetbox evaluates untrusted Jsonnet in a fresh WebAssembly sandbox. It embeds go-jsonnet in a Go WASI guest and runs that guest with wazero. Jsonnet receives no ambient filesystem, environment, network, arguments, or inherited standard streams. The host explicitly supplies imports, pure native capabilities, and resource budgets.

Use the core API for new integrations. Existing go-jsonnet applications can start with the opt-in compat/gojsonnet package, which keeps the familiar VM workflow while adding contexts and an explicit sandbox boundary.

The host API is for Go, and the sonnetbox command provides a secure, intentionally bounded subset of the jsonnet CLI workflow. It is not a drop-in replacement. C++/libjsonnet applications still need the CLI, a Go service, or a sidecar boundary; there is not yet a C ABI.

The module requires Go 1.25.12 or newer. It uses no Cgo, C++, Wasmtime, shared libraries, or go-jsonnet's browser-only js/wasm artifact.

Quick start

The module path is github.com/thevilledev/sonnetbox; its root package name is sonnetbox:

import "github.com/thevilledev/sonnetbox"

engine, err := sonnetbox.NewEngine(
	context.Background(),
	sonnetbox.EngineConfig{},
)
if err != nil {
	log.Fatal(err)
}
defer engine.Close(context.Background())

imports, err := sonnetbox.NewMapImporter(map[string][]byte{
	"lib/data.jsonnet": []byte(`{answer: 42}`),
})
if err != nil {
	log.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

result, err := engine.Evaluate(ctx, sonnetbox.Request{
	Filename: "apps/main.jsonnet",
	Source:   `import "../lib/data.jsonnet"`,
	Importer: imports,
})
if err != nil {
	log.Fatal(err)
}
fmt.Print(string(result.Output))

Create one long-lived Engine per policy profile. It compiles the embedded module once, is safe for concurrent evaluation, and creates a fresh guest for every request. Do not create an engine per evaluation.

Compiling the guest dominates NewEngine. A process that cannot keep an engine alive, such as a short-lived command, should reuse compiled code through a cache:

cache, err := sonnetbox.NewCompilationCacheDir(cacheDir)
if err != nil {
	log.Fatal(err)
}
defer cache.Close(context.Background())

engine, err := sonnetbox.NewEngine(
	context.Background(),
	sonnetbox.EngineConfig{},
	sonnetbox.WithCompilationCache(cache),
)

A cache directory holds executable machine code that later runs in the process, so point it at a location only the current user can write. WithInterpreter is the alternative for a single evaluation: it starts in roughly a tenth of the time but evaluates several times slower.

WithDefaultImporter and WithDefaultCapabilities attach an import policy and a native function set to the engine itself, so every request gets them without repeating the wiring at each call site. A request still wins where the two overlap, but cannot remove a default it did not replace.

For runnable programs that progress from an inline evaluation to a request-serving integration, see the examples.

For a current go-jsonnet codebase, see MIGRATING.md for the compatibility contract, before-and-after code, supported API matrix, and rollout checklist.

Command-line usage

Install the Cgo-free command directly:

go install github.com/thevilledev/sonnetbox/cmd/sonnetbox@latest

Tagged releases also provide Linux, macOS, and Windows archives for amd64 and arm64. Each archive has an SPDX JSON SBOM and is covered by a keyless GitHub build-provenance attestation. After checking the downloaded archive against checksums.txt, verify its provenance with:

gh attestation verify --owner thevilledev sonnetbox_*.tar.gz

Multi-arch container images (linux/amd64, linux/arm64) publish to ghcr.io/thevilledev/sonnetbox on the same tags. Each image is Cosign keyless-signed and carries GitHub provenance plus SPDX SBOM attestations:

docker pull ghcr.io/thevilledev/sonnetbox:0.1.0
gh attestation verify \
  --owner thevilledev \
  oci://ghcr.io/thevilledev/sonnetbox:0.1.0
cosign verify \
  --certificate-identity-regexp='https://github.com/thevilledev/sonnetbox/\.github/workflows/release\.yml@refs/tags/v.*' \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
  ghcr.io/thevilledev/sonnetbox:0.1.0

Or build build/sonnetbox / a local image from a checkout:

make cli
make docker

Evaluate a file. Without --root, the file's containing directory is the entire read-only import workspace:

sonnetbox config/main.jsonnet

Grant a larger workspace explicitly when the program imports from parent or library directories. The entry filename and -J paths are interpreted relative to that root, and the rightmost library path wins:

sonnetbox \
  --root ./jsonnet \
  -J vendor \
  -J lib \
  -V environment=production \
  apps/main.jsonnet

Inline source and stdin have no importer unless --root is supplied:

sonnetbox -e -S '"hello"'
printf '%s\n' 'import "data.libsonnet"' |
  sonnetbox --root ./jsonnet -

The familiar output modes are available:

sonnetbox -o result.json config.jsonnet
sonnetbox -m generated -c files.jsonnet
sonnetbox -y stream.jsonnet

Run sonnetbox --help for the complete supported flag set. Every evaluation uses a fresh WASM guest and a five-second deadline configurable with a positive --timeout duration. std.trace is bounded and written to stderr. Multi-file names are source-controlled, so the command rejects absolute, non-canonical, and traversing names and confines writes beneath the requested output directory.

Every resource ceiling is adjustable. A flag sets one ceiling, --policy loads a JSON file of them, and a flag wins over the file. Sizes accept suffixes such as 512KiB or 16MB. The library validates the result, so a policy can only narrow what sonnetbox already permits:

sonnetbox --max-fuel 20000000 --max-memory 32MiB untrusted.jsonnet
sonnetbox --policy ./sandbox-policy.json untrusted.jsonnet

--print-policy writes the effective ceilings as JSON, which is itself a valid --policy file, so operators can capture and version the defaults:

sonnetbox --print-policy > sandbox-policy.json

The command caches compiled guest code beneath the user cache directory, which takes a warm run from roughly 2.6 seconds to 0.12 seconds. Because the cache holds executable machine code, it is created private to the current user. --cache-dir moves it and --no-cache disables it.

Failures are reported for people by default and for machines on request. --error-format=json writes a structured report naming the error kind, the exhausted resource and its limit, or the denied import path. Exit status distinguishes the failure classes: 0 success, 1 usage or host failure, 2 Jsonnet error, 3 exhausted budget, 4 denied import, and 5 canceled or timed out.

This is deliberately a secure subset rather than a drop-in jsonnet replacement:

  • JSONNET_PATH is ignored;
  • --ext-str, --ext-code, --tla-str, and --tla-code require name=value and never infer values from the environment;
  • variable-file flags, native functions, formatter and linter commands, and exact upstream error text are not supported; and
  • the operator-selected input, workspace, output file, and output directory are host-side grants, while Jsonnet itself receives no ambient host access.

Evaluation

The three input entry points have deliberately different import behavior:

  • Evaluate evaluates inline source relative to Request.Filename.
  • EvaluateAnonymous matches go-jsonnet's anonymous-snippet behavior: Filename is diagnostic only and imports start at the importer root.
  • EvaluateFile loads the root file through Request.Importer; it never opens the path in the guest.

Request.OutputMode selects one JSON value, a multi-file object, or a document stream. Results are returned in Result.Output, Result.Files, or Result.Documents. StringOutput unquotes top-level strings, and OmitTrailingNewline disables go-jsonnet's normal output newline.

External variables, external code, top-level arguments, and top-level code are request-scoped. Requests do not share mutable Jsonnet VM state.

Imports

If Request.Importer is nil, all imports are denied. The guest never uses go-jsonnet's FileImporter.

NewMapImporter is the safest option for an immutable virtual file set. NewWorkspaceImporter exposes a read-only host directory while preventing relative paths and symlinks from escaping it:

workspace, err := sonnetbox.NewWorkspaceImporter(
	"./jsonnet",
	sonnetbox.WithLibraryPaths("vendor", "lib"),
)
if err != nil {
	log.Fatal(err)
}
defer workspace.Close()

Library paths follow go-jsonnet FileImporter precedence. Virtual paths use canonical / separators. Normal relative imports such as ./lib.jsonnet and ../shared.libsonnet work when their resolved path remains inside the virtual root. Absolute paths, backslashes, volume-qualified paths, and root escapes are denied.

Custom importers are trusted host code. They receive the evaluation context, must return stable content for a canonical path, and must be safe for calls from concurrent evaluations. The engine applies per-import, cumulative import, and host-message byte limits to every importer.

Capabilities

Capabilities are request-scoped Jsonnet native functions:

Capabilities: map[string]sonnetbox.Capability{
	"lookup": {
		Params: []string{"key"},
		Call: func(ctx context.Context, args []any) (any, error) {
			return records[args[0].(string)], nil
		},
	},
}

Jsonnet calls the example with std.native("lookup")("key"). Arguments and results must be JSON-compatible. Panics and errors become typed capability errors.

Capabilities must be pure, deterministic queries. Jsonnet is lazy, so a native function may execute zero, one, or multiple times. Effectful operations and exactly-once semantics are unsupported. Handlers are trusted host code, may run concurrently across evaluations, and must honor cancellation.

Budgets and observability

EngineConfig defines ceilings for the engine. Zero fields select these defaults:

Ceiling Default
Guest linear memory 128 MiB
Deterministic WASM fuel 100,000,000 units
Source / one import 256 KiB each
Rendered output 1 MiB
Cumulative imports 2 MiB
Import resolutions 64
Capability calls 128
Host request / response 512 KiB each
Captured trace 64 KiB
Jsonnet stack 256 frames
Concurrent evaluations 4

Invalid values and values above the library's hard ceilings are rejected. Request.Limits can lower every per-evaluation ceiling except memory and concurrency; it can never raise the engine policy. Fuel deterministically bounds guest instruction work, while a context deadline remains the wall-clock backstop for host callbacks and evaluation. Evaluations waiting for a concurrency slot also honor cancellation.

Policy is a value, not a hidden constant. DefaultEngineConfig returns the table above, Ceilings returns the maximum each field accepts, and Engine.Config reports what an engine actually enforces. EngineConfig round-trips through JSON, and EngineConfig.Normalize applies and validates a policy without paying to compile the guest.

Set Request.CaptureTrace to collect bounded std.trace output in Result.Trace. Result.Stats reports deterministic fuel consumed, queue and execution duration, import count and bytes, capability calls, trace bytes, and trace truncation. Fuel is an abstract instruction unit, not elapsed time or a billing unit; the other fields are host-observed diagnostics.

A failed evaluation returns its trace and statistics alongside the error, so the evidence of why a template failed is not discarded. Nothing is recoverable when a fuel, memory, or deadline backstop traps the guest, because no further guest call can succeed.

WithObserver reports activity as it happens rather than as a total afterwards, which is what an audit trail needs. Hooks cover every import attempt, including the denials that a program probing for ungranted files would otherwise perform invisibly, every capability call, and every completed evaluation. NewSlogObserver writes them through log/slog with denials at warn level. Events carry paths, sizes, counts, and outcomes but never imported content or capability arguments, so an audit log cannot become a copy of the data crossing the sandbox.

Version() reports the embedded go-jsonnet version and the private host/guest ABI version, which is useful in logs and compatibility reports.

Compatibility contract

The embedded evaluator is go-jsonnet v0.22.0. For successful evaluations on the documented compatibility surface and within configured budgets, sonnetbox intends to return the same rendered bytes as that version. Differential tests cover variables, arguments, native functions, imports, traces, single output, multi-file output, and streams.

The contract does not promise identical concrete error types or strings, performance, mutable VM behavior, arbitrary newer go-jsonnet behavior, AST APIs, a debugger, or unrestricted filesystem imports. Security policy always wins over compatibility. See MIGRATING.md for the precise matrix and known migration work.

Security model

Jsonnet source is adversarial. Importers and capability implementations are trusted. Each evaluation:

  • instantiates and initializes a fresh, uniquely named WASI guest;
  • uses a precompiled module but no shared guest state;
  • validates every ABI function, pointer, length, and integer conversion;
  • exposes no ambient filesystem, environment, arguments, network, or inherited standard streams;
  • applies source, output, import, trace, host-call, capability, stack, deterministic instruction-fuel, linear-memory, and concurrency limits; and
  • closes and discards the guest after success, error, cancellation, or trap.

The output limit is checked after go-jsonnet renders the complete result. The linear-memory ceiling bounds transient rendering allocations before that check.

Wazero fuel deterministically terminates guest execution that exceeds the configured instruction budget. The caller's context deadline, enforced with WithCloseOnContextDone(true), remains the wall-clock backstop. Trusted host handlers do not consume guest fuel and can still block if they ignore cancellation.

A compilation cache stores machine code compiled from the embedded guest and loads it into the process on a later run, so its directory is part of the trusted computing base. Anyone who can write there can execute code as the user running sonnetbox. The command creates its cache private to the current user; a host passing its own directory must do the same and must never share one across trust boundaries. --no-cache removes the cache from the picture entirely.

An observer sees paths, sizes, counts, and outcomes, never imported content or capability arguments, so enabling an audit trail does not widen exposure of the data crossing the sandbox. Hooks run inline on the evaluation path as trusted host code.

The sandbox protects the host from adversarial Jsonnet, not from malicious importers or capabilities running as ordinary Go code. Engine.Close is idempotent, rejects new work, and aborts active guest calls.

ABI

The embedded guest uses private ABI version 7. The host sends one bounded evaluation request to guest-owned memory. Guest-to-host calls use one imported function for import resolution and capability invocation. Status values distinguish success, denial, handler failure, limits, cancellation, and malformed messages.

The guest exposes bounded result and trace buffers. Host callbacks copy requests before invoking trusted handlers, validate complete memory ranges, and never retain guest-memory views. The ABI is an internal implementation detail and is not a public extension point.

Rebuilding and development

The embedded reactor is generated with the exact Go version in .go-version:

make wasm
make wasm-check

The build uses CGO_ENABLED=0, GOOS=wasip1, GOARCH=wasm, -buildmode=c-shared, -trimpath, and a cleared build ID. CI rebuilds the module and verifies its bytes and checked-in SHA-256 checksum.

Run make check for formatting, module, lint, coverage, portability, and WASM reproducibility checks. make race and make fuzz-smoke provide the extended checks.

See CONTRIBUTING.md for the local workflow and SECURITY.md for private vulnerability reporting.

Documentation

Overview

Package sonnetbox evaluates untrusted Jsonnet programs in fresh WebAssembly guests.

An Engine compiles the embedded go-jsonnet guest once and creates an isolated guest instance for each evaluation. Engines are safe for concurrent use and should normally be long-lived:

engine, err := sonnetbox.NewEngine(ctx, sonnetbox.EngineConfig{})
if err != nil {
	return err
}
defer engine.Close(context.Background())

result, err := engine.Evaluate(ctx, sonnetbox.Request{
	Filename: "main.jsonnet",
	Source:   `{answer: 6 * 7}`,
})

Jsonnet code has no ambient access to the host filesystem, network, environment, arguments, or standard streams. A request can grant read-only virtual imports through an Importer and pure native functions through Capability. Both are trusted host code; implementations must honor context cancellation and the concurrency contracts documented on those types.

EngineConfig sets engine-wide resource ceilings. A Request can lower most ceilings for one evaluation through RequestLimits, but cannot raise them. Context cancellation provides the wall-clock backstop. DefaultEngineConfig and Ceilings report both ends of the valid range, EngineConfig.Normalize validates a policy without compiling the guest, and Engine.Config reports the policy an engine enforces.

An Option customizes an engine without widening the sandbox. Compiling the guest dominates NewEngine, so a process that cannot keep an engine alive should reuse compiled code through WithCompilationCache. WithDefaultImporter and WithDefaultCapabilities apply one policy to every request, and WithObserver reports imports, capability calls, and completed evaluations for audit.

Example
package main

import (
	"context"
	"fmt"

	"github.com/thevilledev/sonnetbox"
)

func main() {
	ctx := context.Background()
	engine, err := sonnetbox.NewEngine(ctx, sonnetbox.EngineConfig{})
	if err != nil {
		panic(err)
	}
	defer func() {
		if err := engine.Close(context.Background()); err != nil {
			panic(err)
		}
	}()

	result, err := engine.Evaluate(ctx, sonnetbox.Request{
		Filename: "main.jsonnet",
		Source:   `{answer: 6 * 7}`,
	})
	if err != nil {
		panic(err)
	}
	fmt.Print(string(result.Output))

}
Output:
{
   "answer": 42
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrImportDenied = errors.New("import denied")

ErrImportDenied can be returned by a custom Importer when a path is absent or rejected by policy.

Functions

This section is empty.

Types

type ABIError

type ABIError struct {
	// Err describes the protocol or guest artifact failure.
	Err error
}

ABIError reports a malformed or incompatible guest/host ABI.

func (*ABIError) Error

func (e *ABIError) Error() string

func (*ABIError) Unwrap

func (e *ABIError) Unwrap() error

type CancellationError

type CancellationError struct {
	// Err is the context cancellation cause.
	Err error
}

CancellationError reports evaluation cancellation or deadline expiry.

func (*CancellationError) Error

func (e *CancellationError) Error() string

func (*CancellationError) Unwrap

func (e *CancellationError) Unwrap() error

type Capability

type Capability struct {
	// Params lists the Jsonnet function's parameter names.
	Params []string
	// Call executes trusted host code with JSON-compatible arguments and must
	// honor context cancellation.
	Call func(context.Context, []any) (any, error)
}

Capability defines a pure Jsonnet native function. Call may execute zero, one, or multiple times and may be called concurrently by separate evaluations.

type CapabilityError

type CapabilityError struct {
	// Name identifies the failed capability when one is available.
	Name string
	// Err is the underlying trusted capability failure.
	Err error
}

CapabilityError reports a trusted capability failure.

func (*CapabilityError) Error

func (e *CapabilityError) Error() string

func (*CapabilityError) Unwrap

func (e *CapabilityError) Unwrap() error

type CapabilityEvent added in v0.2.0

type CapabilityEvent struct {
	// Name is the capability as declared in the request.
	Name string
	// Args is the number of arguments passed, not their values.
	Args int
	// Duration covers decoding, the handler call, and encoding the reply.
	Duration time.Duration
	// Err is the reason the call did not succeed, nil when it did.
	Err error
}

CapabilityEvent describes one native function call.

type CompilationCache added in v0.2.0

type CompilationCache struct {
	// contains filtered or unexported fields
}

CompilationCache stores compiled guest code so that engines can skip recompiling the embedded module. Compiling the guest dominates engine creation, so a shared cache is the difference between seconds and milliseconds for short-lived processes.

A cache is safe for concurrent use and may back any number of engines. It is owned by the caller: Engine.Close never closes it, so close it only after every engine using it has been closed.

func NewCompilationCache added in v0.2.0

func NewCompilationCache() *CompilationCache

NewCompilationCache returns an in-memory cache shared by the engines that use it. It speeds up creating several engines in one process but does not survive process exit.

func NewCompilationCacheDir added in v0.2.0

func NewCompilationCacheDir(dir string) (*CompilationCache, error)

NewCompilationCacheDir returns a cache persisted beneath dir, reused across processes. It is the useful form for short-lived commands, which otherwise recompile the guest on every invocation.

A cache directory holds executable machine code that later runs in this process. Point it at a directory only the current user can write, such as a path beneath os.UserCacheDir, and never at a world-writable or shared-tenant location.

func (*CompilationCache) Close added in v0.2.0

func (c *CompilationCache) Close(ctx context.Context) error

Close releases the cache. It is idempotent, and callers must close every engine that used the cache first.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine owns a compiled guest module and instantiates a fresh guest for every evaluation. An Engine is safe for concurrent use and must be closed when it is no longer needed.

func NewEngine

func NewEngine(
	ctx context.Context,
	config EngineConfig,
	options ...Option,
) (*Engine, error)

NewEngine compiles the embedded guest and prepares an isolated Jsonnet engine. Zero-valued config fields select documented defaults. The context controls initialization and is not retained; callers must close the returned Engine.

Compiling the guest dominates this call. Processes that create engines repeatedly, such as short-lived commands, should pass WithCompilationCache to reuse compiled code.

func (*Engine) Close

func (e *Engine) Close(ctx context.Context) error

Close rejects new evaluations and closes the runtime. It is idempotent and aborts active guest calls.

func (*Engine) Config added in v0.2.0

func (e *Engine) Config() EngineConfig

Config returns the effective configuration after defaults were applied and validation passed. It is useful for logging or reporting the policy that is actually in force, which may differ from the requested configuration.

func (*Engine) Evaluate

func (e *Engine) Evaluate(ctx context.Context, request Request) (Result, error)

Evaluate evaluates Request.Source in a fresh guest instance, using Request.Filename as the base for relative imports. It is safe to call concurrently.

func (*Engine) EvaluateAnonymous

func (e *Engine) EvaluateAnonymous(
	ctx context.Context,
	request Request,
) (Result, error)

EvaluateAnonymous evaluates Request.Source in a fresh guest instance. Request.Filename is used only for diagnostics; imports are resolved from the importer's root.

func (*Engine) EvaluateFile

func (e *Engine) EvaluateFile(
	ctx context.Context,
	filename string,
	request Request,
) (Result, error)

EvaluateFile loads filename through Request.Importer and evaluates it in a fresh guest instance. The filename must be a canonical virtual path, and Request.Source must be empty.

type EngineClosedError

type EngineClosedError struct {
	// Err is the underlying runtime error when an active evaluation was
	// interrupted by Close.
	Err error
}

EngineClosedError reports an evaluation attempted after Engine.Close.

func (*EngineClosedError) Error

func (e *EngineClosedError) Error() string

func (*EngineClosedError) Unwrap

func (e *EngineClosedError) Unwrap() error

type EngineConfig

type EngineConfig struct {
	// MaxMemoryBytes limits each guest's linear memory. It must be a multiple
	// of 64 KiB.
	MaxMemoryBytes uint64 `json:"max_memory_bytes"`
	// MaxFuel limits deterministic WebAssembly instruction work during one
	// evaluation.
	MaxFuel uint64 `json:"max_fuel"`
	// MaxSourceBytes limits Request.Source in bytes.
	MaxSourceBytes uint32 `json:"max_source_bytes"`
	// MaxOutputBytes limits the rendered result in bytes.
	MaxOutputBytes uint32 `json:"max_output_bytes"`
	// MaxStack limits go-jsonnet interpreter stack depth.
	MaxStack int `json:"max_stack"`
	// MaxImports limits import resolutions during one evaluation.
	MaxImports uint32 `json:"max_imports"`
	// MaxImportBytes limits one imported file in bytes.
	MaxImportBytes uint32 `json:"max_import_bytes"`
	// MaxTotalImportBytes limits all imported content during one evaluation.
	MaxTotalImportBytes uint64 `json:"max_total_import_bytes"`
	// MaxCapabilityCalls limits native capability calls during one evaluation.
	MaxCapabilityCalls uint32 `json:"max_capability_calls"`
	// MaxHostRequestBytes limits encoded requests crossing from guest to host
	// and the encoded evaluation request crossing from host to guest.
	MaxHostRequestBytes uint32 `json:"max_host_request_bytes"`
	// MaxHostResponseBytes limits encoded responses crossing from host to
	// guest. Import content is base64-encoded within this limit. Nonzero values
	// must be at least 256 bytes.
	MaxHostResponseBytes uint32 `json:"max_host_response_bytes"`
	// MaxTraceBytes limits captured std.trace output during one evaluation.
	MaxTraceBytes uint32 `json:"max_trace_bytes"`
	// MaxConcurrentEvaluations limits active guest instances. Additional
	// evaluations wait for capacity while honoring context cancellation.
	MaxConcurrentEvaluations uint32 `json:"max_concurrent_evaluations"`
}

EngineConfig sets resource ceilings for an Engine. A zero field selects the documented default for that field.

EngineConfig holds only policy values, so it round-trips through JSON and can be loaded from an operator-supplied policy file. Use DefaultEngineConfig to discover the defaults and Ceilings to discover the maximum value each field accepts.

func Ceilings added in v0.2.0

func Ceilings() EngineConfig

Ceilings returns the library's hard maximum for every EngineConfig field. NewEngine rejects any configuration above these values, so an operator policy can never widen the sandbox beyond them.

func DefaultEngineConfig added in v0.2.0

func DefaultEngineConfig() EngineConfig

DefaultEngineConfig returns the ceilings that a zero-valued EngineConfig selects. Callers can start from these defaults, adjust individual fields, and pass the result to NewEngine.

func (EngineConfig) Normalize added in v0.2.0

func (c EngineConfig) Normalize() (EngineConfig, error)

Normalize resolves zero-valued fields to their defaults and validates every field, returning the configuration an Engine would apply. NewEngine performs the same work, so Normalize lets a caller check or display an operator-supplied policy without paying to compile the guest.

type EvaluationError

type EvaluationError struct {
	// Err contains the evaluator's diagnostic.
	Err error
}

EvaluationError reports a static or runtime Jsonnet evaluation error.

func (*EvaluationError) Error

func (e *EvaluationError) Error() string

func (*EvaluationError) Unwrap

func (e *EvaluationError) Unwrap() error

type EvaluationEvent added in v0.2.0

type EvaluationEvent struct {
	// Filename is the evaluated file or snippet name.
	Filename string
	// Stats is the same value the evaluation reported, including for a
	// failure that reached the point of producing statistics.
	Stats EvaluationStats
	// Err is the reason the evaluation failed, nil when it succeeded.
	Err error
}

EvaluationEvent describes one completed evaluation.

type EvaluationStats

type EvaluationStats struct {
	// QueueDuration is the time spent waiting for an engine concurrency slot.
	QueueDuration time.Duration
	// ExecutionDuration is the time from acquiring a slot through decoding the
	// completed guest result.
	ExecutionDuration time.Duration
	// FuelConsumed is the deterministic WebAssembly instruction work used.
	FuelConsumed uint64
	// ImportResolutions is the number of import requests made by the guest.
	ImportResolutions uint32
	// ImportBytes is the cumulative size of imported content.
	ImportBytes uint64
	// CapabilityCalls is the number of native capability calls.
	CapabilityCalls uint32
	// TraceBytes is the number of captured std.trace bytes.
	TraceBytes uint32
	// TraceTruncated reports whether trace output exceeded its configured
	// limit.
	TraceTruncated bool
}

EvaluationStats reports the work an evaluation performed, including one that failed after the guest reported a status. FuelConsumed is deterministic for the same guest and input; durations and other host-observed counters are diagnostic.

type GuestTrapError

type GuestTrapError struct {
	// Operation identifies the guest operation that trapped.
	Operation string
	// Err is the underlying WebAssembly runtime error.
	Err error
}

GuestTrapError reports an unexpected WASM trap.

func (*GuestTrapError) Error

func (e *GuestTrapError) Error() string

func (*GuestTrapError) Unwrap

func (e *GuestTrapError) Unwrap() error

type ImportDeniedError

type ImportDeniedError struct {
	// ImportedFrom is the canonical path of the importing file, or empty when
	// resolving from the importer root.
	ImportedFrom string
	// ImportedPath is the path requested by Jsonnet.
	ImportedPath string
	// Err describes the policy denial or missing path.
	Err error
}

ImportDeniedError reports an import rejected by policy or not found.

func (*ImportDeniedError) Error

func (e *ImportDeniedError) Error() string

func (*ImportDeniedError) Unwrap

func (e *ImportDeniedError) Unwrap() error

type ImportError

type ImportError struct {
	// ImportedFrom is the canonical path of the importing file, or empty when
	// resolving from the importer root.
	ImportedFrom string
	// ImportedPath is the path requested by Jsonnet.
	ImportedPath string
	// Err is the underlying trusted importer failure.
	Err error
}

ImportError reports a trusted importer failure.

func (*ImportError) Error

func (e *ImportError) Error() string

func (*ImportError) Unwrap

func (e *ImportError) Unwrap() error

type ImportEvent added in v0.2.0

type ImportEvent struct {
	// ImportedFrom is the canonical path of the importing file, empty for the
	// top-level source.
	ImportedFrom string
	// ImportedPath is the path exactly as the Jsonnet program requested it.
	ImportedPath string
	// ResolvedPath is the canonical path the importer returned, empty unless
	// the import was served.
	ResolvedPath string
	// Bytes is the size of the served content, zero unless the import was
	// served.
	Bytes int
	// Duration covers validation, the importer call, and limit accounting.
	Duration time.Duration
	// Denied reports that sandbox policy refused the import. This is the
	// security-relevant outcome, distinct from an importer that failed.
	Denied bool
	// Err is the reason the import did not succeed, nil when it did.
	Err error
}

ImportEvent describes one import attempt.

type Importer

type Importer interface {
	// Import resolves importedPath relative to importedFrom. It returns a
	// canonical virtual path and its content. The content returned for a
	// canonical path must remain stable during an evaluation.
	Import(
		ctx context.Context,
		importedFrom string,
		importedPath string,
	) (canonicalPath string, content []byte, err error)
}

Importer resolves a Jsonnet import without granting guest filesystem access. Implementations are trusted host code and must be safe for concurrent calls from separate evaluations. They should return errors wrapping ErrImportDenied for paths that are absent or rejected by policy.

type InvalidRequestError

type InvalidRequestError struct {
	// Field identifies the rejected public field when one is available.
	Field string
	// Err describes why the request, context, or configuration was invalid.
	Err error
}

InvalidRequestError reports an invalid public request, context, or engine configuration.

func (*InvalidRequestError) Error

func (e *InvalidRequestError) Error() string

func (*InvalidRequestError) Unwrap

func (e *InvalidRequestError) Unwrap() error

type LimitError

type LimitError struct {
	// Resource identifies the exhausted resource.
	Resource string
	// Limit is the configured maximum.
	Limit uint64
	// Actual is the observed or attempted resource use.
	Actual uint64
	// Err is the underlying runtime error, when one is available.
	Err error
}

LimitError reports a configured resource limit.

func (*LimitError) Error

func (e *LimitError) Error() string

func (*LimitError) Unwrap

func (e *LimitError) Unwrap() error

type MapImporter

type MapImporter struct {
	// contains filtered or unexported fields
}

MapImporter resolves imports from an immutable map of canonical virtual paths.

func NewMapImporter

func NewMapImporter(files map[string][]byte) (*MapImporter, error)

NewMapImporter returns an immutable virtual-file importer. It validates all paths and copies all content before returning.

func (*MapImporter) Import

func (m *MapImporter) Import(
	ctx context.Context,
	importedFrom string,
	importedPath string,
) (string, []byte, error)

Import implements Importer.

type Observer added in v0.2.0

type Observer struct {
	// Import runs after every import attempt, whether it was served, refused
	// by policy, or failed.
	Import func(ctx context.Context, event ImportEvent)
	// Capability runs after every native function call.
	Capability func(ctx context.Context, event CapabilityEvent)
	// Evaluation runs once per evaluation, after it succeeds or fails.
	Evaluation func(ctx context.Context, event EvaluationEvent)
}

Observer receives notifications about sandbox activity, so a host can build an audit trail or metrics without inferring behavior from return values. A nil field is skipped, and adding a field later does not break existing implementations.

Hooks run inline on the evaluation path, inside the host call the guest is blocked on. They must return promptly and must not panic: a panic during an import or capability call is reported to the guest as a handler failure, and one during the evaluation hook unwinds to the caller.

Events describe activity, never content. They carry sizes, counts, and paths, but no imported bytes and no capability arguments, so an audit log cannot become an accidental copy of the data flowing through the sandbox.

func NewSlogObserver added in v0.2.0

func NewSlogObserver(logger *slog.Logger) *Observer

NewSlogObserver returns an Observer that writes sandbox activity to logger. Denied imports are logged at warn level because they are the security-relevant events; failures are logged at error level and ordinary activity at debug level.

type Option added in v0.2.0

type Option func(*engineOptions) error

Option customizes an Engine beyond the resource ceilings in EngineConfig. Options control how the guest is compiled and executed; they never widen the sandbox or raise a resource limit.

func WithCompilationCache added in v0.2.0

func WithCompilationCache(cache *CompilationCache) Option

WithCompilationCache reuses compiled guest code from cache. Engines sharing a cache still get isolated runtimes and fresh guest instances per evaluation; only the compiled code is shared.

func WithDefaultCapabilities added in v0.2.0

func WithDefaultCapabilities(capabilities map[string]Capability) Option

WithDefaultCapabilities registers native functions available to every request. A request can replace one by declaring the same name, but cannot remove it, so the set an operator grants is the widest any evaluation sees.

Capabilities must be pure, because Jsonnet laziness makes the number and order of calls unpredictable.

func WithDefaultImporter added in v0.2.0

func WithDefaultImporter(importer Importer) Option

WithDefaultImporter resolves imports for requests that do not set Request.Importer. It lets an operator establish one import policy for every evaluation instead of relying on each call site to attach the same importer. A request that supplies its own importer uses that one instead.

func WithInterpreter added in v0.2.0

func WithInterpreter() Option

WithInterpreter runs the guest on wazero's portable interpreter instead of the optimizing compiler. It starts far faster but evaluates far slower, so it suits one-shot evaluations and platforms without compiler support.

func WithObserver added in v0.2.0

func WithObserver(observer *Observer) Option

WithObserver reports sandbox activity to observer for audit and diagnostics. An observer cannot change any outcome; it only watches.

type OutputMode

type OutputMode uint8

OutputMode selects how the top-level Jsonnet value is manifested.

const (
	// OutputModeSingle manifests one JSON value, or one unquoted string when
	// Request.StringOutput is set.
	OutputModeSingle OutputMode = iota
	// OutputModeMulti manifests a top-level object as filename/output pairs.
	OutputModeMulti
	// OutputModeStream manifests a top-level array as a sequence of documents.
	OutputModeStream
)

type Request

type Request struct {
	// Filename is the virtual, canonical filename used in diagnostics and
	// relative imports. An empty value selects "snippet.jsonnet".
	Filename string
	// Source is the adversarial Jsonnet program to evaluate.
	Source string
	// ExtVars supplies string external variables.
	ExtVars map[string]string
	// ExtCode supplies Jsonnet-code external variables.
	ExtCode map[string]string
	// TLAVars supplies string top-level arguments.
	TLAVars map[string]string
	// TLACode supplies Jsonnet-code top-level arguments.
	TLACode map[string]string
	// Importer resolves virtual imports. A nil Importer denies all imports.
	Importer Importer
	// Capabilities exposes only these request-scoped native functions.
	Capabilities map[string]Capability
	// Limits optionally lowers this evaluation's resource limits.
	Limits RequestLimits
	// OutputMode selects single, multi-file, or stream manifestation.
	OutputMode OutputMode
	// StringOutput returns a top-level Jsonnet string without JSON quoting.
	// It applies to single and multi-file output.
	StringOutput bool
	// OmitTrailingNewline disables go-jsonnet's default output newline.
	OmitTrailingNewline bool
	// CaptureTrace returns bounded std.trace output in Result.Trace.
	CaptureTrace bool
}

Request describes one isolated Jsonnet evaluation.

type RequestLimits

type RequestLimits struct {
	// MaxFuel lowers EngineConfig.MaxFuel.
	MaxFuel uint64 `json:"max_fuel,omitempty"`
	// MaxSourceBytes lowers EngineConfig.MaxSourceBytes.
	MaxSourceBytes uint32 `json:"max_source_bytes,omitempty"`
	// MaxOutputBytes lowers EngineConfig.MaxOutputBytes.
	MaxOutputBytes uint32 `json:"max_output_bytes,omitempty"`
	// MaxStack lowers EngineConfig.MaxStack.
	MaxStack int `json:"max_stack,omitempty"`
	// MaxImports lowers EngineConfig.MaxImports.
	MaxImports uint32 `json:"max_imports,omitempty"`
	// MaxImportBytes lowers EngineConfig.MaxImportBytes.
	MaxImportBytes uint32 `json:"max_import_bytes,omitempty"`
	// MaxTotalImportBytes lowers EngineConfig.MaxTotalImportBytes.
	MaxTotalImportBytes uint64 `json:"max_total_import_bytes,omitempty"`
	// MaxCapabilityCalls lowers EngineConfig.MaxCapabilityCalls.
	MaxCapabilityCalls uint32 `json:"max_capability_calls,omitempty"`
	// MaxHostRequestBytes lowers EngineConfig.MaxHostRequestBytes.
	MaxHostRequestBytes uint32 `json:"max_host_request_bytes,omitempty"`
	// MaxHostResponseBytes lowers EngineConfig.MaxHostResponseBytes.
	MaxHostResponseBytes uint32 `json:"max_host_response_bytes,omitempty"`
	// MaxTraceBytes lowers EngineConfig.MaxTraceBytes.
	MaxTraceBytes uint32 `json:"max_trace_bytes,omitempty"`
}

RequestLimits lowers resource limits for one evaluation. A zero field inherits the corresponding EngineConfig ceiling. A nonzero field cannot exceed that ceiling.

type Result

type Result struct {
	// Output contains single-mode rendered JSON or StringOutput bytes.
	Output []byte
	// Files contains multi-mode rendered outputs keyed by filename.
	Files map[string][]byte
	// Documents contains stream-mode rendered documents in source order.
	Documents [][]byte
	// Trace contains captured std.trace output.
	Trace []byte
	// Stats reports bounded host-observed evaluation work.
	Stats EvaluationStats
}

Result is a completed Jsonnet evaluation. The request's OutputMode selects Output, Files, or Documents for the manifested value.

A failed evaluation still returns Trace and Stats alongside its error when Request.CaptureTrace is set and the guest reached the point of reporting a status. The manifested value is empty in that case. Nothing is recoverable when the guest is trapped by a fuel, memory, or deadline backstop, because no further guest call can succeed.

type VersionInfo

type VersionInfo struct {
	// Jsonnet is the embedded go-jsonnet semantic version.
	Jsonnet string
	// ABI is the private sonnetbox host/guest protocol version.
	ABI uint32
}

VersionInfo identifies the evaluator and private host/guest ABI.

func Version

func Version() VersionInfo

Version reports the embedded go-jsonnet evaluator and private host/guest ABI versions.

type WorkspaceImporter

type WorkspaceImporter struct {
	// contains filtered or unexported fields
}

WorkspaceImporter exposes read-only files beneath one host directory. It prevents relative paths and symlinks from escaping that directory.

func NewWorkspaceImporter

func NewWorkspaceImporter(
	rootPath string,
	options ...WorkspaceOption,
) (*WorkspaceImporter, error)

NewWorkspaceImporter opens a traversal-resistant, read-only workspace root.

func (*WorkspaceImporter) Close

func (w *WorkspaceImporter) Close() error

Close releases the workspace root. It is idempotent.

func (*WorkspaceImporter) Import

func (w *WorkspaceImporter) Import(
	ctx context.Context,
	importedFrom string,
	importedPath string,
) (string, []byte, error)

Import implements Importer.

type WorkspaceOption

type WorkspaceOption func(*workspaceConfig) error

WorkspaceOption configures a WorkspaceImporter.

func WithLibraryPaths

func WithLibraryPaths(paths ...string) WorkspaceOption

WithLibraryPaths adds virtual Jsonnet library paths. They are searched in reverse order after resolution relative to the importing file, matching go-jsonnet FileImporter precedence.

Directories

Path Synopsis
cmd
sonnetbox command
Command sonnetbox evaluates Jsonnet in a fresh WebAssembly sandbox.
Command sonnetbox evaluates Jsonnet in a fresh WebAssembly sandbox.
sonnetbox-guest command
Package main builds the embedded sonnetbox WASI guest.
Package main builds the embedded sonnetbox WASI guest.
compat
gojsonnet
Package gojsonnet provides an opt-in migration surface shaped like the common github.com/google/go-jsonnet VM API.
Package gojsonnet provides an opt-in migration surface shaped like the common github.com/google/go-jsonnet VM API.
examples
config-renderer command
Command config-renderer renders a file from a read-only Jsonnet workspace.
Command config-renderer renders a file from a read-only Jsonnet workspace.
hello command
Command hello demonstrates one isolated inline Jsonnet evaluation.
Command hello demonstrates one isolated inline Jsonnet evaluation.
http-service command
Command http-service exposes bounded Jsonnet evaluation over HTTP.
Command http-service exposes bounded Jsonnet evaluation over HTTP.
internal
cli
Package cli implements the secure sonnetbox command-line interface.
Package cli implements the secure sonnetbox command-line interface.
guestblob
Package guestblob embeds the reproducibly built Jsonnet WASI guest.
Package guestblob embeds the reproducibly built Jsonnet WASI guest.
protocol
Package protocol defines the private host-to-guest wire protocol.
Package protocol defines the private host-to-guest wire protocol.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL