Documentation
¶
Overview ¶
Package capcompute is the kernel of a small library operating system for Extism compute guests: wasm programs run as processes whose only access to the outside world is host-dispatched syscalls.
The package owns the compiled program image (Kernel), process lifecycle, and the guest-to-host syscall wiring. Concrete storage, durable reconstruction, and application scheduling stay outside this package.
A typical runtime does the following:
- build a Kernel with a wasm Manifest and a ProcessTable;
- create a Process from a ProcessSpec, which carries the process's dispatcher;
- save that Process in the ProcessTable before invoking Resume if the guest can make syscalls;
- call Resume and read the single ResumeResult from the returned handle;
- close processes and the Kernel at the application boundary.
The syscall host function receives only the PID through context. It loads the Process from the ProcessTable and dispatches the guest Syscall through the process dispatcher. This keeps runtime lookup explicit and avoids hidden invocation state in context.
Resume has four observable outcomes. A successful guest call whose JSON output contains {"status":"yielded"} returns ResumeYielded, while an explicit {"status":"completed"} returns ResumeCompleted. Missing or unsupported status values return ResumeFailed. A stopped invocation returns ResumeStopped and permanently terminates that physical process. Guest and runtime errors also return ResumeFailed.
ProcessTable is a runtime lookup boundary. Durable stores should persist the data needed by their application to recreate processes, then hydrate a fresh Kernel with CreateProcess and SaveProcess when a host process restarts. CreateProcess deliberately does not save processes; callers decide when a process becomes visible to syscalls and when it is removed.
The library does not own replay scheduling, async completion, durable journal policy, or process cleanup timing. Those are conventions of the wrapping system using this package.
Index ¶
- Variables
- type ChildRunner
- type Config
- type Declassifier
- type DeclassifyRequest
- type DeclassifyResult
- type FlowMonitor
- type GrantSource
- type Kernel
- type Labeler
- type PID
- type Process
- type ProcessSpec
- type ProcessTable
- type RateLimit
- type ResumeHandle
- type ResumeResult
- type ResumeStatus
- type SpawnConfig
- type SpawnRequest
- type Spawner
- type Stack
- type Taints
- type Throttle
- type Validator
Constants ¶
This section is empty.
Variables ¶
var ( ErrAmbientAuthority = errors.New("image grants ambient authority") ErrImageMemoryOverride = errors.New("image overrides the kernel-owned memory cap") ErrInvalidGuestOutput = errors.New("invalid guest output") ErrProcessActive = errors.New("process is already active") ErrProcessRequired = errors.New("process is required") ErrProcessTableRequired = errors.New("process table is required") ErrProcessTerminated = errors.New("process is terminated") )
Functions ¶
This section is empty.
Types ¶
type ChildRunner ¶
type ChildRunner[K any] func(ctx context.Context, child K, granted []sys.Capability, request SpawnRequest) (ResumeResult[K], error)
ChildRunner runs one child process until it completes, yields, fails, or is stopped. The child's own journal (keyed by the child cred's PID) and its dispatcher chain are the runner's to wire; KernelChildRunner is the kernel-backed implementation.
func KernelChildRunner ¶
func KernelChildRunner[ID comparable, K PID[ID]]( kernel *Kernel[ID, K], program string, childDispatcher func(ctx context.Context, child K, granted []sys.Capability) (sys.Dispatcher[K], error), ) ChildRunner[K]
KernelChildRunner runs children on a kernel: it creates the child process, registers it in the process table (the syscall path finds it by PID), gives it the CPU, and closes it once its quantum ends. childDispatcher wires the child's own chain — its journal tape (keyed by the child's PID), validator, and drivers over exactly the granted set (Stack.ForProcess is the natural fit).
A kernel is bound to one program image, so the runner is too: program names what this kernel runs, and a spawn requesting anything else is refused rather than silently running the wrong code. Multi-program apps compose a ChildRunner that routes on request.Program across per-program runners.
type Config ¶
type Config[ID comparable, K PID[ID]] struct { Image extism.Manifest PluginConfig extism.PluginConfig ProcessTable ProcessTable[ID, K] // MaxMemoryPages caps each process's linear memory in 64KiB wasm pages // (0 = runtime default). A guest allocating past the cap traps and the // resume reports ResumeFailed. MaxMemoryPages uint32 // ResumeTimeout bounds the wall-clock time of one Resume quantum // (0 = unbounded). A guest still running at the deadline is stopped and // the resume reports ResumeStopped with context.DeadlineExceeded. ResumeTimeout time.Duration }
Config contains everything needed to compile a program and create per-process instances.
Image is the program image (an Extism manifest naming the wasm and static config). It must not grant ambient authority: AllowedHosts and AllowedPaths must be empty — network and filesystem access are capabilities served by the dispatcher, never ambient rights. Guest module configuration (clock, RNG, env) is owned by the kernel and cannot be supplied by the caller.
type Declassifier ¶
type Declassifier[K any] struct { // contains filtered or unexported fields }
Declassifier serves the reserved sys.declassify syscall — DIFC declassification as a governed operation. Every crossing requires a human approval (there is no unapproved path: an unattended declassify would just be flow-policy bypass), and the approved crossing is a journaled syscall result, so replay re-applies it without asking again. It must sit *below* the replay layer; the FlowMonitor above performs the actual taint removal when the result passes through it.
func NewDeclassifier ¶
func NewDeclassifier[K any](next sys.Dispatcher[K]) *Declassifier[K]
func (*Declassifier[K]) Capabilities ¶
func (d *Declassifier[K]) Capabilities() []sys.Capability
Capabilities publishes sys.declassify (schema'd) alongside the chain's own capabilities; whether a given run may call it is the manifest's decision, enforced by the Validator's grant set like any capability.
func (*Declassifier[K]) Dispatch ¶
func (d *Declassifier[K]) Dispatch(ctx context.Context, cred K, syscall sys.Syscall, auth sys.Authorization) (sys.SyscallResult, error)
type DeclassifyRequest ¶
DeclassifyRequest is the args payload of the reserved sys.declassify syscall: which labels to lift from the run's taint, and why — the reason is required because it is what the approving human reads and what the journal preserves.
type DeclassifyResult ¶
type DeclassifyResult struct {
Declassified []string `json:"declassified"`
}
DeclassifyResult is the journaled outcome of an approved declassification.
type FlowMonitor ¶
type FlowMonitor[ID comparable, K PID[ID]] struct { // contains filtered or unexported fields }
FlowMonitor enforces information-flow policy at the reference monitor (the CaMeL architecture as a kernel primitive). The guest is opaque, so flow is judged conservatively: every label a process observes taints everything it later emits. A syscall to a capability whose Forbid set intersects the process's accumulated taint is refused with ErrnoDenied before any driver runs.
It sits *above* the replay layer: replayed results flow through it, so a crash-restarted host rebuilds the run's taint from the journal exactly. Taint state is the shared Taints; the monitor itself is per-run wiring (see Stack for the canonical chain).
func NewFlowMonitor ¶
func NewFlowMonitor[ID comparable, K PID[ID]](taints *Taints[ID], next sys.Dispatcher[K]) *FlowMonitor[ID, K]
func (*FlowMonitor[ID, K]) Capabilities ¶
func (m *FlowMonitor[ID, K]) Capabilities() []sys.Capability
func (*FlowMonitor[ID, K]) Dispatch ¶
func (m *FlowMonitor[ID, K]) Dispatch(ctx context.Context, cred K, syscall sys.Syscall, auth sys.Authorization) (sys.SyscallResult, error)
type GrantSource ¶
type GrantSource[K any] func(cred K) []sys.Capability
GrantSource reports the capabilities granted to a cred. The app supplies where grants live (the manifest, a policy store); the Validator supplies the enforcement.
type Kernel ¶
type Kernel[ID comparable, K PID[ID]] struct { // contains filtered or unexported fields }
Kernel owns one compiled program image and spawns processes from it.
func NewKernel ¶
func NewKernel[ID comparable, K PID[ID]](ctx context.Context, config Config[ID, K]) (*Kernel[ID, K], error)
NewKernel compiles a program and registers the syscall host function. It rejects images that grant ambient authority (non-empty AllowedHosts or AllowedPaths) with ErrAmbientAuthority.
func (*Kernel[ID, K]) CreateProcess ¶
func (k *Kernel[ID, K]) CreateProcess(ctx context.Context, spec ProcessSpec[ID, K]) (*Process[K], error)
CreateProcess instantiates a fresh process from the kernel's program image. It does not save the process; the caller decides when it becomes visible in the process table.
type Labeler ¶
type Labeler[K any] struct { // contains filtered or unexported fields }
Labeler stamps provenance onto every result: the deriving capability ("syscall:<name>") plus the capability's declared source classes (Capability.Labels). It sits *below* the replay layer so stamped labels are journaled with the completion record — provenance becomes part of the audit trail for free.
func NewLabeler ¶
func NewLabeler[K any](next sys.Dispatcher[K]) *Labeler[K]
func (*Labeler[K]) Capabilities ¶
func (l *Labeler[K]) Capabilities() []sys.Capability
type PID ¶
type PID[ID comparable] interface { PID() ID }
PID lets user-owned process data expose the stable process identity used for process maps. (Compare Go's stdlib `error` interface: the type and its one method share a name.)
type Process ¶
type Process[K any] struct { Cred K Input json.RawMessage Entrypoint string // contains filtered or unexported fields }
Process owns the reusable Extism plugin instance for one PID. Process state is not thread-safe; callers coordinate concurrent use.
Cred is the host-side credential for the process: it identifies the process (PID) and carries whatever authority context the app attaches. It is never visible to or supplied by the guest.
func (*Process[K]) Capabilities ¶
func (process *Process[K]) Capabilities() []sys.Capability
Capabilities returns the operations exposed by this process's dispatcher.
type ProcessSpec ¶
type ProcessSpec[ID comparable, K PID[ID]] struct { Input json.RawMessage Entrypoint string Cred K Dispatcher sys.Dispatcher[K] }
ProcessSpec describes one process to create: its program input, entrypoint, credential, and the dispatcher that will serve its syscalls.
type ProcessTable ¶
type ProcessTable[ID comparable, K PID[ID]] interface { LoadProcess(ctx context.Context, pid ID) (*Process[K], error) SaveProcess(ctx context.Context, pid ID, process *Process[K]) error }
ProcessTable owns per-PID processes.
type RateLimit ¶
type RateLimit struct {
// contains filtered or unexported fields
}
RateLimit is the cross-process token-bucket state behind syscall throttling — shared by all of a host's per-process chains (Stack holds one), while the Throttle decorator that consumes it is wired per process. It only ever *delays* — never denies — because a wall-clock-dependent refusal would be guest-visible nondeterminism, while a delay is invisible to a guest that has no ambient clock. Backpressure, like the scheduler's quotas.
func NewRateLimit ¶
NewRateLimit bounds each key to rate tokens/sec with the given burst (minimum 1). A rate <= 0 disables throttling.
type ResumeHandle ¶
type ResumeHandle[K any] struct { // contains filtered or unexported fields }
ResumeHandle controls one active guest invocation.
func (*ResumeHandle[K]) Results ¶
func (h *ResumeHandle[K]) Results() <-chan ResumeResult[K]
Results returns the channel that receives exactly one result before closing.
func (*ResumeHandle[K]) Stop ¶
func (h *ResumeHandle[K]) Stop()
Stop interrupts this invocation. It is safe to call Stop more than once.
type ResumeResult ¶
type ResumeResult[K any] struct { Status ResumeStatus Output json.RawMessage Exit uint32 Err error }
ResumeResult is delivered when the resume goroutine exits.
type ResumeStatus ¶
type ResumeStatus string
ResumeStatus is the result of one guest invocation attempt.
const ( ResumeCompleted ResumeStatus = "completed" ResumeYielded ResumeStatus = "yielded" ResumeStopped ResumeStatus = "stopped" ResumeFailed ResumeStatus = "failed" )
type SpawnConfig ¶
type SpawnConfig[K any] struct { // Grants reports the parent's capability set — the ceiling a child's // grants are attenuated under. Grants GrantSource[K] // DeriveCred mints the child credential. spawnKey is the spawn syscall's // idempotency key — the intent identity (process, position, call-hash) — // so the derived child is deterministic: a replayed or re-entered spawn // derives the same child, which is what lets a yielded child's journal be // found again on resume. DeriveCred func(parent K, spawnKey string, program string) K // Run executes the child. Run ChildRunner[K] // ChildLabels reports a completed child's accumulated taint, stamped onto // the spawn result so the parent's FlowMonitor observes exactly what the // child observed — the child's reads become the parent's taint, closing the // cross-spawn laundering path (a parent must not launder a forbidden source // by delegating the read to a child and reading back the answer). // // Optional, but load-bearing whenever flow policy is in play: the Spawner is // deliberately taint-unaware (taint is the FlowMonitor's shared concern, not // the spawn primitive's), so a wiring that shares one Taints across parent // and child MUST set this — e.g. return taints.Snapshot(child.PID()) — or a // child's taint silently fails to reach the parent. nil propagates no child // taint, correct only when parent and child do not share flow state. ChildLabels func(child K) []string }
SpawnConfig wires the Spawner decorator.
type SpawnRequest ¶
type SpawnRequest struct {
Program string `json:"program"`
Entrypoint string `json:"entrypoint,omitempty"` // defaults to "run"
Input json.RawMessage `json:"input,omitempty"`
// Capabilities names the grants the child receives. Attenuation law:
// every name must be in the parent's own grant set.
Capabilities []string `json:"capabilities,omitempty"`
}
SpawnRequest is the args payload of the reserved sys.spawn syscall.
type Spawner ¶
type Spawner[K any] struct { // contains filtered or unexported fields }
Spawner is the kernel-provided dispatcher decorator serving sys.spawn: capability attenuation, deterministic child identity, and the sync-first child-workflow protocol (the child borrows the parent's quantum; a yielding child yields the parent transitively). It must sit *below* the parent's replay layer: a completed spawn is then journaled like any syscall, so replay serves the child's result without re-spawning, and the spawn's idempotency key is available for cred derivation.
func NewSpawner ¶
func NewSpawner[K any](config SpawnConfig[K], next sys.Dispatcher[K]) *Spawner[K]
func (*Spawner[K]) Capabilities ¶
func (s *Spawner[K]) Capabilities() []sys.Capability
Capabilities exposes sys.spawn (with its input schema) alongside the chain's own capabilities, so grant-set validation and discovery see it.
type Stack ¶
type Stack[ID comparable, K PID[ID]] struct { // Grants is the manifest seam: the capability set granted to a cred. // Required — a stack without a grant source is not a reference monitor. Grants GrantSource[K] // Taints is the shared cross-process taint state. Required — flow policy // is not optional in the canonical chain; grant nothing with Forbid sets // and it is inert. Taints *Taints[ID] // Limit enables syscall-rate throttling. Optional. Limit *RateLimit // RateKeyOf picks the throttle's accounting bucket (default: the PID, so // each process is limited independently; return the tenant to aggregate). RateKeyOf func(cred K) string // Spawn enables sys.spawn. Optional. Spawn *SpawnConfig[K] // OpenIntents overrides the open-intent policy (default: retry under the // original idempotency key). OpenIntents replay.OpenIntentPolicy }
Stack wires the kernel's canonical dispatcher chain. The order is the load-bearing part, so it lives in code instead of prose:
Validator → Throttle → FlowMonitor → [replay] → Labeler → Declassifier → Spawner → drivers
Above the replay layer sit the pieces whose decisions must re-derive deterministically on every pass and never enter the journal: validation and flow denials replay identically from the same grant set and taint, and the throttle's delays are invisible to a clockless guest. Below the replay layer sit the pieces whose outcomes must be journaled exactly once and served from the tape thereafter: stamped labels, approved declassification crossings, and spawned child results — all of which also need the tape's idempotency keys. Assembling this by hand and getting one layer on the wrong side silently breaks a kernel law; ForProcess cannot.
The Stack holds the chain's cross-process components (grant source, taint state, rate limit, spawn config); ForProcess completes it with the one per-process piece — the tape — and the process's drivers.
func (Stack[ID, K]) ForProcess ¶
func (s Stack[ID, K]) ForProcess(tape replay.Tape, drivers sys.Dispatcher[K]) (sys.Dispatcher[K], error)
ForProcess assembles the chain for one process around its tape and drivers.
type Taints ¶
type Taints[ID comparable] struct { // contains filtered or unexported fields }
Taints is the cross-process taint state: every label each process has observed. It is shared by all of a host's per-process monitor chains (Stack holds one), while the FlowMonitor decorator that consumes it is wired per process — state and wiring have different lifetimes, so they are different types.
func NewTaints ¶
func NewTaints[ID comparable]() *Taints[ID]
func (*Taints[ID]) Declassify ¶
Declassify removes labels from a process's accumulated taint — an explicit, governed crossing of a label boundary (DIFC declassification). Guests reach it through the sys.declassify syscall (the Declassifier decorator), whose approved result replays through the FlowMonitor and lands here; the direct method remains for host-side administrative use.
func (*Taints[ID]) ForgetProcess ¶
func (t *Taints[ID]) ForgetProcess(pid ID)
ForgetProcess releases a terminated process's taint state.
type Throttle ¶
type Throttle[K any] struct { // contains filtered or unexported fields }
Throttle applies a shared RateLimit in front of one process's dispatcher chain (CHALLENGE B, M2.2 — the syscalls-per-second half of aggregate resource control). KeyOf picks the accounting bucket: cred→PID rate-limits each process, cred→tenant rate-limits an owner's aggregate.
func NewThrottle ¶
func (*Throttle[K]) Capabilities ¶
func (t *Throttle[K]) Capabilities() []sys.Capability
type Validator ¶
type Validator[K any] struct { // contains filtered or unexported fields }
Validator is the reference-monitor decorator: complete mediation for the dispatcher chain behind it (kernel law #4). Before delegating it checks (1) the cred's grant set contains the syscall name — otherwise ErrnoDenied — and (2) the args validate against the granted capability's InputSchema — otherwise ErrnoInvalidArgs. Reserved control syscalls (sys.begin/sys.commit) pass through: they are kernel markers, not capabilities.
Policy refusals are results (StatusFailed), not Go errors: the guest sees a classified errno and can react; the process does not crash.
func NewValidator ¶
func NewValidator[K any](grants GrantSource[K], next sys.Dispatcher[K]) *Validator[K]
NewValidator wraps next so every dispatch is checked against the cred's grant set and the capability's input schema.
func (*Validator[K]) Capabilities ¶
func (v *Validator[K]) Capabilities() []sys.Capability
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package otelexport renders a journal as OpenTelemetry spans: the process is the trace root, each intent/completion pair is a span, and the record envelope maps to attributes — the scope hierarchy (tenant → session → process → revision) aligns 1:1 with OTel trace/span/parent, so the exporter is a column mapping, not a translation layer.
|
Package otelexport renders a journal as OpenTelemetry spans: the process is the trace root, each intent/completion pair is a span, and the record envelope maps to attributes — the scope hierarchy (tenant → session → process → revision) aligns 1:1 with OTel trace/span/parent, so the exporter is a column mapping, not a translation layer. |
|
Package sched is the scheduler seam: it decides *when* a process gets the CPU, while the app decides *what* runs (activation — typically journal replay) and the kernel decides *how* (Resume).
|
Package sched is the scheduler seam: it decides *when* a process gets the CPU, while the app decides *what* runs (activation — typically journal replay) and the kernel decides *how* (Resume). |
|
Package sim is the deterministic simulation harness for the kernel's durability laws (FoundationDB/Antithesis-style, scaled to this kernel).
|
Package sim is the deterministic simulation harness for the kernel's durability laws (FoundationDB/Antithesis-style, scaled to this kernel). |
|
Context handoffs across the dispatcher chain: the replay layer hands drivers the in-flight intent identity; the flow monitor hands them the process's accumulated taint.
|
Context handoffs across the dispatcher chain: the replay layer hands drivers the in-flight intent identity; the flow monitor hands them the process's accumulated taint. |
|
replay
Package replay decorates a dispatcher so a re-run guest sees the same results it saw the first time: each syscall is served from a Tape if already recorded, otherwise journaled as an intent, delegated to the underlying dispatcher, and journaled as a completion.
|
Package replay decorates a dispatcher so a re-run guest sees the same results it saw the first time: each syscall is served from a Tape if already recorded, otherwise journaled as an intent, delegated to the underlying dispatcher, and journaled as a completion. |
|
replay/tape/journaled
Package journaled is a replay Tape backed by an append-only Journal of intent/completion records: an intent is appended before a syscall executes (journal-before-execute), a completion after its outcome is known and before the guest observes it (journal-before-observe).
|
Package journaled is a replay Tape backed by an append-only Journal of intent/completion records: an intent is appended before a syscall executes (journal-before-execute), a completion after its outcome is known and before the guest observes it (journal-before-observe). |
|
wire
Package wire is the ABI v3 envelope codec: the messages of envelope.proto in proto3 wire format, hand-rolled and reflection-free.
|
Package wire is the ABI v3 envelope codec: the messages of envelope.proto in proto3 wire format, hand-rolled and reflection-free. |