sandbox

package
v0.14.3-dev Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package sandbox implements outpost's safe-by-default container "sandbox" provider: a filtered libpod/docker API proxy that strips the escape-bearing knobs (privileged, host namespaces, host bind-mounts, added capabilities, devices) and injects per-request resource caps, so a remote caller who clears the cloudbox elevation gate can run containers without getting root-equivalent control of the host.

It mirrors internal/agent/ollama: a small Service glues the in-flight Counter to the two outpost-side surfaces it feeds — the proxy-wrap middleware (live load tracking) and the /_pool/capacity intercept (cloudbox-side scheduling queries). The companion filter.go is the security core; policy.go holds the operator-tunable limits.

This is deliberately distinct from the raw /app/podman/ passthrough (which stays admin-only, for trusted self-use): the sandbox mount is what a thin client or an untrusted tenant talks to.

Index

Constants

View Source
const CapabilityType = "sandbox"

CapabilityType is the AppCapabilities.Type value outpost advertises for the sandbox mount via GET /apps, so cloudbox can discover sandbox- bearing hosts without a separate probe — the same mechanism the ollama builtin uses to advertise {type:"llm"}.

Variables

This section is empty.

Functions

This section is empty.

Types

type CapacityReport

type CapacityReport struct {
	Version int `json:"version"`
	// MaxContainers is the policy ceiling on concurrent sandbox
	// containers this host will run. Zero means "unset / no explicit
	// ceiling" — cloudbox treats zero as "use the host's own judgement",
	// never as "cannot run anything", mirroring the ollama zero-as-
	// default convention.
	MaxContainers int `json:"max_containers"`
	// InFlight is the number of sandbox create/exec requests currently
	// being served through this mount — a live load proxy until the warm
	// pool tracks real running-container counts.
	InFlight int `json:"in_flight"`
	// PoolWarm / PoolWarming are the pre-warmed-container depth and the
	// number currently being replenished. Reserved for a future warm
	// CONTAINER pool; zero today.
	PoolWarm    int `json:"pool_warm"`
	PoolWarming int `json:"pool_warming"`
	// WarmImages / PrewarmImages report the image prewarmer's state: how
	// many of the configured base images are pulled and ready vs. the
	// total it manages. A remote create that targets a ready image skips
	// the pull cost (the dominant cold-start latency).
	WarmImages    int `json:"warm_images,omitempty"`
	PrewarmImages int `json:"prewarm_images,omitempty"`
	// Isolation names the OCI runtime tier this host enforces:
	// "runc" (shared kernel, the Phase-A default), "gvisor", or "kata".
	// cloudbox routes untrusted work only to hosts advertising a
	// VM/sandbox-grade runtime; empty is treated as "runc".
	Isolation string `json:"isolation,omitempty"`
}

CapacityReport is the JSON body served at /app/sandbox/_pool/capacity and (later) pushed to cloudbox's sandbox registry. It is the analog of ollama.CapacityReport: cloudbox's router reads it to pick the warmest / least-loaded host when distributing a sandbox request across the fleet.

Version is bumped when the shape changes so cloudbox can decode old and new payloads. PoolWarm / PoolWarming stay zero until the Phase-A warm pool lands; they are part of the wire shape now so adding the pool later needs no schema change.

type Counter

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

Counter tracks in-flight sandbox create/exec requests proxied through this outpost's /app/sandbox/* route. It is the load signal cloudbox's router reads (via CapacityReport.InFlight) to pick the least-loaded host when distributing a sandbox request across the fleet — the container analog of ollama.Counter.

Only the request shapes that actually spin up work count: container create and exec create. Listing / inspect / pulls are free, the same way ollama's /api/tags pulls don't burn a generation slot.

func NewCounter

func NewCounter(maxContainers int) *Counter

NewCounter returns a Counter whose advertised ceiling is maxContainers (0 == unset).

func (*Counter) Snapshot

func (c *Counter) Snapshot() CapacityReport

Snapshot returns the live capacity report sans pool fields (the Service overlays those). InFlight is an atomic load; MaxContainers is immutable after construction.

func (*Counter) Wrap

func (c *Counter) Wrap(next http.Handler) http.Handler

Wrap returns next instrumented so requests that spin up work bump the in-flight gauge for the duration of the request. Decrement is "first of two events" — handler return OR inbound-context cancel — so a hung daemon doesn't pin the slot past a client disconnect, mirroring the ollama counter's leak-defense.

type Filter

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

Filter is the security core: an http middleware that inspects container create / exec-create requests on the way to the local libpod daemon and either denies them (escape-bearing knobs) or rewrites them (inject / clamp resource caps). Everything else passes through untouched.

It understands two on-the-wire shapes because the daemon serves both:

  • docker-compat: POST /v1.x/containers/create (PascalCase, HostConfig)
  • libpod (native): POST /v4/libpod/containers/create (lowercase SpecGenerator)

ycode's gateway drives the libpod shape (it uses podman's Go bindings); a stock `docker` CLI/SDK drives the docker-compat shape. Both are gated.

Default-deny posture: on a create path whose body can't be parsed, the request is rejected rather than forwarded — we never forward a create we couldn't vet.

func NewFilter

func NewFilter(policy Policy) *Filter

NewFilter returns a Filter bound to policy.

func (*Filter) Wrap

func (f *Filter) Wrap(next http.Handler) http.Handler

Wrap returns next guarded by the filter. The path it matches on is the already-prefix-stripped upstream path (AppRegistry.ProxyTo sets r.URL.Path to the post-/app/<name> remainder before the proxy wrap runs), so a request to /app/sandbox/v1.41/containers/create arrives here as /v1.41/containers/create.

type Policy

