manifest

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package manifest parses and validates devbay.yaml.

This package is the airlock. Everything upstream of it — an introspection agent reading a repository, a human editing YAML, a pull request — is untrusted. Everything downstream of it interprets a Manifest as instructions while holding credentials. A manifest that does not pass Validate must never reach the execution plane.

The rules the validator enforces are numbered R1-R7 and documented on the constructs they govern in spec/devbay.schema.json. The load-bearing one is R1: commands are argv arrays, never shell strings, and are executed with execve rather than sh -c. That single constraint is what makes it safe to have a language model author this file, because there is no path from a manifest to arbitrary shell.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Argv

type Argv []string

Argv is a command as an argument vector.

R1. Declared as a slice so that a YAML string fails to decode structurally rather than being coerced — the type system does the first half of the enforcement and Validate does the rest.

type Build

type Build struct {
	Context    string `yaml:"context,omitempty"`
	Dockerfile string `yaml:"dockerfile,omitempty"`
	Target     string `yaml:"target,omitempty"`
}

Build builds an image from the repo instead of pulling one.

func (*Build) UnmarshalYAML

func (b *Build) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML accepts `build: ./dir` as well as the full mapping.

The shorthand is what Compose uses and therefore what people write, what every example they have seen uses, and what devbay's own `init` suggests when it finds a service that builds from source. Rejecting it produced the worst kind of error: a type-mismatch message that blamed R1, the rule about commands being argv arrays, for a field that has nothing to do with commands.

Accepting a scalar here does not weaken anything. Unlike a command, a context path is not executed, and it is confined to the worktree before use.

type Diagnostic

type Diagnostic struct {
	Severity Severity
	Rule     string // "R1".."R7", or "" for structural findings
	Path     string // e.g. services/api/env/DATABASE_URL
	Msg      string
	Argv     Argv // populated for Approval findings, shown verbatim to the human
}

Diagnostic is one finding, located in the manifest.

func (Diagnostic) Location

func (d Diagnostic) Location() string

Location returns the manifest path a diagnostic refers to, with a rule prefix when one applies. Used by the CLI; Diagnostic.String is for logs.

func (Diagnostic) String

func (d Diagnostic) String() string

type External

type External struct {
	Emulate string `yaml:"emulate"`
	Real    string `yaml:"real,omitempty"` // "never" or "gated"; there is no "always"
	Mint    *Mint  `yaml:"mint,omitempty"`
}

External is a third-party dependency and how it is satisfied locally.

The default is always the emulator. This is the highest-leverage security decision in the format: most bays then need zero real credentials, and the ones that do are a short list a human actually looked at.

type Fork

type Fork string

Fork is how per-bay data isolation is achieved for a stateful service.

const (
	// ForkImage bakes the seeded state into a per-project image; each bay runs
	// its own container and the writable layer provides copy-on-write. This is
	// the default because it is instant, parallelisable, and makes a leaked
	// fork structurally impossible — teardown is a container removal.
	ForkImage Fork = "image"
	// ForkTemplate is Postgres CREATE DATABASE ... TEMPLATE. Note this is a
	// full O(size) physical copy, not copy-on-write, and no other session may
	// be connected to the template while it runs, so forks serialize.
	ForkTemplate Fork = "template"
	ForkPrefix   Fork = "prefix" // key namespace prefix, e.g. Redis
	ForkSchema   Fork = "schema" // separate schema in one database
	ForkNone     Fork = "none"   // genuinely shared; warned about
)

type Health

