sandbox

package
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package sandbox parses and validates .cloop/sandbox.yaml, a repo-committed description of the environment one project's tasks should execute in.

Why this file exists

executors.container.image is a single hub-wide reference. On a laptop that is exactly right; on a hub hosting several teams it means every project shares one toolchain, and a repo that needs a different runtime cannot be executed in isolation at all without an operator editing the hub's own config. That makes the hub a bottleneck for a decision that belongs to the repo, and it is a hard blocker for multi-tenant hosting.

So the spec lives in the repo, next to the code whose toolchain it describes, and the operator keeps control of the envelope rather than the contents.

Trust

This file is untrusted input. It arrives via `git pull`; anyone who can open a pull request can propose one, and on a hub the person who merges it is not the person whose infrastructure runs it. Every rule here follows from that:

  • the schema is closed (yaml.KnownFields) — an unknown key is a typo or a probe for a field a future version might honour, and both deserve an error rather than silence;
  • every numeric field is clamped through the same bounds pkg/config applies to the hub's own executor config, so a repo cannot request 4096 cores;
  • env carries *names*, never values — a spec can ask for a secret the project was already granted, and cannot smuggle one in;
  • mounts are workspace-relative with no ".." (see executor.SpecMount);
  • capabilities can only ever *narrow* what the executor already permits. There is no field that turns the network on. `network:` names an egress grant that must already exist for the project, and its absence turns the network off. A spec that asks for more than the deployment offers is refused at placement time, never quietly granted.

The last point is the one worth restating: this package can make a run more confined than the operator's default, and can never make it less.

Index

Constants

View Source
const (
	MaxSetupCommands = 32
	MaxSetupCmdLen   = 4096
	MaxEnvNames      = 64
)

Bounds on the collection fields. They exist for the same reason as executor.MaxSpecMounts: past these sizes the file has stopped describing an environment, and each extra entry is another line of a generated Dockerfile or another variable forwarded into a sandbox.

View Source
const FileName = ".cloop/sandbox.yaml"

FileName is the spec's path relative to a project directory.

Variables

View Source
var ErrInvalidSpec = errors.New("sandbox: invalid spec")

ErrInvalidSpec wraps every parse and validation failure, so a caller can tell "the repo's file is wrong" (the author fixes it, HTTP 400) from "the file is fine but this deployment cannot honour it" (an operator fixes it, HTTP 409) without matching on message text.

View Source
var ErrNotFound = errors.New("sandbox: no .cloop/sandbox.yaml")

ErrNotFound is returned by Load when the project has no sandbox spec. It is a sentinel rather than a nil-spec-and-nil-error because "this project has no spec" and "this project has an empty spec" mean different things: the first uses the hub's image, the second is a spec that failed to say anything.

Functions

This section is empty.

Types

type Capabilities

type Capabilities struct {
	// Git asserts the sandbox needs a working git. It becomes a placement
	// requirement, so a node that advertises its tooling and does not list git
	// is not chosen.
	Git bool `yaml:"git"`
	// Network names an egress grant (pkg/egressbroker) that must already exist
	// for this project and be active. Empty forces the workload off the network
	// entirely, whatever the executor's default is.
	Network string `yaml:"network"`
	// Virtualized demands an executor whose workloads run behind a hypervisor —
	// a Kata sandbox, locally or on a remote cluster. It becomes a placement
	// requirement, so a project that sets it can never be dispatched to an
	// executor that shares the executing machine's kernel.
	//
	// A bool is the right shape here where it was the wrong one for Network,
	// and the difference is direction. Network asks to *lift* a confinement, so
	// it must name a grant an operator issued; there is no answer to "please
	// turn the network on" that a repo-committed file is entitled to receive.
	// This asks for a boundary stronger than the executor would otherwise
	// provide, which needs nobody's permission — the worst a repo can do with
	// it is refuse to run anywhere, and it refuses loudly.
	Virtualized bool `yaml:"virtualized"`
}

Capabilities are the confinement waivers a project may request.

Every field is a request that placement must be able to satisfy honestly, which is why `network` is a grant name rather than a bool: a bool would mean "please turn the network on", and there is no answer to that a repo-committed file is entitled to receive.

type GrantChecker