type Policy struct {
	// MaxMemoryBytes, when > 0, is injected as the per-container memory
	// limit if the create request didn't set one. A request asking for
	// MORE than this is clamped down to it.
	MaxMemoryBytes int64
	// NanoCPUs, when > 0, is the per-container CPU cap in docker
	// "NanoCpus" units (1e9 == 1 CPU). Injected/clamped like memory.
	NanoCPUs int64
	// PidsLimit, when > 0, caps the number of processes per container
	// (fork-bomb defense). Injected/clamped like memory.
	PidsLimit int64
	// MaxContainers is the advertised ceiling on concurrent sandbox
	// containers (surfaced in CapacityReport so cloudbox can stop
	// routing here when full). Zero means "unset".
	MaxContainers int
	// AllowedImages, when non-empty, is an allowlist of image references
	// (exact match or a "repo/*" wildcard) a create request may use. An
	// empty list allows any image — appropriate for the trusted-fleet
	// Phase-A default; tighten it for multi-tenant Phase B.
	AllowedImages []string
	// ScratchHostPrefix, when non-empty, is the single host path prefix
	// under which bind-mount sources are permitted. Empty (the default)
	// forbids host bind mounts entirely — the safe posture. Anonymous
	// volumes and tmpfs are always allowed; only host-path binds are
	// gated.
	ScratchHostPrefix string
}

Policy is the operator-tunable posture for the sandbox mount. Zero values mean "no explicit limit" for the resource caps (the filter then leaves the caller's value — or the daemon default — untouched) and "deny" for the escape knobs (those are not tunable in Phase A: the whole point of the sandbox mount is that they are always off).

Loaded from FileConfig at boot; passed by value into NewService.

func (Policy) ImageAllowed

func (p Policy) ImageAllowed(ref string) bool

ImageAllowed reports whether ref passes the AllowedImages allowlist. An empty allowlist permits everything. A wildcard entry "repo/*" matches any ref whose slash-delimited prefix equals "repo/". Matching is on the raw reference string the caller supplied (tag/digest included) so an operator can pin "docker.io/library/python:3.12" exactly when desired.

type Prewarmer

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

Prewarmer keeps a configured set of container images pulled on the local podman daemon so a remote sandbox create+start doesn't pay the image-pull cost — the dominant cold-start latency for a thin client running code on this node. It re-checks on a timer so an image that podman garbage-collects gets re-pulled.

This is the Phase-A "warm" optimization: a transparent pre-warmed CONTAINER pool can't work at the raw-docker layer (the caller picks the image / cmd / env at create time), but pre-pulling the images the caller is actually allowed to run is both correct and the bulk of the speedup. The right image set is the operator's allowlist (or an explicit prewarm list).

func NewPrewarmer

func NewPrewarmer(socket string, images []string) *Prewarmer

NewPrewarmer builds a Prewarmer that talks to the libpod socket at `socket` and keeps `images` warm. The HTTP client dials the unix socket regardless of request host (the synthetic "podman" host is ignored by the daemon).

func (*Prewarmer) Ready

func (p *Prewarmer) Ready() int

Ready returns how many configured images are confirmed present locally.

func (*Prewarmer) Run

func (p *Prewarmer) Run(ctx context.Context) error

Run pulls missing images once on start, then reconciles on each tick until ctx is cancelled. Returns ctx.Err() on shutdown. A nil/empty image list makes Run block on ctx (nothing to do) so callers can wire it under the errgroup unconditionally.

func (*Prewarmer) Total

func (p *Prewarmer) Total() int

Total returns how many images the prewarmer is responsible for.

type Service

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

Service glues the security Filter and the in-flight Counter to the two outpost-side surfaces the sandbox mount feeds: the proxy-wrap middleware (filter + live load tracking) and the /_pool/capacity intercept (cloudbox-side scheduling queries). One Service per sandbox mount.

It mirrors ollama.Service so the main.go boot wiring is symmetric:

apps.SetCapabilities("sandbox", &agent.AppCapabilities{Type: sandbox.CapabilityType})
svc := sandbox.NewService(policy)
apps.SetProxyWrap("sandbox", svc.WrapProxy)
apps.AddIntercept("sandbox", "/_pool/capacity", svc.CapacityHandler())

func NewService

func NewService(policy Policy) *Service

NewService builds a Service from policy. The OCI isolation tier defaults to "runc" (shared kernel) — the Phase-A posture; a Phase-B gVisor/Kata build sets it via SetIsolation so cloudbox can route untrusted work to suitably-isolated hosts.

func (*Service) CapacityHandler

func (s *Service) CapacityHandler() http.Handler

CapacityHandler returns the http.Handler bound at /app/sandbox/_pool/capacity. It answers quickly (an atomic load + a struct encode) because cloudbox's scheduler may probe it on every routed request.

func (*Service) SetIsolation

func (s *Service) SetIsolation(tier string)

SetIsolation records the OCI runtime tier this host enforces ("runc", "gvisor", "kata"). Surfaced in CapacityReport.Isolation.

func (*Service) SetPrewarmer

func (s *Service) SetPrewarmer(p *Prewarmer)

SetPrewarmer attaches the image prewarmer so the capacity report can surface how many base images are warm. Passing nil clears it. Mirrors ollama.Service.SetWatcher — wired after construction in main.go once the podman socket is resolved.

func (*Service) Snapshot

func (s *Service) Snapshot() CapacityReport

Snapshot composes the capacity report from the counter plus the service-level isolation tier. Pool fields stay zero until the Phase-A warm pool lands.

func (*Service) WrapProxy

func (s *Service) WrapProxy(next http.Handler) http.Handler

WrapProxy is the middleware factory passed to AppRegistry.SetProxyWrap. The filter runs outermost so a denied create never increments the in-flight counter, then the counter wraps the reverse proxy.

Jump to

Keyboard shortcuts

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