type Health struct {
	HTTP string `yaml:"http,omitempty"` // path probed on the primary port
	TCP  int    `yaml:"tcp,omitempty"`  // port that must accept a connection
	Cmd  Argv   `yaml:"cmd,omitempty"`  // exec in container; healthy on exit 0

	// Log is an RE2 regex matched against stdout and stderr. Preferred for
	// processes with no port, and often better than an HTTP probe for dev
	// servers, which print an explicit ready line: Vite emits "ready in 412
	// ms", Sidekiq "Starting processing, hit Ctrl-C to stop", Celery
	// "celery@host ready.". Matching that is real readiness.
	Log string `yaml:"log,omitempty"`

	// Process is liveness only: healthy while the main process runs. The
	// weakest probe available, and Validate warns on it. It exists because the
	// alternative — inventing a fake HTTP endpoint for a Sidekiq or Celery
	// worker — is a lie that silently defeats the verification loop.
	Process bool `yaml:"process,omitempty"`

	Timeout     string `yaml:"timeout,omitempty"`
	Interval    string `yaml:"interval,omitempty"`
	StartPeriod string `yaml:"start_period,omitempty"`
}

Health is a readiness probe. Exactly one of the five forms must be set.

R5. Without a health probe there is no verification loop, and without a verification loop auto-detection is only guessing.

Probes run from the host against 127.0.0.1:<allocated port>, never against a *.localhost hostname — the daemon's own resolver cannot resolve those, because macOS does not honour RFC 6761 for subdomains of localhost and only Chrome, Firefox and curl special-case them.

type Kind

type Kind string

Kind distinguishes a long-running service from a one-shot step.

const (
	// KindService is a long-running process. Requires a health probe (R5).
	KindService Kind = "service"
	// KindOneshot runs to completion and is healthy on exit code 0.
	//
	// Oneshot exists because every real repository has ordering-sensitive
	// setup — migrate, then seed, then start the app — and a flat list of
	// setup commands gets the ordering wrong: migrations must finish before
	// the app starts, not after every service is healthy. Modelling each step
	// as a oneshot means `needs` alone expresses the ordering, and there is
	// one dependency mechanism in the manifest rather than two.
	KindOneshot Kind = "oneshot"
)

type Manifest

type Manifest struct {
	Version     int                  `yaml:"version"`
	Project     string               `yaml:"project"`
	Services    map[string]*Service  `yaml:"services"`
	Tasks       map[string]*Task     `yaml:"tasks"`
	Externals   map[string]*External `yaml:"externals,omitempty"`
	Supervision *Supervision         `yaml:"supervision,omitempty"`

	// Path is the file this manifest was loaded from. Not part of the format.
	Path string `yaml:"-"`
}

Manifest is a parsed devbay.yaml.

func Load

func Load(path string) (*Manifest, error)

Load reads and decodes a devbay.yaml. It does not validate; call Validate.

Decoding is strict: an unknown key is an error rather than being silently ignored, because a typo in a security-relevant key — egress, install_scripts — must never be read as "the author didn't set it".

func Parse

func Parse(b []byte) (*Manifest, error)

Parse decodes a manifest from YAML bytes.

func (*Manifest) PrimaryService

func (m *Manifest) PrimaryService() string

PrimaryService returns the name of the service that claims the bare <bay>.<project> hostname. It is inferred when exactly one long-running service exposes a port, and declared otherwise. Validate guarantees one exists, so this does not report an error.

type Mint

type Mint struct {
	Provider string         `yaml:"provider,omitempty"` // aws-sts, github-app, gcp-sa
	TTL      string         `yaml:"ttl,omitempty"`
	Scope    map[string]any `yaml:"scope,omitempty"`
}

Mint describes how to issue a short-lived scoped credential instead of passing a long-lived one through.

type Mount

type Mount struct {
	// Source is relative to the repository root and confined to it.
	Source string `yaml:"source"`
	// Target is an absolute path inside the container.
	Target string `yaml:"target"`
}

Mount is a directory from the repository, bound into the container.

type Report

type Report struct {
	Format ReportFormat `yaml:"format"`
	Path   string       `yaml:"path,omitempty"`
}

Report says where a runner writes results. Path is empty for formats that stream to stdout rather than writing a file, notably go-json.

type ReportFormat