type GrantChecker interface {
	// HasEgressGrant reports whether projectPath currently holds an active
	// grant named grantID.
	HasEgressGrant(projectPath, grantID string) bool
}

GrantChecker reports whether a project already holds an active egress grant by that name.

It is an interface, and a nil one denies, because of what the alternative would be. If this package imported pkg/egressbroker directly it would need a broker instance at every call site, and the call sites that could not supply one (the CLI validator, tests, `cloop sandbox check`) would end up passing a permissive stub. Denying by default means the awkward path is the safe one.

type GrantDeniedError

type GrantDeniedError struct {
	ProjectPath string
	GrantID     string
}

GrantDeniedError reports a sandbox spec asking for an egress grant the project does not hold.

It is typed for the same reason executor.HostExecutionDeniedError is: the developer who wrote the YAML and the operator who can issue the grant are different people, so the error has to carry enough for the first to file a useful request to the second.

func (*GrantDeniedError) Error

func (e *GrantDeniedError) Error() string

Error implements error.

func (*GrantDeniedError) Remediation

func (e *GrantDeniedError) Remediation() string

Remediation returns the "what to do about it" half, for UI surfaces that render cause and fix separately.

type Mount

type Mount struct {
	Source   string `yaml:"source"`
	Target   string `yaml:"target"`
	ReadOnly bool   `yaml:"read_only"`
}

Mount is the YAML shape of executor.SpecMount.

type Resolved

type Resolved struct {
	// Spec is the validated spec, or nil when the project has none.
	Spec *Spec
	// Hash identifies the spec's content. It is recorded in the task artifact
	// so a run can be tied back to the exact file that shaped it, including
	// after the file changes.
	Hash string
	// Warnings are the clamps applied while parsing.
	Warnings []string
	// Source is the path the spec was read from, for error messages.
	Source string
}

Resolved is a spec together with everything derived from it that a caller needs to keep: its identity, and the warnings raised while normalizing it.

func Resolve

func Resolve(projectDir string) (*Resolved, error)

Resolve loads the spec for a project, if it has one.

A missing file is not an error: it returns a Resolved whose Present() is false, so callers need no special case for the overwhelmingly common project that never writes one.

func (*Resolved) ApplyTo

func (r *Resolved) ApplyTo(spec *executor.Spec, projectPath string, grants GrantChecker) error

ApplyTo writes the spec onto an executor.Spec.

projectPath and grants are used only for the egress capability: a spec naming a grant the project does not hold is refused, and a spec naming no grant at all takes the network away. Those are the only two outcomes — there is no third in which the workload ends up with more network than the executor was configured to give it.

Callers must still run executor.CheckSandboxSupport against the bound executor; ApplyTo shapes the request, it does not decide whether the request can be honoured.

func (*Resolved) CheckImagePolicy

func (r *Resolved) CheckImagePolicy(p imagepolicy.Policy) (imagepolicy.Decision, error)

CheckImagePolicy evaluates the spec's image against the hub's trust policy.

This is the *validation* surface, not the enforcement one. Enforcement lives in the executors, where the reference is about to become a runtime argument and can additionally be pinned and signature-checked. This runs earlier, from the same place that parses the file, so a project whose image is refused learns it as a config error naming the rule — before a run starts, before a container is created, and in the same response as every other thing wrong with its sandbox.yaml.

The returned Decision is populated whether or not the image was allowed, so the UI can render "this would be refused, because X" without an error path. A spec with no image, or an unconfigured policy, yields a zero Decision and a nil error.

func (*Resolved) FilterEnv

func (r *Resolved) FilterEnv(env []string) []string

FilterEnv returns the subset of env (K=V entries) whose names the spec asked for, plus every entry the spec did not mention that the caller marked mandatory.

The direction matters: this filters an environment the caller already assembled from the project's grants. It cannot introduce a variable, only remove ones the spec did not ask for. A name in `env:` that the project holds no grant for therefore forwards nothing, silently and correctly — the spec expressed a wish, not an entitlement.

A spec with no `env:` at all is not an empty allowlist; it is no opinion, and the caller's environment passes through untouched. Reading an absent key as "forward nothing" would break every project that adds a sandbox.yaml purely to pin an image, by stripping the API key its harness needs.

func (*Resolved) Present