type ReportFormat string

ReportFormat is the machine-readable shape a test runner emits.

There is no cross-language standard for agent-consumable test results. JUnit XML is the universal fallback; native streaming JSON is preferred where it exists because it gives live progress a report file cannot.

const (
	ReportJUnit  ReportFormat = "junit"   // pytest, phpunit, vitest natively
	ReportJSON   ReportFormat = "json"    // jest --json, vitest --reporter=json
	ReportGoJSON ReportFormat = "go-json" // go test -json, a streaming event log
	ReportTAP    ReportFormat = "tap"
)

type Result

type Result struct {
	Diagnostics []Diagnostic
}

Result is the outcome of validating a manifest.

func Validate

func Validate(m *Manifest) *Result

Validate applies rules R1-R7 and the semantic checks a schema cannot express.

This function is the airlock. A manifest that does not pass it must never be interpreted by anything holding credentials.

func (*Result) Approvals

func (r *Result) Approvals() []Diagnostic

Approvals returns the commands needing one-time human approval.

func (*Result) Err

func (r *Result) Err() error

Err returns a single error summarising the rejections, or nil.

func (*Result) Errors

func (r *Result) Errors() []Diagnostic

Errors returns only the findings that reject the manifest.

func (*Result) OK

func (r *Result) OK() bool

OK reports whether the manifest may cross into the execution plane. Approvals do not make a manifest invalid; they gate execution of one command.

func (*Result) Warnings

func (r *Result) Warnings() []Diagnostic

Warnings returns the advisory findings.

type Scope

type Scope string

Scope controls how many instances of a service exist.

const (
	ScopeBay    Scope = "bay"    // one per bay; the normal case, including datastores
	ScopeShared Scope = "shared" // one total, forked per bay
)

type Seed

type Seed struct {
	After   []string `yaml:"after"`
	Sources []string `yaml:"sources"`
}

Seed defines the state captured into a service's image for ForkImage.

Seeding names oneshot services rather than carrying commands, because a migration does not run inside the database container: it runs inside the application container, against the database, using the application's own toolchain. A command here would have no unambiguous place to execute.

type Service

type Service struct {
	Kind  Kind   `yaml:"kind,omitempty"`
	Image string `yaml:"image,omitempty"`
	Build *Build `yaml:"build,omitempty"`

	Scope Scope `yaml:"scope,omitempty"`
	Fork  Fork  `yaml:"fork,omitempty"`
	Seed  *Seed `yaml:"seed,omitempty"`

	Workdir string `yaml:"workdir,omitempty"`

	Install Argv `yaml:"install,omitempty"`
	// InstallScripts permits package lifecycle scripts during Install.
	//
	// False by default: devbay appends --ignore-scripts or the ecosystem
	// equivalent. Running install scripts on a freshly cloned untrusted
	// repository is the delivery mechanism used by the self-replicating
	// Shai-Hulud npm worm, which steals cloud credentials. Setting this true
	// is approval-gated.
	InstallScripts bool `yaml:"install_scripts,omitempty"`

	Start Argv `yaml:"start,omitempty"` // long-running services
	Run   Argv `yaml:"run,omitempty"`   // oneshots

	// Port is the primary port: the one that gets a hostname and, unless
	// overridden, the one an http probe targets. Exactly one exists per
	// service so hostname routing is unambiguous.
	Port int `yaml:"port,omitempty"`
	// Ports are additional named ports. Real services routinely expose more
	// than one — a mail catcher listens on SMTP and serves a web UI, an object
	// store serves an API and a console.
	Ports map[string]int `yaml:"ports,omitempty"`

	Primary bool     `yaml:"primary,omitempty"`
	Needs   []string `yaml:"needs,omitempty"`

	Health *Health `yaml:"health,omitempty"`

	// Watch globs are evaluated by the daemon on the host using native
	// FSEvents or inotify, never by a watcher inside the container: virtiofs
	// does not implement inotify, so host edits do not reliably produce
	// events in a container, and polling costs real CPU per watcher per bay.
	Watch       []string    `yaml:"watch,omitempty"`
	WatchAction WatchAction `yaml:"watch_action,omitempty"`

	// Mounts bind a directory from the repository over a path in the
	// container.
	//
	// Needed by the common `target: dev` pattern, where an image builds the
	// source in and the source is then bound back over the top so edits are
	// live. Declared rather than inferred: devbay tried inferring it from the
	// build context and got it right for interpreted images and wrong for
	// compiled ones, where the same directory holds build output and mounting
	// source over it hides the binary.
	Mounts []Mount `yaml:"mounts,omitempty"`

	// Volumes are paths backed by a named volume rather than the bind mount —
	// node_modules, .venv, vendor/bundle, target, .next. Not an optimisation
	// to defer: a bind-mounted dependency tree runs at roughly 2.5x native on
	// macOS and a named volume recovers most of that.
	Volumes []string `yaml:"volumes,omitempty"`

	// Egress is the outbound allowlist. Absent or empty means no outbound
	// network at all.
	//
	// R4. This field is never authorable by the introspection agent: the
	// validator strips it from model-produced manifests. If a model could
	// write the allowlist, a prompt injection would append its own
	// destination and the sandbox would defeat itself.
	Egress []string `yaml:"egress,omitempty"`

	Env map[string]string `yaml:"env,omitempty"`
}