func (r *Resolved) Present() bool

Present reports whether a spec was found and asks for anything.

func (*Resolved) Requirements

func (r *Resolved) Requirements() executor.Requirements

Requirements returns the placement constraints this spec implies.

Every entry is a capability the executor must genuinely have. Nothing here is advisory: a requirement that placement cannot satisfy produces a refusal naming the constraint, which is the whole point — a sandbox spec that is quietly ignored is worse than one that is rejected, because the task then runs in an environment nobody described.

type Resources

type Resources struct {
	// CPU is a core allowance (1.5 = one and a half cores); 0 = executor default.
	CPU float64 `yaml:"cpu"`
	// Memory is a size string ("512m", "2g"); empty = executor default.
	Memory string `yaml:"memory"`
	// PIDs caps processes/threads; 0 = executor default.
	PIDs int `yaml:"pids"`
	// Disk is a size string bounding the workspace and scratch space; empty =
	// executor default.
	//
	// It is the one resource key that also bounds something the project does
	// not control: the size of the tree an executor fetches for it. A git
	// workspace is provisioned from a remote repository whose contents the
	// executor learns only by downloading them, so the limit reaches both the
	// volume's own sizeLimit and the provisioner's post-fetch check — see
	// Resolved.ApplyTo.
	Disk string `yaml:"disk"`
}

Resources is the per-task resource request.

type Spec

type Spec struct {
	// Image is the container image the project's tasks run in. Empty means
	// the executor's configured image.
	Image string `yaml:"image"`
	// Setup are shell commands baked into a derived image once per unique
	// (image, setup) pair — not re-run per task. Requires an executor that
	// can build (see executor.Capabilities.SupportsSandboxBuild).
	Setup []string `yaml:"setup"`
	// Env is an allowlist of environment variable *names* to forward into the
	// sandbox. Values are never written here; they come from the project's
	// existing secret grants, and a name the project was not granted forwards
	// nothing.
	Env []string `yaml:"env"`
	// Resources bounds one task's share of the executor.
	Resources Resources `yaml:"resources"`
	// Capabilities are the confinement waivers the project asks for.
	Capabilities Capabilities `yaml:"capabilities"`
	// Mounts re-expose sub-paths of the workspace elsewhere in the sandbox.
	Mounts []Mount `yaml:"mounts"`
}

Spec is the parsed sandbox description.

Field names match the YAML keys exactly. The struct is the schema: with KnownFields set on the decoder there is nowhere else a valid key can be declared, so reading this type is reading the file format.

func Load

func Load(projectDir string) (*Spec, []string, error)

Load reads and validates the spec for a project.

It returns ErrNotFound when the project has none, which every caller treats as "use the executor's defaults" rather than as a failure. Any other error means a spec exists and is wrong, and that must not degrade to defaults: a project that asked for a rust toolchain and got the hub's generic image would fail in a way that looks like its own bug.

func Parse

func Parse(data []byte) (*Spec, []string, error)

Parse decodes and validates a spec from YAML.

The returned warnings describe values that were clamped rather than rejected. Clamping is the right answer for a number that is merely out of range (the author wanted "lots of memory"; giving them the ceiling honours that) and the wrong answer for a value that is malformed or dangerous, which is an error. Callers surface warnings; they never have to act on them.

func (*Spec) Hash

func (s *Spec) Hash() string

Hash returns a stable content hash of the spec.

It hashes a canonical rendering of the *normalized* fields rather than the raw file bytes, so reformatting the YAML or reordering keys does not change the identity of the environment it describes — which matters because the hash is also the cache key for the derived sandbox image. Two files that mean the same thing must not build two images.

func (*Spec) IsZero

func (s *Spec) IsZero() bool

IsZero reports whether the spec asks for nothing at all, in which case applying it is a no-op and the executor's defaults stand.

func (*Spec) SetupHash

func (s *Spec) SetupHash() string

SetupHash identifies just the image-build inputs, which is what the derived image is keyed by. It is separate from Hash because changing `env:` or `resources:` must not invalidate a built image — those are applied per run.

func (*Spec) SpecMounts

func (s *Spec) SpecMounts() []executor.SpecMount

SpecMounts converts the YAML mounts to the executor's mount type.

Jump to

Keyboard shortcuts

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