Service is a container in a bay.

func (*Service) Command

func (s *Service) Command() Argv

Command returns the argv that launches s, whichever field holds it.

func (*Service) IsOneshot

func (s *Service) IsOneshot() bool

IsOneshot reports whether s runs to completion rather than staying up.

type Severity

type Severity int

Severity distinguishes a manifest that must be rejected from one that is merely questionable.

const (
	// Error means the manifest does not reach the execution plane.
	Error Severity = iota
	// Warn means it does, but a human should see the message. Two things
	// produce warnings rather than errors: an argv[0] outside the allowlist,
	// which is permitted subject to approval, and a liveness-only health
	// probe, which is weak but honest.
	Warn
	// Approval means execution is blocked until a human approves the exact
	// argv. This is the R2 escape hatch: rejecting outright would make people
	// fork the project, and permitting a shell string would make R1 theatre.
	Approval
)

func (Severity) String

func (s Severity) String() string

type Supervision

type Supervision struct {
	Banner      *bool `yaml:"banner,omitempty"`
	FaviconTint *bool `yaml:"favicon_tint,omitempty"`
}

Supervision controls the per-bay identity surface injected by the proxy.

type Task

type Task struct {
	Run Argv `yaml:"run"`

	// Needs is the service subgraph this task requires. An empty slice is
	// valid and common: a unit suite that boots zero containers is the
	// fastest path to a verified result.
	//
	// R6. Omitting the key entirely is an error rather than a default,
	// because forcing the author to think about it is the point.
	//
	// The distinction is carried by nil versus empty: YAML decodes `needs: []`
	// to an empty non-nil slice and an omitted key to nil, so no side channel
	// is needed -- which also means a Task built in Go rather than parsed from
	// a file can satisfy the rule, by writing []string{}.
	Needs []string `yaml:"needs"`

	In      string            `yaml:"in,omitempty"`
	Report  *Report           `yaml:"report,omitempty"`
	Env     map[string]string `yaml:"env,omitempty"`
	Timeout string            `yaml:"timeout,omitempty"`
	// contains filtered or unexported fields
}

Task is a named finite command an agent or human runs against a bay.

type WatchAction

type WatchAction string

WatchAction is what the daemon does when a watched path changes.

const (
	WatchRestart WatchAction = "restart"
	WatchSync    WatchAction = "sync"
	WatchRebuild WatchAction = "rebuild"
)

Jump to

Keyboard shortcuts

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