config

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package config parses and resolves a Concourse-style pipeline YAML file (resource_types/resources/jobs) and the config-merge logic (task and agent-invocation resolution) that both plan-time hashing and run-time execution share.

Index

Constants

View Source
const (
	// AssertOutcomeSucceeded requires the plan to have produced no error. It
	// is not a no-op: it opts a job OUT of the rule that a matching
	// execution: clears whatever the plan produced.
	AssertOutcomeSucceeded = "succeeded"
	// AssertOutcomeFailed requires the plan to have produced an error, and
	// then clears it — the assertion is what makes the job green.
	AssertOutcomeFailed = "failed"
)

The two legal values of a job's assert.outcome.

View Source
const (
	// DecideMajority takes the verdict more than half the voters chose.
	DecideMajority = "majority"
	// DecideUnanimous requires every voter to agree.
	DecideUnanimous = "unanimous"
	// DecideAny takes the first verdict in verdicts: that anyone chose,
	// which is the "one objection is enough" shape when the verdicts are
	// ordered from most to least severe.
	DecideAny = "any"
)

The decision rules an ensemble can use. Anything else in decide: is read as the name of an agent that judges.

View Source
const (
	// MemberErrorsFail fails the whole ensemble. The default: a missing vote
	// changes what the remaining votes mean, and pretending otherwise is how
	// a two-agent ensemble silently becomes a one-agent one.
	MemberErrorsFail = "fail"
	// MemberErrorsExclude decides among the members that did vote.
	MemberErrorsExclude = "exclude"
)

How an ensemble treats a member that failed rather than voted.

View Source
const (
	GitHubPRsType       = "github-prs"
	GitHubCommentsType  = "github-comments"
	GitHubPRCommentType = "github-pr-comment"
	GitHubPRReviewType  = "github-pr-review"
	GitHubReactionType  = "github-reaction"
)

The built-in GitHub type names.

View Source
const (
	GitHubCommentConversation = "conversation"
	GitHubCommentReview       = "review"
	GitHubCommentIssue        = "issue"
)

The comment kinds github-comments can watch, as GitHub files them.

View Source
const (
	RememberBuiltinName = "remember"
	ForgetBuiltinName   = "forget"
)

RememberBuiltinName and ForgetBuiltinName are the builtins that write a step's memory. Named as constants for the reason AskUserBuiltinName is: config validates where they may be granted and internal/agent implements them, and the two must agree on the spelling.

Both are absent from DefaultAgentToolSpecs. What a bot keeps about a person is decided by whoever holds these, and that is a choice the pipeline makes in writing.

View Source
const (
	ReadFileBuiltinName    = "read_file"
	ListDirBuiltinName     = "list_dir"
	SearchFilesBuiltinName = "search_files"
	WriteFileBuiltinName   = "write_file"
	EditFileBuiltinName    = "edit_file"
	RunShellBuiltinName    = "run_shell"
)

The builtin tools that more than one package names.

View Source
const AskUserBuiltinName = "ask_user"

AskUserBuiltinName is the builtin that lets an agent say it does not know something and get an answer. Named as a constant for the reason WebFetchBuiltinName is: config validates the grant's shape, internal/agent binds the dials to the implementation, and the two must agree on the spelling.

It is deliberately absent from DefaultAgentToolSpecs. Interrupting a person is a capability a pipeline hands over explicitly, exactly like the ability to write a file.

View Source
const CLISettingsProject = "project"

CLISettingsProject is the only accepted value of Agent.Settings: the CLI subprocess loads the repo's checked-in project scope (.claude/ settings, CLAUDE.md, hooks). Absent, it loads no settings at all.

View Source
const CLISourcePrefix = "@"

CLISourcePrefix marks a model string as naming a CLI rather than a hosted provider. Chosen because no provider prefix can contain it, so the two namespaces cannot collide as either table grows.

View Source
const CronType = "cron"

CronType is the built-in type whose versions are moments a crontab expression names; it is Go rather than YAML because there is nothing to run, only a clock to read.

View Source
const DefaultAgentStepTimeout = 30 * time.Minute

DefaultAgentStepTimeout is the wall-clock ceiling an agent step gets when it declares no timeout: of its own (see docs/attempts-timeout.md, "Agent steps: the one implicit deadline"). Kept here rather than in internal/agent so internal/web can resolve the same effective deadline a running step is actually held to — depguard forbids internal/web from importing internal/agent.

View Source
const DefaultCacheMaxEntries = 50

DefaultCacheMaxEntries bounds the resource cache when a pipeline enables it without saying how big. Sized as a judgment call — big enough that a pipeline with a handful of resources never evicts anything it still wants, small enough that an abandoned pipeline's cache is bounded.

View Source
const DefaultDelegateBudgetPercent = 10

DefaultDelegateBudgetPercent is how much of its remaining allowance an agent hands to one sub-agent call when nothing says otherwise.

A fraction of what REMAINS, so delegation cannot drain a parent outright: successive calls take a tenth of a shrinking number and the parent always keeps something to finish its own work with. Ten percent leaves room for roughly a dozen helpers before the slices get small enough to be worth noticing, which is far past what any sensible plan asks for.

View Source
const DefaultGitHubEndpoint = "https://api.github.com"

DefaultGitHubEndpoint is the REST API of github.com. A GitHub Enterprise Server's is https://<host>/api/v3.

View Source
const DefaultGitHubTokenEnv = "GH_TOKEN"

DefaultGitHubTokenEnv is the variable a github-* resource reads its token from when source.token_env does not name another — the one gh itself reads first, so `export GH_TOKEN=$(gh auth token)` is the whole setup.

View Source
const DefaultMaxContextBytes = 100_000

DefaultMaxContextBytes is how much of one context_paths: file reaches the model when an agent sets no max_context_bytes:.

100,000 is what priming used before the knob existed — read_file's own tool budget, inherited because a context file is delivered as a synthetic read_file result. Keeping it as the default is what makes this field free to add: every pipeline that does not set it is handed exactly what it was handed before.

View Source
const DefaultMemoryEntries = 100

DefaultMemoryEntries is how many entries one scope keeps when the pipeline does not say, the oldest going first.

View Source
const DefaultRunHistory = 100

DefaultRunHistory is how many runs of each job steps keeps when neither the pipeline nor the command line says.

Smaller than DefaultVersionHistory by an order of magnitude, and deliberately: a version is a few dozen bytes and a run is tens of kilobytes, so the same number would make these two caps mean very different things. A hundred runs is far more than anyone scrolls back through while keeping a busy daemon's database in the low tens of megabytes.

View Source
const DefaultVersionHistory = 1000

DefaultVersionHistory is how many versions of each resource steps remembers when neither the pipeline nor the command line says.

Deliberately the same order as the consumed-version bound next to it: far past any check window anyone polls, while keeping a busy resource from growing the table forever.

View Source
const MCPToolNameSep = "__"

MCPToolNameSep separates the server and tool names in a single-tool MCP grant's reference name (ToolSpecName). Deliberately "__", not ".": the result is sent to the model as a function name, and OpenAI's (and most OpenAI-compatible providers') function-calling API restricts that to [a-zA-Z0-9_-] — a dot would be rejected outright.

View Source
const MaxAcrossCells = 1000

MaxAcrossCells bounds the PRODUCT of a matrix's axes, which MaxAcrossItems alone does not: two model-written arrays each under the per-axis cap multiply to a million cells. Checked before any combination is built.

View Source
const MaxAcrossItems = 1000

MaxAcrossItems bounds how many items one from_file: axis may expand to.

The array is produced during the run, often by a model, so its length is not something the pipeline author reviewed. Each item becomes a cell with its own hash, workspace and possibly its own model call, so an unbounded array turns a typo upstream into an unbounded bill. Refusing with a message that says what to do beats discovering it halfway through.

View Source
const PinLifetime = 15 * time.Minute

PinLifetime is how long an agent stays on a fallback with nothing re-affirming that its primary is still unwell. internal/agent owns the behavior and documents the reasoning at length; the value lives here so validatePreflight can hold defaults.preflight.cache: below it, which is a load-time question about a setting rather than a runtime one.

View Source
const RouteTargetNext = "next"

RouteTargetNext is the reserved to: TARGET — the value side, where every other entry is a step name — meaning "continue in declaration order".

It exists because a verdict must name a target, and a container has none: stepName returns "" for in_parallel:, race:, across:, ensemble: and approval:. An author whose next step was one of those had to route PAST it to some later leaf, which does not skip a formality — routing past an approval: skips the human gate the pipeline exists to have — or insert a leaf step purely to give the verdict somewhere safe to land.

Positional rather than named, so it needs no naming surface on block steps, and it says the thing an author wants to say independently of containers: this outcome is not a jump. Always forward, so it never requires max_visits:.

The cost is one reserved word on the value side. A step named "next" inside a to:-using segment is refused at load rather than silently shadowed — otherwise "next" would mean two things in the one place they collide.

View Source
const SubAgentRequestParam = "request"

SubAgentRequestParam is the one argument a sub-agent tool takes.

View Source
const UnlimitedInFlight = 1_000_000

UnlimitedInFlight is what an unset max_in_flight: is stored as.

A large number rather than NULL or 0, because Store.ClaimNextJob has to read this in one atomic statement and needs a value it can compare against. That leaves a missing row — a job removed from the pipeline between enqueue and claim — free to default to 1, which is the conservative answer, instead of being indistinguishable from "unlimited".

Unlimited is bounded in practice anyway: `steps web --max-concurrent` caps how many builds run at once across the whole pipeline.

View Source
const UpstreamDir = "upstream"

UpstreamDir is the directory a TASK reader's demanded outcomes are materialized into, one file per sender named for the step. A shell command cannot be handed a synthetic tool result the way an agent can, so the filesystem is the interface it gets instead.

Reserved as an artifact name (see ValidateArtifactName): an artifact so called would materialize over the delivered outcomes.

View Source
const WebFetchBuiltinName = "web_fetch"

WebFetchBuiltinName is the one builtin whose grant may carry an allow: list. Named as a constant because config and internal/agent must agree on the spelling: config validates the shape here, agent binds the list to the tool's implementation.

View Source
const WebhookCustom = "custom"

WebhookCustom is the provider whose scheme is the resource's own signature:.

View Source
const WebhookType = "webhook"

WebhookType is the built-in type whose versions are deliveries; it is Go rather than YAML because it has no command to run. CronType (cron.go) is the other.

Variables

This section is empty.

Functions

func AssertFilesMismatch

func AssertFilesMismatch(files []string, dir string) error

AssertFilesMismatch reports the first assert.files entry that is not a non-empty regular file under dir, or nil when every entry checks out.

dir is the step's own working directory, read before its outputs are captured — internal/pipeline calls it for a task step and internal/agent for an agent step, which is why it lives here: this package is the one they share, and it already owns the paired ValidateArtifactPath that made the paths safe to join in the first place.

func AssertFilesMismatches

func AssertFilesMismatches(files []string, dir string) []string

AssertFilesMismatches reports EVERY unmet entry rather than stopping at the first, each phrased as the reason alone ("x does not exist") so a caller can join them into one message.

The plural form exists for the agent path, which does not merely report the mismatch but hands it back to the model: told one missing file at a time, a model writes one file, tries to stop, and is told about the next — paying a turn per artifact to learn something one sentence could have said.

func AttemptCount

func AttemptCount(attempts *int) int

AttemptCount is how many times a step whose attempts: is set to n should try its work — 1 when the field was omitted. Exported because internal/pipeline resolves the same field for get/task/put steps, whose default is a single attempt rather than the agent's three.

func CLIBinary

func CLIBinary(cli string) string

CLIBinary reports the executable backing a CLI source, or "" for an unrecognized one. Exported for internal/agent (spawning, preflight) and for this package's own PATH check.

func CLIProviderNames

func CLIProviderNames() []string

CLIProviderNames lists every recognized CLI key; only this package's tests walk it.

func CachePaths added in v0.2.0

func CachePaths(caches []TaskCache) []string

CachePaths are caches' paths, cleaned, the form every consumer keys and mounts by.

func CollectedArtifacts

func CollectedArtifacts(step Step) []string

CollectedArtifacts returns the store-level artifact names a collecting matrix's cells capture into: the collected outputs, renamed through any author-written output_mapping — the same rename CollectedOutputMapping composes the cell coordinates onto.

func CollectedOutputMapping

func CollectedOutputMapping(outputs []string, mapping map[string]string, subdir string) map[string]string

CollectedOutputMapping is the output mapping a collecting matrix's cell captures through: each declared output, redirected under the cell's own coordinates — findings -> findings/alpha/fast. Composed AFTER any author-written output_mapping (mapping renames the artifact; the coordinates say where inside it this cell lands), and nil when the cell is not part of a collecting matrix, so every other step passes through untouched.

One function, called by both internal/pipeline (task cells) and internal/agent (agent cells), so the two kinds of cell can never disagree about where a capture lands.

func CollectedOutputs

func CollectedOutputs(step Step) []string

CollectedOutputs returns the output names an across: step collects — the outputs: of the step its try: chain wraps, which is where a cell template keeps its fields. Empty when the matrix does not collect. Exported for internal/pipeline, which resets the collected artifacts before the block's cells capture (see CollectedArtifacts).

func ConfinedPath

func ConfinedPath(dir, rel string) (string, bool)

ConfinedPath joins rel onto dir and reports whether the cleaned result is dir itself or under it. An absolute rel is taken as it is, so it is inside only when it already names a place under an absolute dir.

It is the one lexical confinement predicate: internal/agent's file tools and context_paths: ask it at run time, and load asks it of a workspace-relative path ("." for the step root), which is why it lives here rather than beside the tools: this package imports nothing internal, so the run can call load's answer but not the other way round. Lexical only; symlinks are the caller's.

POSIX path rather than filepath: pipeline paths are written with "/", a container's tree is POSIX whatever runs steps, and steps ships for linux and darwin, where the two agree.

func GitHubFinds

func GitHubFinds(kind string) bool

GitHubFinds reports whether kind is one of the two types a get reads.

func GitHubTypes

func GitHubTypes() []string

GitHubTypes is every github-* type, in the order the docs list them.

func GrantsAskUser

func GrantsAskUser(specs []ToolSpec) bool

GrantsAskUser reports whether a resolved tool grant includes ask_user.

Here rather than in each caller because two packages that cannot see each other ask it — internal/agent, to size a CLI child's tool-call deadline, and internal/merkle, to decide whether max_questions: belongs in the cache key. Two copies would let the second silently disagree with the first, and the merkle one decides whether a step skips work it never did.

func GrantsMemory

func GrantsMemory(specs []ToolSpec) bool

GrantsMemory reports whether a resolved tool grant writes memory.

func HasRuntimeAxis

func HasRuntimeAxis(step Step) bool

HasRuntimeAxis reports whether any axis of step's matrix is file-valued, which is what makes the whole matrix un-expandable at load time.

func ImageReference

func ImageReference(source, version map[string]any) (string, error)

ImageReference builds the image an artifact names: registry-image's convention, the source's repository at the version's digest.

The repository comes ONLY from source. The version is whatever a check, webhook or pin said, and letting it name the repository would let any of those redirect a step to any image anywhere; the digest merely pins content inside the repository the pipeline's author chose. Errors never quote source, which routinely carries a registry password.

ponytail: one convention for every resource type. A resource_type-level reference template is the upgrade path if a second shape appears.

func InferToolParams

func InferToolParams(run string) []string

InferToolParams is each distinct NAME a custom tool's run template reads as {{ .args.NAME }}, in first-seen order.

func InterpolateVars

func InterpolateVars(source []byte, vars map[string]string) []byte

InterpolateVars substitutes ((name)) throughout a pipeline's YAML source, before it is parsed.

Textual, not structural, and deliberately so: a var may appear anywhere a value does — inside a URI, in the middle of a command, as a whole mapping value — and a structural pass would have to enumerate every field that might contain one, which is the same list that goes stale every time a field is added.

Unknown references are LEFT IN PLACE rather than blanked. They are either a load_var: value that does not exist yet or a mistake, and blanking them would turn both into a silently empty string in a command.

func IsCLISource

func IsCLISource(source AgentSource) bool

IsCLISource reports whether a source names a CLI rather than a hosted provider. It answers the question by SPELLING, not by resolvability, so a misspelled "@cluade/sonnet" is still recognized as a CLI source and gets the CLI-specific error instead of the generic "no known provider prefix".

func ListBuiltinAgentNames

func ListBuiltinAgentNames() ([]string, error)

ListBuiltinAgentNames returns the available built-in agent profile names.

func ListBuiltinResourceTypeNames

func ListBuiltinResourceTypeNames() ([]string, error)

ListBuiltinResourceTypeNames returns the available built-in resource type names.

func MCPStatusReason

func MCPStatusReason(name string, err error) string

MCPStatusReason renders a probe failure as a status cell, dropping the copies of the server's name the error carries — the row already names it — because the actionable part of these messages ("run `steps mcp login` …", "$TOKEN is not set") is at the END.

func ModelSeesImages

func ModelSeesImages(modelName string) bool

ModelSeesImages reports whether a hosted model is known to accept an image. A CLI source is not asked: what its CLI does with an image is the CLI's business, not the model name's.

func ParseRate

func ParseRate(rate string) (int, time.Duration, error)

ParseRate reads "<n>/<s|m|h>" into a count and the window it is counted over.

func ParseTimeout

func ParseTimeout(s string) (time.Duration, error)

ParseTimeout parses a timeout string into a time.Duration. Empty string is valid and returns 0 (no timeout). Returns an error for invalid Go duration format (e.g., "2m", "30s", "1h30m" are valid; "2 minutes" is not).

func ReadBuiltinPrompt

func ReadBuiltinPrompt(name string) (string, error)

ReadBuiltinPrompt returns the content of a named built-in system prompt. name is like "builder" — resolves to "prompts/<name>.md".

func ReadBuiltinToolDescription

func ReadBuiltinToolDescription(name string) (string, error)

ReadBuiltinToolDescription returns the description for a named built-in tool. name is like "read_file" — resolves to "tools/<name>.md".

func ReadSource

func ReadSource(path string, vars map[string]string) ([]byte, error)

ReadSource reads the pipeline file at path with ((name)) substitution applied: the bytes a revision is taken over, and what `steps pipeline set` uploads.

func RenderValue

func RenderValue(value any, vars map[string]string) any

RenderValue substitutes ((name)) throughout a decoded YAML value, returning a copy. Used for params:, which is a free-form mapping rather than a field.

func RenderVars

func RenderVars(value string, vars map[string]string) string

RenderVars substitutes ((name)) in a single value from a run-time var map, leaving unknown references in place.

func ReservedEnvNames

func ReservedEnvNames() []string

ReservedEnvNames lists the variable names env: refuses.

func ResolvedAgentTimeout

func ResolvedAgentTimeout(riTimeout string) time.Duration

ResolvedAgentTimeout applies the default-and-zero-means-unlimited convention documented in docs/attempts-timeout.md to an agent step's timeout: string: empty resolves to DefaultAgentStepTimeout, "0" resolves to 0 (no deadline — the caller's convention for "unlimited"), and an (unreachable against a validated config) parse error falls back to the default rather than to "no deadline" — a typo must never silently produce an unbounded step.

func Slugify

func Slugify(path string) string

Slugify turns a pipeline path into its identity: the base name without its extension.

It lives here, and web.Slugify calls it, because a second copy is how the identity split the first time. internal/web cannot be imported from this package, and the string is needed on both sides.

func StableStrings

func StableStrings(names []string) []string

StableStrings returns a non-nil copy of names, so json.Marshal always encodes it as [] rather than null — a nil inputs/outputs list and an explicit inputs: [] must hash identically, since they mean the same thing (no artifacts). Used by merkle/agent content-hashing when folding in a step's Inputs/Outputs.

func ToolSpecName

func ToolSpecName(spec ToolSpec) string

ToolSpecName is the name a ToolSpec is referenced by: the builtin name for a builtin, the sub-agent's name for a sub-agent tool, "server__tool" (see MCPToolNameSep) for a single-tool MCP grant, the bare server name for a multi/all-tool MCP grant (selectable by a step as a unit — see resolveEffectiveTools), or the custom tool's name.

func UnionEnv

func UnionEnv(typeEnv, extraEnv []string) []string

UnionEnv combines a resource type's own env: with one resource instance's additional env: (config.Resource.Env), without mutating either slice — both are shared config data that later calls (or other resources of the same type) still read.

It lives here rather than in internal/resource because internal/merkle folds the identical union into a get/put node's hashed content — the env NAMES are cache identity for the same reason resourceType.Env alone already was (see merkle.GetNodeContent) — and the two packages must agree on what that union is without merkle importing resource. A name repeated between the two lists appears once, in first-occurrence order. It IS a union, and a resource restating a name its type already allows means nothing new — but a duplicate would otherwise reach shell.RunnerSpec.Env twice and, worse, hash differently from the identical effective allow-list without the repeat, missing the merkle cache for no semantic change.

func UnresolvedVars

func UnresolvedVars(value string) []string

UnresolvedVars lists every ((name)) still present in a value, sorted.

func UpstreamPath

func UpstreamPath(sender string) string

UpstreamPath is the workspace-relative file one sender's outcome is delivered at. Relative, because it is resolved against the reading step's own working directory.

func ValidPipelineName

func ValidPipelineName(name string) error

ValidPipelineName is an allow-list because the name is CONCATENATED into "/p/"+slug, where "?" truncates into a query, "%" decodes into a segment the name never had, and ".." traverses.

func ValidateArtifactName

func ValidateArtifactName(name string) error

ValidateArtifactName checks name against artifactNamePattern. Used both at config-load time (validateArtifactNames) and by internal/workspace at runtime, when materializing a step's directories.

func ValidateArtifactPath

func ValidateArtifactPath(artifactPath string) error

ValidateArtifactPath accepts what a capture may materialize to: an artifact name, optionally followed by /-separated segments inside it — a collecting matrix captures a cell's output at findings/alpha/fast (see CollectedOutputMapping). The first segment is a full artifact name, reservations included; later segments need only the charset, since they name directories inside an artifact rather than artifacts. Every segment matching the pattern is what makes the path safe to join: no segment can be empty, "..", or absolute, so it implies ConfinedPath without asking it.

USER-authored mapping values never reach this looser rule — they are pinned to ValidateArtifactName at load (see validateArtifactMappings), so the only slash-carrying paths are machine-composed from already-validated parts.

Types

type Access

type Access struct {
	// Viewer is who may see the pipeline at all. Anybody else is told it does not exist.
	Viewer string `yaml:"viewer,omitempty"`
	// Operator is who may trigger, approve and abort in it. Anybody else who may see it only reads.
	Operator string `yaml:"operator,omitempty"`
}

Access narrows who may see and operate one pipeline on a daemon people sign in to. Each field is a rule, an expression over the signed-in person (see docs/authentication.md), and it can only NARROW what the daemon's own --role rules grant: a pipeline is a file anybody with a pull request can edit, so nothing written here can widen anyone's access. A daemon's admins are not narrowed — they can set or destroy the pipeline regardless.

The rules are compiled by the daemon that serves the pipeline, which refuses the set if one cannot be; here only their shape is checked. A pipeline run with `steps run` has nobody signed in, and nothing to narrow.

Never hashed: who may look at a build changes nothing it does.

type AcrossVar

type AcrossVar struct {
	Var    string   `yaml:"var"`
	Values []string `yaml:"values,omitempty"`
	// FromFile is a workspace-relative path to a JSON array of strings, whose
	// first path component names the artifact holding it (`findings/items.json`
	// requires an artifact `findings` fetched or produced earlier in the plan —
	// the same rule dir: follows, checked by workspace.ValidateArtifactFlow).
	//
	// A file rather than a key in a store: the producing step already declares
	// the artifact in its outputs:, so the value travels the way every other
	// piece of inter-step data does, and there is nothing extra to opt into on
	// the writing side. Mutually exclusive with Values.
	FromFile string `yaml:"from_file,omitempty"`
}

AcrossVar is one axis of a matrix: a variable name and the values it takes.

The values come from exactly one of two places. values: is the static list, known when the pipeline is written. from_file: names a JSON file an earlier step produced, so the matrix's width is decided during the run — "review each of these findings", where nobody knew the findings when the pipeline was authored.

func (AcrossVar) Runtime

func (a AcrossVar) Runtime() bool

Runtime reports whether this axis takes its values from a file an earlier step wrote, rather than from the pipeline text.

func (AcrossVar) SourceArtifact

func (a AcrossVar) SourceArtifact() string

SourceArtifact is the artifact a from_file: axis reads from: the path's first component, the way dir: names one. "" for a static axis.

Defined here rather than in each consumer so the plan-time availability check (internal/workspace) and the run-time read (internal/pipeline) can never disagree about which artifact a path names.

type Agent

type Agent struct {
	Name   string      `yaml:"name"`
	Source AgentSource `yaml:"source"`
	// Description is the human-readable summary of what this agent does. It is
	// shown to the model when another agent grants this one as a sub-agent tool
	// (see ToolSpec.Agent) with no inline description. An agent referenced as a
	// sub-agent MUST have a description — either inline on the grant or on the
	// agent itself — or pipeline load fails with a clear error.
	Description string `yaml:"description,omitempty"`
	// File loads every field of this agent but name: from a YAML
	// document at a path relative to the pipeline file's directory (see
	// LoadConfig's resolveFileIncludes), so one agent definition can be
	// shared across pipelines. Any field also set inline on this entry
	// overrides the loaded document's value for that field — the same "wins
	// when set" idiom ResolveTask already uses between a step and its tasks:
	// entry. The loaded document may not itself use file:/system_file:.
	File string `yaml:"file,omitempty"`
	// Image, when set, runs this agent's run_shell/custom-tool commands in a
	// fresh `docker run --rm` container from this image instead of on the
	// host. A step's own Image, if set, overrides this for that step only
	// (see Step.Image). Empty (the default) keeps host execution.
	Image string `yaml:"image,omitempty"`
	// Env names host environment variables this agent's run_shell/custom-tool
	// commands are allowed to see, on top of the always-allowed baseline (see
	// shell.HostEnv). Names only — see validateEnvValues. A step's own Env, if
	// non-nil, overrides this for that step only (see Step.Env).
	Env []string `yaml:"env,omitempty"`
	// User is the container user this agent's run_shell/custom-tool commands
	// execute as (docker's --user). Empty takes the platform default — see
	// shell's defaultContainerUser. Only meaningful alongside Image.
	User string `yaml:"user,omitempty"`
	// Network is the container network this agent's run_shell/custom-tool
	// commands join (docker's --network); "none" cuts off egress entirely.
	// Requires Image, except "none" beside Sandbox.
	Network string `yaml:"network,omitempty"`
	// Privileged runs this command's container with `docker run --privileged`.
	// Mirrors Concourse's privileged: (concourse-ci.org/docs/steps/task/).
	// Container-only, like Network — a host command has nothing to elevate,
	// so it is a load error without image:.
	Privileged bool `yaml:"privileged,omitempty"`
	// Limits caps the container's CPU and memory. Mirrors Concourse's
	// container_limits:; container-only for the same reason Privileged is.
	Limits *ContainerLimits `yaml:"container_limits,omitempty"`
	// Sandbox fences this entity's host commands with the OS sandbox: writes only under the step's workspace and a private TMPDIR, credential directories unreadable, and network: none becomes legal on the host. See internal/config/sandbox.go for the rules and docs/infra.md for the policy.
	Sandbox bool `yaml:"sandbox,omitempty"`
	// System is the persona/system message given to the model. Empty falls
	// back to a generic CI-agent persona.
	System string `yaml:"system,omitempty"`
	// SystemFile loads System's text from a file at a path relative to the
	// pipeline file's directory, instead of writing it inline — useful since a
	// persona is often long freeform prose. Mutually exclusive with System.
	SystemFile string `yaml:"system_file,omitempty"`
	// Generation dials, forwarded to the model when set. ReasoningEffort is
	// one of "low", "medium", "high" (for reasoning-capable models).
	Temperature     *float64 `yaml:"temperature,omitempty"`
	TopP            *float64 `yaml:"top_p,omitempty"`
	MaxTokens       int      `yaml:"max_tokens,omitempty"`
	ReasoningEffort string   `yaml:"reasoning_effort,omitempty"`
	// MaxTurns caps the tool-calling loop. Unset takes defaultTurnsFor(source)
	// — 30 for a hosted agent, none for a cli one; an explicit 0 removes the
	// cap entirely — see dials.go for the convention this shares with
	// MaxContextBytes and Timeout.
	MaxTurns *int `yaml:"max_turns,omitempty"`
	// MaxQuestions is the ask_user budget every step of this agent gets when
	// it declares none of its own. Unset takes defaultMaxQuestions; an
	// explicit 0 removes the cap. See Step.MaxQuestions for why the dial
	// counts questions rather than turns.
	MaxQuestions *int `yaml:"max_questions,omitempty"`
	// Timeout is the wall-clock deadline every step of this agent gets when
	// it declares none of its own (e.g. "20m"). Unset takes the package
	// default (30 minutes — DefaultAgentStepTimeout); "0" means no deadline
	// at all, which no other step kind needs a spelling for because omitting
	// the field already says it there.
	//
	// It lives here as well as on the step because the right deadline is
	// usually a property of the AGENT — a deep reviewer needs twenty minutes
	// whichever step invokes it — and without it a shared deadline had to be
	// copied onto every step naming the agent. Not hashed, exactly as the
	// step's own timeout: is not: a deadline is not part of what a step asks
	// for.
	Timeout string `yaml:"timeout,omitempty"`
	// Attempts is how many times a step of this agent re-issues a failed
	// REQUEST when it sets no attempts: of its own (see defaultAgentAttempts,
	// and docs/attempts-timeout.md for why a retry here is a transport
	// concern rather than a re-run of the conversation). Unset takes that
	// default; 0 is a load error, not "unlimited" — see dials.go. Never
	// hashed.
	Attempts *int `yaml:"attempts,omitempty"`
	// CompactAfterTokens caps how large a conversation's token
	// count grows before older turns are summarized away and replaced by a
	// running summary (see internal/agent/compaction.go and docs/agents.md's
	// "Compacting long conversations" section). A pointer, not a plain int,
	// because unset and explicit 0 mean different things: unset resolves to
	// defaultCompactAfterTokens (compaction ON by default), while 0 disables
	// compaction entirely — the same distinction Temperature/TopP already
	// need a pointer for. Like MaxTurns, this is agent-only: no per-step
	// override.
	CompactAfterTokens *int `yaml:"compact_after_tokens,omitempty"`
	// ContextWindow states this model's context window in tokens, for a model
	// contextWindows does not recognize — a local build, a release newer than
	// that table, or a HOST that serves a known model with a smaller window
	// than it has natively (a table keyed on model name cannot express the
	// last one at all).
	//
	// It is the "what the model is" knob to compact_after_tokens:' "what the
	// budget is": setting it keeps compactBudgetPercent applying, so an
	// operator says 200000 rather than computing 160000, and the run's own
	// compaction log reports a derived window instead of an assumed one.
	// compact_after_tokens:, when also set, still wins outright. Not hashed, for compact_after_tokens:' own reason: its only consumer is resolveCompactionBudget, so it is the same operational budget reached by another route.
	ContextWindow int `yaml:"context_window,omitempty"`
	// MaxContextBytes caps how much of a context_paths: file is handed to the
	// model at conversation start. Unset takes DefaultMaxContextBytes; an
	// explicit 0 hands over the whole file however large it is (see dials.go).
	//
	// It exists because the byte budget priming borrows was chosen for a
	// different job: context_paths is delivered as a synthetic read_file
	// result, so it inherited read_file's tool budget, and that number is sized
	// against the spill mechanics (it sits above maxToolOutputBytes so a
	// spilled output can be read back whole). Nothing in that reasoning is
	// about how much evidence a step should open holding — and on a model whose
	// context_window: is 1M, the inherited 100KB is about 2.5% of the window.
	//
	// Over the limit the file is TRUNCATED with a pointer, never refused, so
	// raising this buys context rather than turning a warning into an error.
	// Operational, like CompactAfterTokens, and excluded from the hash for the
	// same reason: it governs how much of a conversation's own history and
	// evidence is carried, not what the step is asking for.
	MaxContextBytes *int `yaml:"max_context_bytes,omitempty"`
	// Fallback lists alternate sources to use when the primary is
	// UNREACHABLE, in order. See AgentFallback. Never hashed: the primary is what the step ASKS for and is what the key names, while this is outage handling — keying it would re-run every cached step because somebody edited what happens when a provider is down.
	Fallback []AgentFallback `yaml:"fallback,omitempty"`
	// Preflight opts this agent out of (or into) the pre-run health check.
	// A pointer so unset inherits defaults.preflight. The case it exists for
	// is a model expected to be slow to WAKE — a cold local model would fail
	// a probe that a real conversation would have waited out. Never hashed: it decides whether a health probe runs BEFORE the step, and nothing about the conversation the step then has.
	Preflight *bool `yaml:"preflight,omitempty"`
	// Budget caps what one invocation of this agent may spend (see Budget).
	// Per invocation, not per job: a job budget is the cumulative ceiling and
	// lives on the job. Never hashed.
	//
	// The ceiling covers this agent AND everything it delegates to: a
	// sub-agent draws on this allowance rather than adding to it, so the
	// number bounds the whole subtree rather than one conversation in it.
	Budget *Budget `yaml:"budget,omitempty"`
	// DelegateBudgetPercent overrides how much of this agent's REMAINING
	// allowance one sub-agent call may take, for agents whose helpers are
	// unusually cheap or expensive relative to the parent. Pipeline-wide
	// default in Defaults.DelegateBudgetPercent. Never hashed, like the
	// budget it divides.
	DelegateBudgetPercent *int `yaml:"delegate_budget_percent,omitempty"`
	// Tools is the grant: the built-in tools this agent may use plus any
	// reusable custom tool definitions. A step selects a subset by name and
	// may add its own inline custom tools. Empty grants the read-only
	// built-ins (DefaultAgentToolSpecs).
	Tools []ToolSpec `yaml:"tools,omitempty"`
	// Settings opts a CLI-backed agent into the CLI's own configuration
	// scopes. The only accepted value is "project" — load the repo's
	// checked-in project scope (.claude/ settings, CLAUDE.md, hooks).
	// Absent, the subprocess loads no settings at all: a pipeline step is
	// not a personal session, and repo config shaping an agent is a
	// capability the pipeline must declare, not inherit. Rejected on hosted
	// (HTTP) agents, where there is no CLI to configure.
	Settings string `yaml:"settings,omitempty"`
	// Caches are directories this agent's commands keep between runs on the machine that ran them (see TaskCache). A referencing step's own Caches, if non-nil, override these for that step only.
	Caches []TaskCache `yaml:"caches,omitempty"`
}

Agent is a named, reusable worker an `agent` step invokes: it owns the model connection, persona, generation dials, limits, and the tool grant (the set of tools a step may draw from). A step supplies the per-task prompt, working directory, and tool selection.

func ReadBuiltinAgent

func ReadBuiltinAgent(name string) (Agent, error)

ReadBuiltinAgent returns the parsed YAML of a named built-in agent profile. name is like "builder" — resolves to "agents/<name>.yml".

type AgentFallback

type AgentFallback struct {
	Source AgentSource `yaml:"source"`
}

AgentFallback is one alternate model to fall over to when the primary is unreachable — a different provider, a different endpoint, or the same endpoint's larger sibling.

It exists because a provider outage is not the sort of failure retrying fixes. In one real run a model went unavailable upstream and killed three consecutive runs over roughly 50 minutes, while attempts: amplified the waste rather than absorbing it. The manual fix was exactly this: point the agent at a different model, one line.

Only the source can vary. A fallback is meant to be the same agent reaching a different endpoint, not a different persona or a different tool grant — swapping those would make an outage change what the agent is allowed to do.

type AgentSource

type AgentSource struct {
	Endpoint         string `yaml:"endpoint,omitempty"`
	Model            string `yaml:"model"`
	APIKeyEnv        string `yaml:"api_key_env,omitempty"`
	StringToolChoice *bool  `yaml:"string_tool_choice,omitempty"`
}

AgentSource selects the model and how to reach it. Model may carry a provider prefix (e.g. "openrouter/anthropic/claude-3.5-sonnet", "lmstudio/qwen2.5-coder") that resolves Endpoint and a default APIKeyEnv from a built-in table (see resolveAgentTarget in agent.go); Endpoint, when set, is the API base URL (e.g. "https://api.openai.com/v1/") and overrides the derived one. APIKeyEnv names an OS environment variable read at run time — the key is never stored in YAML.

StringToolChoice overrides whether forcing a required tool (see forceRequiredTool in internal/agent/conversation.go) uses OpenAI's named tool_choice object or the string "required" fallback. Left unset, it defaults to the resolved provider's requiresKey (cloud providers get the precise named form; local/no-auth providers like lmstudio/ollama, whose OpenAI-compat servers often don't support the object form, get the fallback) or false for an explicit endpoint: with no recognized provider prefix. A pointer so "unset" is distinguishable from an explicit false. Never hashed: it picks how one request is SPELLED for a server that cannot parse the precise form, the same boundary drawn around a CLI's version — steps hashes which thing was asked for, not the wire it was asked over.

type Approval

type Approval struct {
	// Message is what the waiting approval says it is asking about. Required:
	// an approval prompt with no question is one nobody can answer
	// responsibly, and "approve job build step 3?" is not a question.
	Message string `yaml:"message"`
	// Timeout bounds the wait (e.g. "24h"). Empty means the default (see
	// defaultApprovalTimeout in internal/pipeline). An expired approval
	// classifies as ABORTED, not failed — the outcome vocabulary already
	// distinguishes "nobody answered" from "somebody said no", and conflating
	// them would make a silent expiry indistinguishable from a rejection.
	Timeout string `yaml:"timeout,omitempty"`
}

Approval pauses a plan until a person says yes.

Agent pipelines eventually gate something that cannot be undone — publishing, deploying, sending — and this is the oldest and most direct safeguard for that. Before it, a plan ran start to finish with no place to stop and ask.

type Assert

type Assert struct {
	Execution []string `yaml:"execution,omitempty"`
	// Outcome, on a Job, asserts what the plan CONCLUDED rather than which
	// steps ran: AssertOutcomeFailed or AssertOutcomeSucceeded. It exists
	// because execution: cannot express "this job should have failed" — a
	// matching execution: clears the plan's error, so a fixture for any defect
	// about error propagation or swallowing passes on the broken build and the
	// fixed build alike. See docs/control-flow.md.
	Outcome string  `yaml:"outcome,omitempty"`
	Stdout  *string `yaml:"stdout,omitempty"`
	Code    *int    `yaml:"code,omitempty"`
	// Verdict, on an agent step that declares verdicts:, asserts which one the
	// model emitted — an exact match against the declared vocabulary.
	//
	// It exists because a verdict is the one agent outcome a fixture could not
	// pin directly: stdout: tests the prose around the decision, and
	// tool_calls: could only assert that the verdict tool was CALLED, not what
	// it chose. A classifier whose whole product is the choice was therefore
	// untestable at the one place it matters.
	Verdict *string `yaml:"verdict,omitempty"`
	// ToolCalls, on an agent step, asserts the ordered trajectory of tool
	// calls the model made (see ExpectedToolCall). Agent-only: a task step
	// runs no tools. Every entry must appear, in order, as a subsequence of
	// the observed calls.
	ToolCalls []ExpectedToolCall `yaml:"tool_calls,omitempty"`
	// Files, on a task or agent step, asserts that every listed path exists
	// as a non-empty file in the step's own outputs — the one thing no other
	// assert field checks: what the step WROTE rather than what it SAID. Each
	// path is artifact-relative (ValidateArtifactPath), and its first
	// component must name one of the step's declared outputs:.
	Files []string `yaml:"files,omitempty"`
	// Nudge, on an agent step, opts the step into being TOLD about an unmet
	// files:/tool_calls: entry while the model can still act on it — a
	// message at the moment it tries to stop, up to a fixed allowance — and
	// into the verdict tool refusing a decision over the same gap. Off, the
	// assert is the silent post-hoc judge every other field is.
	//
	// Opt-in because a nudge is automatic RECOVERY, not detection: a step
	// that never writes its file fails loudly either way, but a fixture whose
	// model is always put back cannot prove its prompt asked for the file at
	// all. It may only name an unmet obligation, never a wanted conclusion:
	// alongside stdout: or verdict: it is a load error, since a classifier
	// told the answer is not a test of anything.
	Nudge bool `yaml:"nudge,omitempty"`
}

Assert is a self-verification directive, in one of two shapes depending on where it's attached:

  • On a Config (top level), only Execution is valid: an ordered list of the job names that must have run.
  • On a Job, Execution (task/agent/get/put/hook names, in execution order) and Outcome. By omission Execution also asserts what must NOT run. A matching Job Execution clears the plan's failure, so one green fixture can contain deliberately-failing tasks; Outcome is how a fixture says what the plan concluded, which Execution structurally cannot.
  • On a task/agent Step, only Stdout/Code are valid: Stdout is a substring the step's captured output must contain, Code the exact expected exit code (task only). A matching assert makes a non-zero-exit task a success.

type Budget

type Budget struct {
	// Tokens is the ceiling for a HOSTED agent, counted from the provider's
	// own reported usage (prompt + completion, including reasoning tokens
	// where a provider reports them) — not an estimate. 0 is not "unlimited",
	// it is rejected: a budget block that caps nothing is a typo, and reading
	// it as "no limit" would be the most expensive possible interpretation.
	Tokens int `yaml:"tokens,omitempty"`
	// USD is the ceiling for a CLI agent, enforced by the CLI itself
	// mid-conversation. It exists because a subprocess reports its spend only
	// on exit — too late for a token counter here to stop anything — while the
	// CLI has a real circuit breaker of its own that takes dollars.
	USD float64 `yaml:"usd,omitempty"`
	// ReservePerCell is what an across: block's admission control ASSUMES one
	// not-yet-finished cell will spend, so Tokens binds at any max_in_flight.
	//
	// Admission can only see what FINISHED cells reported, so without a
	// reservation the first max_in_flight cells are always admitted against a
	// total of ~0 — and when the width covers every cell there is no
	// serialization point at all and the ceiling bounds nothing. Reserving on
	// admit and settling on completion closes that: overshoot becomes bounded
	// by how wrong the reservation is rather than by how many cells got a free
	// pass.
	//
	// Across-only, like Tokens on a step. Unset falls back to the cell agent's
	// own budget.tokens (the author already declared what one invocation may
	// cost), and with neither the block behaves exactly as it did before this
	// existed. Deliberately no global default: a guessed reservation that
	// silently under-admits is worse than an honest warning.
	ReservePerCell int `yaml:"reserve_per_cell,omitempty"`
}

Budget caps what an agent invocation, or a whole job's agent steps together, may spend. It is the AI equivalent of timeout:.

Two spellings, because the two runners meter different things and neither can be converted into the other honestly. A hosted agent's conversation is driven here, so tokens are counted exactly as the provider reports them; a CLI agent's conversation happens inside a subprocess that meters itself in dollars. Translating between them would need a per-model price table that goes stale every time any provider changes its rates — an ongoing maintenance burden, and a number that would silently go wrong — so each runner takes the unit it can actually enforce and validation rejects the other (see validateCLIAgents).

Like assert: and timeout:, a budget is an operational limit and is never hashed: adding one must not invalidate a cached step.

type Bundle

type Bundle map[string]string

Bundle is CLOSED on purpose: a daemon resolving an include it was not sent must fail rather than read its own disk, or a crafted run_file: reads daemon-local files the sender never uploaded.

func (Bundle) ReadFile

func (b Bundle) ReadFile(name string) ([]byte, error)

ReadFile cleans name first because filepath.Join cleans silently on disk, so `./ci/unit.sh` is recorded as ci/unit.sh and a sender keys its bundle off that form.

type CacheConfig

type CacheConfig struct {
	// Resources enables the cache. A separate field rather than the block's
	// presence meaning yes, so `cache: {resources: false}` can turn it off
	// without deleting tuning alongside it.
	Resources bool `yaml:"resources"`
	// MaxEntries bounds how many cached versions are kept, oldest-used
	// evicted first. 0 takes defaultCacheMaxEntries. A cache with no ceiling
	// grows until the disk does not, which on a long-lived daemon host is a
	// question of when, not whether.
	MaxEntries int `yaml:"max_entries,omitempty"`
}

CacheConfig opts a pipeline into reusing fetched resource versions across builds, the disk-and-bandwidth win baggageclaim provides in Concourse.

It is off by default because it changes an observable thing: a cached version's in: does NOT run again. That is correct under the resource contract — in: materializes a version, and the same version materializes the same content (see docs/conformance.md) — but a resource type whose in: has side effects beyond writing the directory (incrementing a counter, posting a notification) would see those stop happening. Opting in is the pipeline author asserting their in: is a pure fetch.

type Config

type Config struct {
	ResourceTypes []ResourceType `yaml:"resource_types"`
	Resources     []Resource     `yaml:"resources"`
	Agents        []Agent        `yaml:"agents"`
	MCPServers    []MCPServer    `yaml:"mcp_servers,omitempty"`
	// MCPExports are bundles of this pipeline's tools other people's agents may connect to through the daemon. See MCPExport.
	MCPExports []MCPExport `yaml:"mcp_exports,omitempty"`
	// Rules are named checks on a call to a shared or granted tool. See Rule.
	Rules []Rule `yaml:"rules,omitempty"`
	Tasks []Task `yaml:"tasks"`
	Jobs  []Job  `yaml:"jobs"`
	// Defaults supplies pipeline-wide fallbacks and bounds: the model an
	// agent uses when it names none, preflight tuning, the delegation budget,
	// and the version/run/memory caps. See Defaults.
	Defaults *Defaults `yaml:"defaults,omitempty"`
	// Workspace opts the pipeline into Concourse-style per-step isolation.
	// Absent (the default) keeps every step in a triggered build sharing one
	// mutable directory, exactly as before this field existed. See
	// WorkspaceConfig.
	Workspace *WorkspaceConfig `yaml:"workspace,omitempty"`
	// Assert, at the top level, names the ordered set of job names that
	// `steps test` must have run (see Assert). It's a self-verification
	// meta-check, never hashed.
	Assert *Assert `yaml:"assert,omitempty"`
	// Access narrows who may see and operate this pipeline on a daemon people
	// sign in to. See Access.
	Access *Access `yaml:"access,omitempty"`
	// Name is WHICH pipeline this is, stamped by the loader. Not YAML, never
	// hashed: it is identity, not content.
	//
	// It exists because one daemon serves several pipelines (each uploaded by
	// `steps pipeline set`), and process-wide state keyed by a name a pipeline
	// chose — an agent, a job — collides across them. The Config holds it for
	// the same reason a store.Store handle holds its pipeline: the scope has to be
	// impossible to forget.
	//
	// It is the SAME STRING the store's pipelines.name and the web UI's
	// /p/<slug> route use, and that is the whole point of it being supplied
	// rather than derived here. A path was the obvious discriminator and was
	// the wrong one: it is a second identity that disagrees with the one the
	// repo already resolves and publishes, so a pin log line said
	// /abs/infra/deploy.yml where every run record said deploy — and --name,
	// which exists precisely to say which pipeline this is, moved the store
	// and the route and not the pin.
	//
	// A Config built in a test rather than loaded has an empty Name, which
	// shares one scope with every other such Config, exactly as everything
	// did before this field existed.
	Name string `yaml:"-"`
	// Revision is WHICH configuration this is: the bytes it was parsed from,
	// and their hash. Stamped by the loader, never YAML, never hashed into a
	// node — it identifies the content, so folding it into a step's content
	// would make every step of an edited file re-run, which is the merkle
	// cache's job to decide and not this field's.
	//
	// Computed AFTER ((var)) substitution, because substitution happens
	// before the parse: one file under two --vars-files is two
	// configurations, and a revision taken from the file on disk would call
	// them one.
	//
	// A Config built in a test rather than loaded has an empty Revision, and
	// a run started from one records no revision at all — see
	// store.Store.RecordRevision.
	Revision Revision `yaml:"-"`
}

Config is the top-level shape of a Concourse-style pipeline YAML file.

func Load

func Load(path string, name string, vars map[string]string) (*Config, error)

Load is the on-disk form of Parse: substitution happens BEFORE the parse so a var may appear anywhere a value does, and a substituted value is ORDINARY CONFIG, hashed and stored — vars are parameters, never secrets.

func LoadConfig

func LoadConfig(path string) (*Config, error)

LoadConfig reads and parses a pipeline YAML file at path, under the identity its file name implies.

The convenience form. A caller that resolves identities of its own — main, which applies the --name overrides — calls Load and says which one, so the Config cannot end up under a name nothing else uses.

func Parse

func Parse(source []byte, name string, fsys Files) (*Config, error)

Parse builds a Config from already-substituted source with its includes read through fsys, which is what a daemon does with a `steps pipeline set` upload it has no directory for.

func WithResolvedMCPCwd

func WithResolvedMCPCwd(cfg *Config, baseDir string) *Config

WithResolvedMCPCwd returns cfg with every stdio server's relative Cwd joined against baseDir — the working directory of the agent step whose tools are about to be built. cfg is returned unchanged when no server needs it, so the overwhelmingly common case (no mcp_servers:, or all absolute) allocates nothing and hashes identically.

The copy is shallow but the MCPServers slice is fresh, so a step resolving its own view can never mutate the shared config another step (with a different working directory) will resolve later.

func (*Config) CheckEnvironment

func (c *Config) CheckEnvironment() []Problem

CheckEnvironment reports what this machine is missing for the pipeline to run: credentials the agents need, and binaries the MCP servers and CLI agents need. It makes no network calls and starts no processes — every check here is a yes/no fact answerable in microseconds.

It reports every problem it finds rather than the first, since discovering them one run at a time is the failure mode it exists to end.

func (*Config) DelegateBudgetFraction

func (c *Config) DelegateBudgetFraction(agentName string) float64

DelegateBudgetFraction is the share of the named agent's remaining allowance that one of its sub-agent calls may take: the agent's own override, else the pipeline default, else DefaultDelegateBudgetPercent.

Keyed by name rather than by *Agent because every caller has the name and an unresolvable one is not worth an error here — it means the agent could not have run at all, which the step reports far more clearly.

func (*Config) FindAgent

func (c *Config) FindAgent(name string) (*Agent, error)

FindAgent returns the agent with the given name, or an error if not found.

func (*Config) FindJob

func (c *Config) FindJob(name string) (*Job, error)

FindJob returns the job with the given name, or an error if not found.

func (*Config) FindMCPServer

func (c *Config) FindMCPServer(name string) (*MCPServer, error)

FindMCPServer returns the mcp_servers: entry with the given name, or an error if not found.

func (*Config) FindResource

func (c *Config) FindResource(name string) (*Resource, error)

FindResource returns the resource with the given name, or an error if not found.

func (*Config) FindResourceType

func (c *Config) FindResourceType(name string) (*ResourceType, error)

FindResourceType returns the resource type with the given name, or an error if not found.

func (*Config) FindRule

func (c *Config) FindRule(name string) (*Rule, error)

FindRule is the rule a tool or an export names.

func (*Config) FindTask

func (c *Config) FindTask(name string) (*Task, error)

FindTask returns the task with the given name, or an error if not found.

func (*Config) ImageArtifacts

func (c *Config) ImageArtifacts(jobName string) map[string]bool

ImageArtifacts names every value a step image: in jobName resolves as an artifact rather than as an image: each resource, and each get in the job (aliases included). A resource the job never fetches is included on purpose: otherwise its name would silently pull a Docker Hub image of that name. Artifact names cannot hold ':', '/' or '@', so only a bare image name can collide, and a resolved reference never does.

func (*Config) Images

func (c *Config) Images() []string

Images returns every distinct image: this pipeline runs on THIS machine's daemon, sorted.

Used to pull them all before the first step runs. Without that, the first command needing an uncached image pays the pull inside its own step: the progress output lands in whatever that command's stderr is being used for (a resource check's parsed output, an agent's tool result), and the download counts against the step's timeout, so a large image on a cold daemon can exhaust a budget meant for the work itself.

A PLACED step's image is deliberately absent. Its container runs on the worker's daemon, which does not exist yet when this is asked — a machine acquired for the job has not been acquired — so pulling it here would download it to a machine that will never run it, and `docker image inspect` finding a LOCALLY built tag would skip the pull the worker actually needed. It is also what lets an orchestrator with no daemon at all run a pipeline whose every container lives on a worker, which is the arrangement the feature exists for.

func (*Config) JobNames

func (c *Config) JobNames() []string

JobNames returns the names of every job in the pipeline, in declaration order, for use in "which job?" error messages.

func (*Config) MCPServerUsers

func (c *Config) MCPServerUsers(name string) []string

MCPServerUsers names everything that references the mcp_servers: entry called name: the agents whose tools: grant it, the fix: blocks (on a tasks: entry or on a step) whose own tools: grant it, and the resource types whose mcp: backend calls it. Those are every place an MCP grant may appear — a step's own tools: cannot introduce one, see rejectInlineMCPTools.

Each user is named once however many of the server's tools it grants, and a job is named once however many of its steps fix with it: the question this answers is "who reaches this server", not "how often".

A server with no users is configured and unreachable by any step, which is the kind of thing a listing exists to make visible — so a grant this misses is a server wrongly reported as dead weight.

func (*Config) MCPUsers

func (c *Config) MCPUsers(name string) string

MCPUsers is the "used by" cell: every agent, task fix and resource type that depends on this server, or a phrase saying nothing does.

func (*Config) MaxInFlightByJob

func (c *Config) MaxInFlightByJob() map[string]int

MaxInFlightByJob maps each job to how many of its builds may run at once.

serial:/serial_groups: force 1 and take precedence, matching Concourse. A job that sets neither and names no max_in_flight: is unlimited.

func (*Config) MemoryEntriesLimit

func (c *Config) MemoryEntriesLimit() int

MemoryEntriesLimit is how many entries each scope keeps. Zero from the pipeline means no limit, as for every other cap here.

func (*Config) PlacedImage

func (c *Config) PlacedImage(step Step) string

PlacedImage is the image a placed step runs in: its own or its task's or agent's, or for a get or put its resource type's; "" runs bare.

func (*Config) PolledResourceNames

func (c *Config) PolledResourceNames() []string

PolledResourceNames returns every resource a get step references with trigger: true or passed: anywhere in the pipeline, in first-seen order — the resources internal/trigger's poller checks on an interval and owns the resource_checks baseline for (internal/trigger.Resources delegates here).

passed: counts alongside trigger: true because a constrained get needs its version OBSERVED for the constraint to be judged at all, even when a change to it triggers nothing: without this a `get: artifacts, passed: [build]` with no trigger: was never checked, so the constraint could never be judged. Polling it enqueues nothing on its own, though: AffectedJobs still requires trigger: true, so a change to a passed:-only resource cannot start a build that never asked to be started by it.

A resource reached only by a plain get is deliberately excluded — nothing polls it, and internal/pipeline's per-run refresh records its version instead (see internal/pipeline/passed.go's recordResolvedVersion, gated by ResourceIsPolled below), which must never touch a resource this function names, or a run would corrupt the poller's dirty-bit baseline for it.

func (*Config) PreflightSettings

func (c *Config) PreflightSettings() *Preflight

PreflightSettings is this pipeline's preflight block, or nil when it has none — which every accessor above treats as the default.

A method rather than two callers each reaching through `cfg.Defaults`: that field is a pointer and is nil for any pipeline without a defaults: block, so the reach is a nil dereference waiting for the first pipeline that does not write one.

func (*Config) PutSteps

func (c *Config) PutSteps(resource string) []Step

PutSteps returns every step in the pipeline that publishes to the named resource, across every job's plan and hooks — the whole-pipeline form of Job.PutSteps, whose doc comment explains when each scope is the right one.

func (*Config) ResolveAgentInvocation

func (c *Config) ResolveAgentInvocation(step Step) (ResolvedInvocation, error)

ResolveAgentInvocation resolves the agent named by step against c, applying provider-prefix resolution, tool-grant merging, and defaulting (step.Attempts defaults to defaultAgentAttempts; agent.MaxTurns defaults to defaultTurnsFor(agent.Source) — 30 for a hosted agent and no cap for a cli one, since a turn is not the same quantity on the two paths; agent.CompactAfterTokens, when nil, defaults to defaultCompactAfterTokens — unlike every other field resolved here, an explicit zero value is meaningfully different from "unset" and is preserved as 0, not defaulted).

func (*Config) ResolveTask

func (c *Config) ResolveTask(step Step) (ResolvedTask, error)

ResolveTask resolves step into a ResolvedTask: a step carrying its own run: is inline and used as-is; otherwise step.Task names a tasks: entry, whose run/fix/inputs/outputs/image/timeout are used, except the step's own fix:, inputs:, outputs:, image:, and timeout:, if set (non-nil/non-empty), which override the referenced task's for this step only — the same override idiom for all five.

func (*Config) ResourceIsPolled

func (c *Config) ResourceIsPolled(resourceName string) bool

ResourceIsPolled reports whether resourceName is in PolledResourceNames — for a caller (internal/pipeline's recordResolvedVersion) that asks this once per fetched version and only ever wants a yes/no answer. A second, independent traversal here would risk answering a different question than PolledResourceNames does the moment one of them changes and the other doesn't; delegating makes that impossible by construction.

func (*Config) RunHistoryLimit

func (c *Config) RunHistoryLimit() int

RunHistoryLimit is how many runs of each job to keep, resolving the pipeline's own setting against the built-in default.

Zero from the pipeline means NO limit, matching every other cap in this repo, and is why this cannot simply be a value with a default: the difference between "unset, use the default" and "set to zero, keep everything" is the difference between a bounded database and an unbounded one, and only a pointer can carry it.

func (*Config) SerialGroupsByJob

func (c *Config) SerialGroupsByJob() map[string][]string

SerialGroupsByJob maps each job to the serial groups it belongs to.

`serial: true` contributes an implicit group named after the job itself, so the cross-job machinery covers the single-job case too.

It also turns something on now. Until job-level max_in_flight existed this runner serialized every job unconditionally, which made serial: true a statement of intent with nothing behind it; a job's effective concurrency is decided by MaxInFlightByJob below, and serial: forces it to 1.

func (*Config) StepsForAgent

func (c *Config) StepsForAgent(name string) []Step

StepsForAgent returns every step that runs the named agent.

It exists because a step is what an agent's runtime is actually resolved AGAINST: image:, network: and the rest merge the agent's own settings with the step's overrides (see resolveAgentRuntime), so any question of the form "does this agent run in a container" has no answer at the agent alone. A caller with no step to hand — preflight for a sub-agent, which no step names — gets an empty slice and should fall back to a bare Step.

func (*Config) UsesImages

func (c *Config) UsesImages() bool

UsesImages reports whether any resource_type, agent, task, or step sets image: — used to fail fast (before any step runs) when docker isn't available but the pipeline needs it.

func (*Config) VersionHistoryLimit

func (c *Config) VersionHistoryLimit() int

VersionHistoryLimit is how many versions of each resource to keep, resolving the pipeline's own setting against the built-in default.

A caller that has a command-line default writes it into Defaults before asking, so precedence stays in one place: whatever the pipeline says wins, because it is the thing that knows what its resources do. Zero means NO limit, matching the convention docs/attempts-timeout.md states for every dial in this repo — omitted takes the default, 0 means no limit. It read `> 0` before, which quietly turned `version_history: 0` into the 1000 default: a reader who wrote it to stop pruning got pruning anyway, and the field disagreed with run_history: next to it.

type ContainerLimits

type ContainerLimits struct {
	// CPU is docker's `--cpu-shares`: a RELATIVE weight against other
	// containers, not a core count and not a percentage. 1024 is the default
	// weight, so 512 is "half the share of a default container when both are
	// contending" — and it caps nothing at all on an idle machine. Named cpu:
	// to match Concourse rather than renamed to something more honest, since a
	// pipeline moving between the two should mean the same thing in both.
	CPU int `yaml:"cpu,omitempty"`
	// Memory is docker's `--memory`, in BYTES. A container exceeding it is
	// OOM-killed by the kernel, which surfaces as exit code 137 — worth
	// knowing, because that reads as an ordinary command failure and not as a
	// limit being enforced.
	Memory int64 `yaml:"memory,omitempty"`
}

ContainerLimits caps a container's CPU and memory, mirroring Concourse's container_limits: (concourse-ci.org/docs/steps/task/).

Both fields are optional and independent: setting one and not the other is the common case, since memory is the limit people actually reach for.

func (*ContainerLimits) CPUShares

func (l *ContainerLimits) CPUShares() int

CPUShares and MemoryBytes unpack an optional limits block into the zero-means-omit form the shell runner takes, nil included — so every construction site reads the same and none of them needs a nil branch.

Methods on the type rather than a helper copied into each consuming package: they were duplicated verbatim in internal/pipeline and internal/agent, which is two places for a future default to be added and one of them to be missed.

func (*ContainerLimits) MemoryBytes

func (l *ContainerLimits) MemoryBytes() int64

MemoryBytes is CPUShares' counterpart for --memory. See its doc comment.

type ContextSpec

type ContextSpec struct {
	// From is what this step reads from named earlier steps: their verdict,
	// and optionally the note and response that came with it. Declared on the
	// READER — nothing arrives unasked — and the demand is what makes the
	// sender's note required. See contextfrom.go.
	From map[string]FromLevel
}

ContextSpec is a step's context: — what this step reads from named earlier steps.

func (*ContextSpec) UnmarshalYAML

func (c *ContextSpec) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML decodes a ContextSpec from its one mapping key, from:.

type CronSource

type CronSource struct {
	// Expression is a crontab: five fields, or six with seconds in front, or a descriptor like @hourly.
	Expression string `yaml:"expression"`
	// Location is the zone the expression is read in, UTC when unset.
	Location string `yaml:"location,omitempty"`
	// FireImmediately makes the first-ever check mint a version instead of waiting for a slot.
	FireImmediately bool `yaml:"fire_immediately,omitempty"`
}

CronSource is a cron resource's source:, typed. Config knows its shape; the expression's meaning lives in internal/resource, which is where resource.Cron finishes the job.

func ParseCronSource

func ParseCronSource(raw map[string]any) (CronSource, error)

ParseCronSource is CronSource for a caller handed the map and not the resource — the check is. The zone is only named here; whoever needs it resolves it once through TimeLocation.

func (CronSource) TimeLocation

func (s CronSource) TimeLocation() (*time.Location, error)

TimeLocation is the zone the expression is read in.

type Defaults

type Defaults struct {
	// Model is the source.model: an agents: entry inherits when it declares
	// none. Falls back to $STEPS_MODEL when unset here.
	Model string `yaml:"model,omitempty"`
	// Preflight tunes the pre-run health check (see Preflight). It belongs
	// here, unlike attempts:/timeout:, because it changes no step's failure
	// semantics — it only decides how early a failure that WOULD happen is
	// discovered.
	Preflight *Preflight `yaml:"preflight,omitempty"`
	// DelegateBudgetPercent is how much of an agent's REMAINING token
	// allowance one sub-agent call may take, pipeline-wide (see
	// Agent.DelegateBudgetPercent for the per-agent override, and
	// DefaultDelegateBudgetPercent for the value when neither is set).
	DelegateBudgetPercent *int `yaml:"delegate_budget_percent,omitempty"`
	// VersionHistory is how many versions of each resource steps remembers.
	//
	// It belongs to the pipeline because the right number is a property of
	// the resources it watches: a git branch produces a version per push, a
	// chat feed one per message. `--version-history` supplies a default when
	// this is unset, and DefaultVersionHistory when neither is.
	//
	// The floor is not zero: history is what lets a job build a version that
	// scrolled out of a check's window, and what a `passed:` gate proves
	// against — a version pruned from history takes its green record with it,
	// so a number below what a slow downstream job needs holds that job back.
	VersionHistory *int `yaml:"version_history,omitempty"`
	// RunHistory is how many runs of each job steps keeps in full.
	//
	// It belongs next to VersionHistory because it answers the same question
	// about the other half of the database, and it was the half with no answer:
	// resource_versions was the only table with a prune path, so every run
	// recorded its nodes, its events, its usage and its agent transcripts
	// forever. Measured on a pipeline answering Slack mentions overnight, a
	// build cost about 23KB and nothing ever gave a byte back.
	//
	// Per JOB rather than pipeline-wide, for the reason the cap exists at all:
	// a global number makes the least active job the least inspectable, since a
	// busy neighbour evicts its history.
	//
	// Reaping a run takes its events, steps and usage rows with it by foreign
	// key, and the nodes no surviving run refers to with them — so the floor is
	// not free of meaning either. It is a cache horizon: a step whose content
	// has not changed since before the window runs once more instead of being
	// skipped. `--run-history` supplies a default when this is unset, and
	// DefaultRunHistory when neither is.
	RunHistory *int `yaml:"run_history,omitempty"`
	// MemoryEntries is how many entries each memory: scope keeps, the oldest
	// going first. Its own bound rather than run_history:'s, because memory
	// outlives the runs that wrote it — that is what it is for — and
	// DefaultMemoryEntries when unset.
	MemoryEntries *int `yaml:"memory_entries,omitempty"`
}

Defaults holds pipeline-wide fallbacks. It carries exactly one field, and the omissions are deliberate.

model: is pure repetition — every agent in a pipeline usually calls the same one, and spelling out a connection string per agent is noise that makes examples unrunnable for anyone whose setup differs. Defaulting it erases nothing: the resolved model is still hashed, so overriding it correctly busts the affected agents' caches.

attempts:/timeout: are deliberately NOT here, upholding the argument in docs/attempts-timeout.md. A budget default changes failure behavior at a distance: a step written with no timeout means "this one has no deadline", and a pipeline-level default silently converts that into a deadline somebody else picked. image: is out for the same reason in reverse — an absent image *means* "run on the host", so a default would flip a documented semantic for every step that never asked.

type DirFS

type DirFS string

DirFS is a thin os.ReadFile rather than an fs.FS because fs.ValidPath refuses "..", and a shared ../tasks/ beside a pipelines/ directory is a legitimate layout at the same trust level as the pipeline file.

func (DirFS) ReadFile

func (d DirFS) ReadFile(name string) ([]byte, error)

ReadFile reads name relative to the directory d wraps.

type Ensemble

type Ensemble struct {
	// Agents are the members, each an ordinary agent step carrying its own
	// prompt and tool selection. They vote on the block's Verdicts, which are
	// declared once here rather than repeated per member.
	Agents []Step `yaml:"agents"`
	// Verdicts is the vocabulary every member votes in, in order of
	// precedence for decide: any — and, on the block, where the combined
	// decision sends the plan. Same list shape as a lone agent's (see
	// VerdictRoute); it sits here rather than on the step because the members
	// vote in it and the block routes on what they conclude.
	Verdicts []VerdictRoute `yaml:"verdicts"`
	// Decide is the rule (majority/unanimous/any) or the name of an agent
	// that judges the members' answers.
	Decide string `yaml:"decide"`
	// MemberErrors is what to do about a member that failed rather than
	// voted: MemberErrorsFail (the default) or MemberErrorsExclude.
	MemberErrors string `yaml:"member_errors,omitempty"`
}

Ensemble asks several agents the same question at once and combines their answers into one decision, which the step then routes on.

A single model has blind spots. Ask one reviewer "is this correct?" and you get one opinion with no signal about how much to trust it; ask three and require a majority, and one model's bad day stops being decisive.

⚠️ N agents cost N times one. This is the step where a job budget: matters most.

func (*Ensemble) EnsembleVerdictsFor

func (e *Ensemble) EnsembleVerdictsFor() []VerdictRoute

EnsembleVerdictsFor returns that vocabulary as verdict entries, stripped of the block's routing.

A member emits a vote; the BLOCK decides where that sends the plan. Handing members the targets too would make each one's recorded identity depend on routing it can neither see nor take — and would put the reserved failure catch, which no model may choose, in front of the voters.

func (*Ensemble) FailsOnMemberError

func (e *Ensemble) FailsOnMemberError() bool

FailsOnMemberError reports whether one member's failure fails the ensemble.

func (*Ensemble) JudgeAgent

func (e *Ensemble) JudgeAgent() string

JudgeAgent returns the agent name decide: names, or "" for a rule.

func (*Ensemble) VoteNames

func (e *Ensemble) VoteNames() []string

VoteNames is the vocabulary an ensemble's members vote in, as plain names in declaration order: what decide: any walks in precedence order, and what a judge is told it may pick.

type ExpectedToolCall

type ExpectedToolCall struct {
	Name string            `yaml:"name"`
	Args map[string]string `yaml:"args,omitempty"`
}

ExpectedToolCall is one entry in an agent step's assert.tool_calls: the tool's name, plus (optionally) a subset of the arguments the model must have called it with. Args is a subset match — every listed key must be present with an equal value, and any extra actual argument is ignored. Values compare as strings, since every argument reaching a tool's run: template is rendered as one (this is a deliberate divergence from secret-agent's eval matcher, which coerces across int/float).

type ExprResourceConfig

type ExprResourceConfig struct {
	Check     string `yaml:"check,omitempty"`
	CheckFile string `yaml:"check_file,omitempty"`
	In        string `yaml:"in,omitempty"`
	InFile    string `yaml:"in_file,omitempty"`
	Out       string `yaml:"out,omitempty"`
	OutFile   string `yaml:"out_file,omitempty"`
}

ExprResourceConfig backs a resource type's three lifecycle stages with expressions instead of shell commands — the form for a resource that is a JSON HTTP API and nothing else.

The rule for choosing, narrow on purpose:

Reach for expr: when the resource is a JSON HTTP API and nothing else.
Reach for check:/in:/out: for everything else.

Shell stays the only option — not merely the traditional one — whenever a real tool does the work (git, docker, aws, kubectl), whenever the artifact is binary or big enough to want streaming to disk rather than holding as a string, and whenever the command should run in a container: image:, network:, privileged: and container_limits: are shell-only, because an expression evaluates in-process and has no container to configure.

Each slot has a _file sibling, following run_file:/system_file:/message_files: A twenty-line program has no business inside a YAML scalar, and a real file is reviewable: a diff reads as a diff and a comment lands on a line. Suggested extension .expr.

type Fetched

type Fetched struct {
	Source  map[string]any
	Version map[string]any
}

Fetched is what one get put in a build: its resource's source and the version it fetched — the two halves an artifact image is made of.

type FileRef

type FileRef struct {
	Path     string
	Artifact string
}

FileRef is an agent step's message_files: — the text of the model's prompt for this step, loaded from a file rather than written inline. A scalar path is relative to the pipeline YAML's own directory and is resolved at load time, exactly like every other *_file field (see LoadConfig's resolveFileIncludes). A mapping {artifact, path} instead names a file inside an artifact this step declares in its inputs: — resolved at run time, once that artifact is fetched, since merkle.PlanChains hashes every step before any get's `in` has run (see internal/merkle's planGetStep) and so cannot see the file at plan time.

func (*FileRef) Deferred

func (f *FileRef) Deferred() bool

Deferred reports whether f names a run-time artifact file (the {artifact, path} mapping form) rather than a load-time, pipeline-relative one.

func (*FileRef) UnmarshalYAML

func (f *FileRef) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML decodes a FileRef from either a scalar (a pipeline-relative path) or a mapping ({artifact, path}) YAML node — the same scalar-or-mapping idiom as WhenSpec/FixSpec.

type Files

type Files interface {
	ReadFile(name string) ([]byte, error)
}

Files is what a pipeline's includes are read through: a directory on disk, or the bundle a `steps pipeline set` uploaded, since a daemon has no sibling filesystem of the sender's.

type FixSpec

type FixSpec struct {
	Agent string // agents: entry to invoke on failure
	// Messages is what the repair agent is asked, one user turn per entry.
	// Empty uses a default fix prompt. Named for the step field it mirrors:
	// this is the same concept, and calling it something else here would make
	// the DSL say two words for one idea.
	Messages []string
	// MessageFiles loads Messages from files, one message per file, at paths
	// relative to the pipeline file's directory. Mutually exclusive with
	// Messages. Mapping form only (a bare scalar fix: has no room for it).
	MessageFiles []string
	Dir          string     // optional working dir, relative to the workspace
	Tools        []ToolSpec // optional subset/addition to the agent's tool grant
	// Attempts is how many times the fix agent's failing REQUEST is
	// re-issued. Unset takes the agent default; 0 is a load error, like
	// attempts: anywhere else — see dials.go.
	Attempts *int
	Timeout  string // optional wall-clock deadline per fix-agent attempt
}

FixSpec is a task step's fix: — the agent to invoke when a run misses the step's own success criteria. A bare scalar names the agent (all other fields derived); a mapping allows per-task overrides. It implements yaml.Unmarshaler for the same scalar-or-mapping reason ToolSpec does.

Those criteria are the step's Assert when it declares one and a nonzero exit otherwise — the SAME rule that grades the result, so a repair is never triggered by a different question than the one it is graded on. On a failure the named agent is invoked seeded with that verdict and the captured output, with the parent task auto-injected as a zero-arg rerun tool (its run:, never its fix:, so a rerun can't recurse); after it stops, the task's command is re-run and judged the same way. Fields left unset fall back to the agent's own defaults (Attempts to 1).

func (*FixSpec) UnmarshalYAML

func (f *FixSpec) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML decodes a FixSpec from either a scalar (agent name) or a mapping ({agent, prompt, message_files, dir, tools, attempts, timeout}) YAML node.

type FromLevel

type FromLevel string

FromLevel is how much of a named step's outcome a reader is shown.

const (
	// FromVerdict delivers the verdict name alone. Demands nothing extra of
	// the sender: a step declaring verdicts: already must emit one.
	FromVerdict FromLevel = "verdict"
	// FromNote delivers the verdict and the sender's note, and REQUIRES that
	// note — the verdict tool's note argument stops being optional.
	FromNote FromLevel = "note"
	// FromFull delivers the verdict, the note, and the sender's final response
	// text. Also requires the note.
	FromFull FromLevel = "full"
)

func (FromLevel) RequiresNote

func (l FromLevel) RequiresNote() bool

RequiresNote reports whether demanding this level forces the sender to write a note.

func (FromLevel) Valid

func (l FromLevel) Valid() bool

Valid reports whether l is one of the known levels.

type GitHubCommentsSource

type GitHubCommentsSource struct {
	Repo     string `yaml:"repo"`
	TokenEnv string `yaml:"token_env,omitempty"`
	Endpoint string `yaml:"endpoint,omitempty"`
	// Author is who wrote the comment, compared without regard to case, as
	// GitHub compares logins. "@me" is the token's own user.
	Author string `yaml:"author,omitempty"`
	// Body is an RE2 pattern the comment's text must contain a match for;
	// anchor it with ^ to mean "starts with".
	Body string `yaml:"body,omitempty"`
	// Kinds are which comments to watch: conversation (a pull request's
	// timeline), review (inline, on a line of its diff), issue (an issue's
	// timeline). Default conversation and review.
	Kinds []string `yaml:"kinds,omitempty"`
	// Checkout, true unless set false, lays the tree of the pull request a
	// comment sits on into the artifact. An issue has no tree.
	Checkout *bool `yaml:"checkout,omitempty"`
	// SkipReacted is a reaction that, left by the token's own user, marks a
	// comment done: check does not report it. The mark lives on GitHub, so
	// it outlasts a wiped state database, whose first check would otherwise
	// report a page of commands already answered.
	SkipReacted string `yaml:"skip_reacted,omitempty"`
}

GitHubCommentsSource is a github-comments resource's source:.

func ParseGitHubCommentsSource

func ParseGitHubCommentsSource(raw map[string]any) (GitHubCommentsSource, *regexp.Regexp, error)

ParseGitHubCommentsSource decodes a github-comments source: strictly and checks it, compiling body: so a bad pattern is a load error.

func (GitHubCommentsSource) Connection

func (s GitHubCommentsSource) Connection() GitHubConnection

Connection is where this resource talks to.

func (GitHubCommentsSource) WantsTree

func (s GitHubCommentsSource) WantsTree() bool

WantsTree reports whether a get lays down the pull request's tree.

func (GitHubCommentsSource) WatchedKinds

func (s GitHubCommentsSource) WatchedKinds() []string

WatchedKinds is Kinds with its default filled in.

type GitHubConnection

type GitHubConnection struct {
	Repo     string
	TokenEnv string
	Endpoint string
}

GitHubConnection is where a github-* resource talks to and as whom, common to every source: shape.

func GitHubConnectionOf

func GitHubConnectionOf(raw map[string]any) GitHubConnection

GitHubConnectionOf is where a github-* resource talks to, read from its source: without the rest of the checks — for a caller that has already loaded the pipeline and wants only the token's name.

func (GitHubConnection) API

func (c GitHubConnection) API() string

API is the REST base, without a trailing slash.

func (GitHubConnection) Token

func (c GitHubConnection) Token() string

Token is the variable name the resource reads.

type GitHubPRsSource

type GitHubPRsSource struct {
	Repo     string `yaml:"repo"`
	TokenEnv string `yaml:"token_env,omitempty"`
	Endpoint string `yaml:"endpoint,omitempty"`
	// Author is who opened it. "@me" is the token's own user.
	Author string `yaml:"author,omitempty"`
	// Assignee is someone it is assigned to. "@me" is the token's own user.
	Assignee string `yaml:"assignee,omitempty"`
	// ReviewRequested is someone whose review it directly requests; GitHub
	// clears the request once they submit one. "@me" is the token's own user.
	ReviewRequested string `yaml:"review_requested,omitempty"`
	// Labels must all be on it.
	Labels []string `yaml:"labels,omitempty"`
	// Base is the branch it would merge into.
	Base string `yaml:"base,omitempty"`
	// Draft, when set, keeps only drafts (true) or only ready ones (false).
	Draft *bool `yaml:"draft,omitempty"`
	// Checkout, true unless set false, lays the tree at the version's commit
	// into the artifact beside the metadata files.
	Checkout *bool `yaml:"checkout,omitempty"`
}

GitHubPRsSource is a github-prs resource's source:. Every filter narrows: a pull request is a version only while it is open and matches all of them.

func ParseGitHubPRsSource

func ParseGitHubPRsSource(raw map[string]any) (GitHubPRsSource, error)

ParseGitHubPRsSource decodes a github-prs source: strictly and checks it.

func (GitHubPRsSource) Connection

func (s GitHubPRsSource) Connection() GitHubConnection

Connection is where this resource talks to.

func (GitHubPRsSource) WantsTree

func (s GitHubPRsSource) WantsTree() bool

WantsTree reports whether a get lays down the tree.

type GitHubPostParams

type GitHubPostParams struct {
	// BodyFile is the text to post, a path inside the put's inputs.
	BodyFile string `yaml:"body_file"`
	// From names the get input whose version carries the pull request's
	// number. Needed only when more than one input carries one.
	From string `yaml:"from,omitempty"`
	// Number is the pull request, for a put with no input that names one.
	Number string `yaml:"number,omitempty"`
	// InThread answers the comment a github-comments get fetched where it was
	// written: an inline review comment in its thread, any other kind in the
	// conversation, which has no threads — so a pipeline watching both kinds
	// sets it once. From then names that get.
	InThread bool `yaml:"in_thread,omitempty"`
}

GitHubPostParams is a github-pr-comment put's params:, and the part of a github-pr-review put's params: they share.

func ParseGitHubPostParams

func ParseGitHubPostParams(raw map[string]any) (GitHubPostParams, error)

ParseGitHubPostParams decodes a github-pr-comment put's params:.

type GitHubPostSource

type GitHubPostSource struct {
	Repo     string `yaml:"repo"`
	TokenEnv string `yaml:"token_env,omitempty"`
	Endpoint string `yaml:"endpoint,omitempty"`
}

GitHubPostSource is the source: of every put-only type: where to post, and as whom.

func ParseGitHubPostSource

func ParseGitHubPostSource(raw map[string]any) (GitHubPostSource, error)

ParseGitHubPostSource decodes a put-only type's source: strictly and checks it.

func (GitHubPostSource) Connection

func (s GitHubPostSource) Connection() GitHubConnection

Connection is where this resource talks to.

type GitHubReactionParams

type GitHubReactionParams struct {
	// Add is the reaction to put on the comment.
	Add string `yaml:"add,omitempty"`
	// Remove is a reaction of the token's own user to take off it, after Add
	// is on, so the comment is never without a mark. Anybody else's stays.
	Remove string `yaml:"remove,omitempty"`
	// From names the github-comments get whose comment this marks. Needed
	// only when more than one input carries one.
	From string `yaml:"from,omitempty"`
}

GitHubReactionParams is a github-reaction put's params:. The comment is always a github-comments get's: a reaction marks a command, and only that type's version names one.

func ParseGitHubReactionParams

func ParseGitHubReactionParams(raw map[string]any) (GitHubReactionParams, error)

ParseGitHubReactionParams decodes a github-reaction put's params:.

type GitHubReviewParams

type GitHubReviewParams struct {
	BodyFile string `yaml:"body_file"`
	From     string `yaml:"from,omitempty"`
	Number   string `yaml:"number,omitempty"`
	// Event is comment (the default), approve, request_changes, or pending:
	// a draft only the token's user can see until they submit it, which
	// replaces that user's own earlier draft.
	Event string `yaml:"event,omitempty"`
	// CommentsFile is a JSON array of comments on lines of the diff, posted
	// as part of the same review: [{path, line, body}], with start_line for
	// a range and side LEFT for a removed line. A path inside the put's
	// inputs, like body_file. An empty array posts the body alone.
	CommentsFile string `yaml:"comments_file,omitempty"`
}

GitHubReviewParams is a github-pr-review put's params:.

func ParseGitHubReviewParams

func ParseGitHubReviewParams(raw map[string]any) (GitHubReviewParams, error)

ParseGitHubReviewParams decodes a github-pr-review put's params:.

func (GitHubReviewParams) Post

Post is the part of a review's params: every put shares.

type Hooks

type Hooks struct {
	OnSuccess *Step `yaml:"on_success,omitempty"`
	OnFailure *Step `yaml:"on_failure,omitempty"`
	OnError   *Step `yaml:"on_error,omitempty"`
	OnAbort   *Step `yaml:"on_abort,omitempty"`
	Ensure    *Step `yaml:"ensure,omitempty"`
}

Hooks is the Concourse-style hook set a Step or a Job can carry. Each hook is itself a full Step restricted to task/put/agent kinds (get is rejected at LoadConfig time); a hook may recursively carry its own Hooks. on_success runs after a green outcome, on_failure/on_error/on_abort after the matching failure classification, and ensure always runs last regardless of outcome.

func (Hooks) Each

func (h Hooks) Each(fn func(name string, step *Step) error) error

Each calls fn for every non-nil hook, in a fixed order (on_success, on_failure, on_error, on_abort, ensure), passing the hook's YAML name.

func (Hooks) Empty

func (h Hooks) Empty() bool

Empty reports whether no hook is set.

type InParallel

type InParallel struct {
	// Steps are the branches, each an ordinary step. Empty is a load error:
	// an in_parallel: with nothing in it is a typo, and running zero steps
	// "successfully" is the least useful reading of it.
	Steps []Step `yaml:"steps"`
	// Limit caps how many branches are in flight at once. 0 (the default) is
	// unbounded — the plan author asked for parallelism, so making them also
	// ask for a width would be a second decision for the common case.
	Limit int `yaml:"limit,omitempty"`
	// FailFast, when true, cancels the remaining branches as soon as one
	// fails. A pointer so unset is distinguishable from an explicit false.
	//
	// Either way the block FAILS when a branch does. fail_fast decides
	// whether the siblings are given a chance to finish, never whether the
	// failure counts — a swallowed failure was a real defect in the first
	// implementation of this step, and it is what assert.outcome exists to
	// catch (see docs/control-flow.md).
	FailFast *bool `yaml:"fail_fast,omitempty"`
}

InParallel runs its steps concurrently rather than in sequence.

A plan is otherwise strictly sequential: three independent downloads, or a lint task and a test task, wait on each other for no reason at all, and one slow resource check stalls everything behind it.

func (*InParallel) FailsFast

func (p *InParallel) FailsFast() bool

FailsFast reports whether a branch failure cancels its siblings. Absent means false: letting the others finish is the less surprising default, and it is what makes the failure report complete rather than truncated at whichever branch happened to fail first.

type InputSpec

type InputSpec struct {
	Names []string
	All   bool
}

InputSpec is a step's inputs: declaration. It is a distinct type rather than a plain []string for two reasons: an absent inputs: key (nil *InputSpec) must be distinguishable from an explicit empty list (so ResolveTask can apply a step's inputs: over its tasks: entry only when the step actually declared one), and put steps accept the scalar `inputs: all` — every available artifact — in addition to a sequence of names. Names holds the explicit list; All is set only by the scalar form and is valid on put steps only.

func Inputs

func Inputs(names ...string) *InputSpec

Inputs constructs a declared *InputSpec from an explicit name list — Inputs() declares the empty "starts from nothing" form, Inputs("a", "b") a named list. It exists so callers building a Step programmatically (tests, and any future config synthesis) get the same declared-vs-absent distinction yaml.v3 gives a decoded pipeline.

func (*InputSpec) UnmarshalYAML

func (in *InputSpec) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML decodes an InputSpec from either the scalar `all` or a sequence of artifact names. Any other scalar is rejected here so a typo like `inputs: repo` (meaning `inputs: [repo]`) fails loudly rather than silently parsing as a zero-name declaration.

type Job

type Job struct {
	Name  string `yaml:"name"`
	Plan  []Step `yaml:"plan"`
	Hooks Hooks  `yaml:",inline"`
	// Tags places every step in the plan and every job-level hook on one
	// worker, the way a step's own tags: places that step (see Step.Tags). A
	// do: or in_parallel: block's tags:, a get/put's resource tags:, or the
	// step's own override it — never union with it. Resolved onto each step at
	// load (resolveTags). Never hashed: placement, not content, see Step.Tags.
	Tags []string `yaml:"tags,omitempty"`
	// Assert, on a job, names the ordered set of task/agent/hook names the
	// job's run must have produced (see Assert). A match clears the plan's
	// failure; a mismatch fails the job. Never hashed.
	Assert *Assert `yaml:"assert,omitempty"`
	// Budget caps what every agent step in this job may spend TOGETHER (see
	// Budget). A cumulative ceiling, so the step that trips it is rarely the
	// one that cost the most. Never hashed.
	Budget *Budget `yaml:"budget,omitempty"`
	// Timeout is a wall-clock deadline for the whole job (e.g. "45m"), the
	// same ceiling Budget is in the other unit. Empty means none.
	//
	// Checked BETWEEN steps, never interrupting one. A step keeps its own
	// timeout:, and this decides whether the NEXT one starts — so the two
	// compose instead of racing, and a job never reports a deadline breach for
	// a step that was actually still making progress. The cost is that the job
	// can overrun by at most one step's duration, which is the honest price of
	// not cutting work off mid-flight.
	//
	// It FAILS the job rather than degrading, unlike an across: block's
	// budget:. Same reasoning as the token ceiling here: a job-level limit is a
	// backstop against a run that has gone wrong, and the thing you want then
	// is to stop and say so. Degrading belongs on the block whose width nobody
	// knew when they wrote the pipeline.
	//
	// Never hashed — an operational limit, like every other.
	Timeout string `yaml:"timeout,omitempty"`
	// Serial states that two builds of this job must never run at once —
	// max_in_flight: 1 in Concourse's older spelling. It matters because an
	// unset job is UNLIMITED (see EffectiveMaxInFlight): serial:/serial_groups:
	// force the effective value to 1, and setting max_in_flight: beside either
	// is a load error (checkJobMaxInFlight).
	Serial bool `yaml:"serial,omitempty"`
	// SerialGroups names locks this job holds while it runs. Two jobs sharing
	// a group never run at the same time, which is what stops two different
	// deploy jobs mutating one target concurrently.
	SerialGroups []string `yaml:"serial_groups,omitempty"`
	// MaxConsecutiveFailures holds this job's automatic triggers under `steps web` once it has
	// failed this many triggered RUNS in a row, until someone releases it or a run passes.
	//
	// It counts runs, not the attempts: retries inside one — conflating them
	// would trip the breaker on ordinary flakiness a retry would have
	// absorbed, which is the opposite of the intent. 0 (the default) means no
	// breaker.
	MaxConsecutiveFailures int `yaml:"max_consecutive_failures,omitempty"`
	// MaxInFlight caps how many builds of this job run at once under
	// `steps web`. Mirrors Concourse's job-level max_in_flight
	// (concourse-ci.org/docs/jobs/), including that serial:/serial_groups:
	// take precedence by forcing the effective value to 1.
	//
	// 0/unset is unlimited, as in Concourse — bounded in practice by
	// `steps web --max-concurrent`, which is the worker pool this runner
	// has instead of Concourse's workers.
	//
	// The same word appears on an across: step meaning cell concurrency.
	// That is Concourse's own overload, not one invented here, and the two
	// live on different things: a job field and a step field.
	MaxInFlight int `yaml:"max_in_flight,omitempty"`
	// Concourse's interruptible: and its default of false: a `steps web` shutdown waits (bounded) for this job's running build, because a deploy half-applied by a daemon restart is the case it exists for, while `steps run` is a person at a terminal and ctrl-C there means now (internal/scheduler's LocalRunner.runContext).
	Interruptible bool `yaml:"interruptible,omitempty"`
	// Line is the job's source line in the pipeline file, filled in after
	// decoding (see stampLines). Never written in YAML and never hashed.
	Line int `yaml:"-"`
}

Job is a named sequence of steps to run.

func (Job) AgentNames

func (j Job) AgentNames() []string

AgentNames lists every agent this job's plan invokes — plan steps, hook steps, any task's fix: agent, and any sub-agent granted inline on a step — in plan order, deduplicated.

It exists for preflight, whose whole point is to check only what THIS job needs: a pipeline with ten agents whose job uses two must probe two. Sub-agents granted on an agents: entry rather than inline are expanded by the caller, which has the Config needed to resolve them.

func (Job) EffectiveMaxInFlight

func (j Job) EffectiveMaxInFlight() int

EffectiveMaxInFlight is how many builds of this job may run at once, after serial:/serial_groups: have had their say.

func (Job) GetResourceNames

func (j Job) GetResourceNames() []string

GetResourceNames lists every resource this job's plan or hooks fetch, in first-appearance order and without repeats, resolving aliases the way GetResourceName does.

Deduped because two gets of one resource are two artifacts of the same thing: a caller asking what to load for this job wants each answered once.

func (Job) GetsResource

func (j Job) GetsResource(resource string) bool

GetsResource reports whether this job's plan or hooks fetch the named resource, resolving aliases the way GetResourceName does.

Preflight needs it to judge only the stages a job actually reaches: the check:/in: tools are a get's concern, and a job that merely publishes to a resource must not be blocked from running because that resource's check tool is missing or its source does not satisfy it.

func (Job) Inputs

func (j Job) Inputs() []JobInput

Inputs is every resource a job gets, in plan order, hooks included — the same walk as Concourse's JobConfig.Inputs. Concourse lists each get step; this folds gets of one resource together (trigger if any triggers, passed unioned), since the graph draws one edge per resource and job either way.

func (Job) Outputs

func (j Job) Outputs() []string

Outputs is every resource a job puts, in plan order and once each, hooks included — a put in on_failure is still something the job can change, and Concourse's JobConfig.Outputs counts it too.

func (Job) PassedConstraints

func (j Job) PassedConstraints() map[string][]string

PassedConstraints returns every (resource, upstream jobs) pair a job's plan declares, so the trigger can ask whether a version is green everywhere it needs to be before enqueueing.

func (Job) PutSteps

func (j Job) PutSteps(resource string) []Step

PutSteps returns every step in this job's plan and hooks that publishes to the named resource.

Preflight needs it to answer a question that is a property of the STEP, not of the resource: an mcp out: with no args: sends the put's own params: as the tool's arguments, so whether the tool's required arguments will be there depends on which put is asking. Mirrors StepsForAgent.

Job-scoped is the useful scope for a `steps run`: whether THIS job can work cannot depend on how some other job spells its own put to a resource this one only gets. Config.PutSteps is the whole-pipeline form, for the daemon, which is going to run every job.

func (Job) ResourceGets

func (j Job) ResourceGets(resource string) []Step

ResourceGets returns every get step in this job's plan and hooks that fetches the named resource, resolving aliases the way GetResourceName does.

func (Job) ResourceNames

func (j Job) ResourceNames() []string

ResourceNames lists every resource this job's plan touches — the resource a get fetches and the resource a put publishes to (resolving resource:, so an aliased step names the real one) — across plan steps and hooks alike, in plan order, deduplicated.

The mirror of AgentNames, and it exists for the same reason: preflight checks only what THIS job needs. A pipeline with ten resources whose job touches two connects to two.

func (Job) TriggersOn

func (j Job) TriggersOn(resourceName string) bool

TriggersOn reports whether this job has a trigger:true get step resolving to resourceName, walking the same nested tree PolledResourceNames does (a trigger:true get is legal — just not version:every-eligible — inside an in_parallel:/race: branch).

Exported because internal/trigger.AffectedJobs needs it from outside this package (visitSteps is unexported): AffectedJobs used to scan job.Plan flatly, which was symmetric with the old Resources() before it started delegating to PolledResourceNames — once the poller started reaching a nested trigger:true get, a flat AffectedJobs stopped being able to find the job for it, so the resource's baseline in resource_checks would advance on every poll while the job it was meant to trigger was never enqueued.

func (Job) VisitSteps

func (j Job) VisitSteps(fn func(label string, step *Step) error) error

VisitSteps exposes the step walk to callers outside this package, so a rule enforced elsewhere covers hooks, try: bodies and concurrent branches exactly as a rule written here does. Exported for the placement check, which needs the step tree but reads its answer from the invocation rather than from the pipeline.

type JobInput

type JobInput struct {
	Resource string
	Trigger  bool
	Passed   []string
}

JobInput is one resource a job's plan gets, as the pipeline graph draws it: whether any get of it triggers the job, and every job it must have passed.

type MCPExport

type MCPExport struct {
	// Name is the export's address under the pipeline: /mcp/x/<pipeline>/<name>.
	Name string `yaml:"name"`
	// Access is who may connect, a rule over the signed-in person, required:
	// an export with no rule would be one anybody signed in could use.
	Access string `yaml:"access"`
	// Tools are what a connected agent may call.
	Tools []ToolSpec `yaml:"tools"`
	// Rules names rules: entries that check every call of every tool in the export, before the rules of the tool itself.
	Rules []string `yaml:"rules,omitempty"`
	// Rate bounds calls per person, as "<n>/<s|m|h>"; empty is unbounded.
	Rate string `yaml:"rate,omitempty"`
	// Image runs the export's custom run: commands in a container from this image, a fresh one per command; empty runs them on the daemon's machine.
	Image string `yaml:"image,omitempty"`
	// User, Network, Privileged and Limits are the container's, as on a task, and need Image.
	User       string           `yaml:"user,omitempty"`
	Network    string           `yaml:"network,omitempty"`
	Privileged bool             `yaml:"privileged,omitempty"`
	Limits     *ContainerLimits `yaml:"container_limits,omitempty"`
}

MCPExport is a named bundle of this pipeline's tools that an agent somewhere else can connect to through the daemon, as an MCP server, under the name of a person who signed in. The tools run with the pipeline's own credentials and the caller never sees them; what narrows a tool — a fixed argument, a replacement description, a time limit — is the same ToolSpec an agent's grant uses, so a pin written for an agent means the same here.

MCP grants, sub-agents and custom run: tools may be exported; a built-in has no workspace to work in. A custom tool runs its command with the caller's values quoted into it (an unquoted one is refused, see internal/agent), in a directory made for that one call, on the host or in the container the export names. Never hashed: an export changes nothing a build does.

type MCPReadiness

type MCPReadiness int

MCPReadiness is what is knowable about a server with no request made, and the reason a status cell says what it says.

const (
	// MCPReady is every static condition met: a stdio command found on PATH, a bearer variable set, or an unauthenticated endpoint, which has nothing to check.
	MCPReady MCPReadiness = iota
	// MCPMissing is a static condition that FAILS, and that waiting will not fix: the command is not on PATH, or the variable is not set.
	MCPMissing
	// MCPUnknown is a server whose readiness this package cannot answer — an oauth server's credential is a login internal/mcp keeps, and a stdio server with a relative cwd: resolves per step.
	MCPUnknown
)

type MCPResourceConfig

type MCPResourceConfig struct {
	Server string       `yaml:"server"`
	Check  *MCPToolCall `yaml:"check,omitempty"`
	In     *MCPToolCall `yaml:"in,omitempty"`
	Out    *MCPToolCall `yaml:"out,omitempty"`
}

MCPResourceConfig backs a resource type's check/in/out with calls to a named mcp_servers: entry instead of shell commands. Check is required (a type with no way to discover versions is useless); In and Out are optional — see resource.CheckVersions/RunIn/RunOut's mcp*/ branches for exactly what arguments each call receives and how its result is used.

type MCPServer

type MCPServer struct {
	Name     string   `yaml:"name"`
	Endpoint string   `yaml:"endpoint,omitempty"`
	Command  string   `yaml:"command,omitempty"`
	Args     []string `yaml:"args,omitempty"`
	// Cwd is the working directory a stdio server's subprocess is spawned
	// in. An ABSOLUTE path is used verbatim — a fixed location on the host,
	// resolved identically for every step. A RELATIVE path is resolved
	// against the agent step's own working directory (see
	// WithResolvedMCPCwd), which is what lets a server be pointed at an
	// input artifact — `cwd: repo` for a language server that must index the
	// same materialized tree the agent's file tools read. Empty inherits the
	// steps process's own cwd.
	//
	// Relative only makes sense where a step workspace exists, so it is
	// rejected for a server backing a resource type's mcp: config — a
	// check/in/out runs with no agent step to resolve against.
	Cwd  string        `yaml:"cwd,omitempty"`
	Auth MCPServerAuth `yaml:"auth,omitempty"`
}

MCPServer is a reusable, named MCP server connection: configured once under mcp_servers: and shared across any number of agents: tool grants and resource_types: mcp: backends — the same once-configured/many- consumers idiom as Agent/Resource. Two transports are supported: HTTP (Streamable HTTP, via Endpoint) or stdio (a local subprocess, via Command/ Args/Cwd) — exactly one of Endpoint/Command is set (see validateMCPServerTransport). A stdio server has no request to attach credentials to, so Auth must be unset ("none") when Command is set.

func (MCPServer) AuthLabel

func (s MCPServer) AuthLabel() string

AuthLabel names the auth type and, for bearer, the environment variable the credential is read from — the thing to go check when it is the credential that is missing. Never the value.

func (MCPServer) IsStdio

func (s MCPServer) IsStdio() bool

IsStdio reports whether srv is a stdio (local subprocess) server rather than an HTTP one. Exactly one of Endpoint/Command is set — see validateMCPServerTransport.

func (MCPServer) NotProbableHere

func (s MCPServer) NotProbableHere() string

NotProbableHere reports the status for a server that cannot honestly be probed from outside a run, or "" for one that can. A relative cwd: is resolved against the working directory of the agent step whose tools are being built (WithResolvedMCPCwd) — a build workspace that exists only during a run, so spawning it from wherever the operator happens to be would chdir somewhere else entirely and report a server that works perfectly in a run as broken.

func (MCPServer) StaticStatus

func (s MCPServer) StaticStatus() MCPStatus

StaticStatus answers what this machine can say about the server right now without a single request: whether a stdio command is on PATH, and whether a bearer variable is set. The oauth answer is deliberately MCPUnknown rather than a guess — the credential is a login internal/mcp keeps, sealed in the state database, and this package may not import it. Whoever holds the token fills that in; see web.Authorizer.

func (MCPServer) Target

func (s MCPServer) Target() string

Target renders what the server actually is, for a reader: the endpoint for HTTP, the argv (plus any pinned working directory) for stdio. URL credentials are masked, because argv pasted from vendor docs carries tokens as userinfo and this string is drawn on a page every viewer of the daemon can read; nothing spawns from it.

func (MCPServer) Transport

func (s MCPServer) Transport() string

Transport names which of the two shapes this server is.

type MCPServerAuth

type MCPServerAuth struct {
	Type            string   `yaml:"type"`
	APIKeyEnv       string   `yaml:"api_key_env,omitempty"`
	ClientID        string   `yaml:"client_id,omitempty"`
	ClientSecretEnv string   `yaml:"client_secret_env,omitempty"`
	CallbackPort    int      `yaml:"callback_port,omitempty"`
	Scopes          []string `yaml:"scopes,omitempty"`
}

MCPServerAuth selects how steps authenticates to an MCP server. Type is "none" (default, when Auth is omitted entirely), "bearer" (a static token read from an OS environment variable named by APIKeyEnv — mirrors AgentSource.APIKeyEnv exactly: the credential is never stored in YAML), or "oauth" (interactive authorization-code + PKCE via `steps mcp login`, with silent refresh at run time — see internal/mcp).

ClientID/ClientSecretEnv are the oauth-only escape hatch for an authorization server that does not offer dynamic client registration — notably Slack's, which requires clients to be backed by a pre-registered app with a fixed ID. Setting ClientID makes `steps mcp login` skip registration and use those credentials directly; everything downstream (PKCE, code exchange, silent refresh) is unchanged. A client ID is a public application identifier, so it lives in YAML like an endpoint does; the SECRET follows the api_key_env convention and is only ever named. CallbackPort pins the loopback port `steps mcp login` listens on for the browser redirect, and exists for the same authorization servers ClientID does. `steps mcp login` otherwise takes an ephemeral port, which is what RFC 8252 §7.3 asks servers to allow for native apps — but the MCP authorization spec requires the opposite ("authorization servers MUST validate exact redirect URIs against pre-registered values"), and the providers that need a pre-registered client are generally the ones enforcing it. Against those, an ephemeral port cannot match whatever redirect URI was registered in a dashboard, so login fails no matter how the client was configured. Setting this makes the redirect URI predictable, so `http://127.0.0.1:<port>/callback` can be registered once. Never hashed: its only reader is `steps mcp login`, so no step ever runs with it.

type MCPStatus

type MCPStatus struct {
	Readiness MCPReadiness
	// Detail is the whole cell, written for somebody who has to act on it — `$GITHUB_PAT is not set`, not `false`.
	Detail string
}

MCPStatus is one server's static status: whether it is usable, and the phrase both front ends print for it.

type MCPToolCall

type MCPToolCall struct {
	Tool string         `yaml:"tool"`
	Args map[string]any `yaml:"args,omitempty"`
}

MCPToolCall names the remote tool a resource-type lifecycle stage calls, and (optionally) shapes the arguments it is called with.

Args is the argument object sent to Tool. Every string leaf in it is a template rendered over exactly the data the shell path's command for that same stage gets — {source} for check, {source, version, params} for in, {source, params} for out — so a value the remote tool requires can be lifted out of whichever of them holds it. Naming one a stage does not have (a {{ .version }} in check:) fails the step rather than rendering empty:

in:
  tool: slack_read_thread
  args:
    channel_id: "{{ .version.channel }}"
    message_ts: "{{ .version.ts }}"

This exists because an MCP tool's arguments are its own published schema, not ours: `slack_read_thread` requires `channel_id`/`message_ts` at the top level and will reject anything else. Before args:, steps sent a fixed envelope ({"source": source}, {"source", "version"}, {"source", "params"}), which no third-party server has ever declared a parameter for — so the mcp: backend could only ever talk to a server written against steps' envelope. Naming the mapping is what makes an off-the-shelf server usable.

When Args is omitted the arguments are the stage's natural payload, sent verbatim: source for check and in, params for out. That covers the common case where the resource's source: IS the tool's argument object (a `check` whose source is `{query: ...}`), and it is why `in:` needs args: whenever its tool wants fields that live on the VERSION rather than the source.

type ParamSpec

type ParamSpec struct {
	// MaxLength and MinLength count characters, not bytes. String parameters only.
	MaxLength *int `yaml:"max_length,omitempty"`
	MinLength *int `yaml:"min_length,omitempty"`
	// Pattern is a Go (RE2) regular expression the value must match somewhere; anchor it with ^ and $ to match all of it. String parameters only.
	Pattern string `yaml:"pattern,omitempty"`
	// Enum lists every value the parameter may take.
	Enum []string `yaml:"enum,omitempty"`
	// Min and Max bound a number. Number parameters only.
	Min *float64 `yaml:"min,omitempty"`
	Max *float64 `yaml:"max,omitempty"`
	// Default fills the argument when it is missing, which also makes it optional.
	Default *string `yaml:"default,omitempty"`
}

ParamSpec limits what one argument of a tool may be. The limits are published in the tool's input schema, so a model sees them and corrects itself before calling, and are enforced before the call is made, so a caller that ignores the schema is refused all the same.

func (ParamSpec) CheckNumber

func (p ParamSpec) CheckNumber(value float64) string

CheckNumber is why value is not an acceptable number for p, or "".

func (ParamSpec) CheckString

func (p ParamSpec) CheckString(value string) string

CheckString is why value is not an acceptable string for p, or "".

func (ParamSpec) NumberLimits

func (p ParamSpec) NumberLimits() bool

NumberLimits reports whether p sets a limit only a number has.

func (ParamSpec) StringLimits

func (p ParamSpec) StringLimits() bool

StringLimits reports whether p sets a limit only a string has.

type Preflight

type Preflight struct {
	// Disabled turns the check off entirely. Spelled as the negative so the
	// zero value — an absent preflight: block — is ON, which is the useful
	// default: the check exists because nobody thinks to enable it until
	// after it would have saved them.
	Disabled bool `yaml:"disabled,omitempty"`
	// Timeout bounds one check. A model that has not answered in this long is
	// reported as unavailable rather than waited on.
	Timeout string `yaml:"timeout,omitempty"`
	// Cache is how long a verified target is trusted for. It is a real
	// requirement, not an afterthought: without it every `steps web` poll
	// pays for a probe request against every model in the pipeline.
	Cache string `yaml:"cache,omitempty"`
}

Preflight tunes the pre-run health check that proves a job's models and MCP servers work before any of its steps run. See internal/agent's Preflight.

func (*Preflight) CacheWindow

func (p *Preflight) CacheWindow() time.Duration

CacheWindow is how long a verified target stays trusted.

func (*Preflight) Enabled

func (p *Preflight) Enabled() bool

Enabled reports whether preflight runs at all. A nil Preflight (no defaults: block) is enabled.

func (*Preflight) ProbeTimeout

func (p *Preflight) ProbeTimeout() time.Duration

ProbeTimeout is the per-check deadline.

type Problem

type Problem struct {
	// Target names what is wrong, e.g. `agent "coder"` or `mcp "gopls"`.
	Target string
	// Detail says what about it is wrong, and where possible how to fix it.
	Detail string
	// Transient marks a problem that WAITING could fix — a server that did
	// not answer, a token that needs refreshing — as opposed to one no amount
	// of time will change, like a tool the server does not expose or required
	// arguments the pipeline never sends.
	//
	// It exists because the same problem deserves opposite reactions from the
	// two callers. `steps run` refuses either way: a person is waiting, and
	// starting a run that cannot finish wastes their time and money. `steps
	// web` is a daemon, and quitting because a VPN was not up yet — or
	// because a token needed the refresh the next poll would have done — is
	// how a daemon that should have recovered on its own ends up dead all
	// weekend.
	Transient bool
}

Problem is one reason a pipeline cannot run here, with the target it concerns so a report can be read target-by-target rather than as prose.

func (Problem) Error

func (p Problem) Error() string

type Race

type Race struct {
	// Steps are the racers. Fewer than two is a load error: a race with one
	// runner is a step with extra words, and a race with none is a typo.
	Steps []Step `yaml:"steps"`
}

Race runs its branches concurrently and keeps whichever completes successfully first, cancelling the rest.

⚠️ This is a LATENCY hedge, not a cost saver, and that is the single most important thing to know about it. Running both branches always costs both, every time, even when the fast one wins. The value is "never wait for the slow path", not "spend less" — anyone reaching for race: to save money wants caching or plain retries instead.

type ResolvedInvocation

type ResolvedInvocation struct {
	AgentName   string
	Description string
	BaseURL     string
	ModelName   string
	APIKeyEnv   string
	RequiresKey bool
	// CLI, when non-empty, names the coding-agent CLI that runs this
	// invocation as a subprocess instead of an HTTP conversation (see
	// cliagent.go). BaseURL is empty in that case, and ModelName is the model
	// the CLI is asked for (e.g. "sonnet"). Hashed as `cli`, so moving an
	// agent between a CLI and a hosted provider invalidates its cache.
	CLI string
	// CLISettings is the CLI configuration scope the subprocess loads (see
	// Agent.Settings): "" loads none, CLISettingsProject loads the repo's
	// checked-in .claude/ scope. Hashed as `cli_settings` — repo config
	// shaping the agent is part of what the step asked for.
	CLISettings string
	Persona     string
	// ContextPaths mirrors Step.ContextPaths once resolved — populated from
	// the step, not the agent definition, since concrete input paths are
	// only known at the step level.
	ContextPaths []string
	// MaxContextBytes is the resolved per-file cap for ContextPaths (see
	// Agent.MaxContextBytes). 0 means no cap — an author asked for it, since
	// resolution substitutes the default for an unset field.
	MaxContextBytes int
	// Generation dials, mirroring Agent's own fields once resolved. Kept flat
	// here (rather than a nested type) so this package doesn't need to depend
	// on anything LLM-client-specific — internal/agent assembles its own
	// request-config shape from these.
	Temperature     *float64
	TopP            *float64
	MaxTokens       int
	ReasoningEffort string // "", "low", "medium", or "high"
	// MaxTurns is the resolved tool-calling cap; 0 means no cap, which an
	// explicit max_turns: 0 produces and so does an unset cap on a cli source
	// (see defaultTurnsFor).
	MaxTurns int
	// TurnsDefaulted records that neither the step nor the agent named a turn
	// cap, so MaxTurns is this package's answer rather than the author's.
	//
	// Kept because the default is the one dial whose value depends on the
	// SOURCE KIND, and a failover crosses that boundary: WithSource has to
	// re-derive it, and can only tell "the author chose 0" from "nobody chose"
	// if resolution says which happened.
	TurnsDefaulted bool
	// MaxQuestions is the resolved ask_user budget; 0 means no cap, which
	// only an explicit max_questions: 0 produces.
	MaxQuestions int
	Attempts     int
	// Timeout is the resolved wall-clock deadline per attempt. Empty means
	// "take internal/agent's default"; "0" means no deadline. Unlike every
	// other step kind, an agent step's empty is NOT "no deadline" — see
	// Step.Timeout.
	Timeout string
	// CompactAfterTokens is the already-resolved conversation-size budget (see
	// Agent.CompactAfterTokens); 0 means compaction is disabled for this
	// invocation.
	CompactAfterTokens int
	// ContextWindow is the model's context window when this package
	// recognizes the model (see contextWindows), 0 when it does not. It is
	// what CompactAfterTokens was derived from, carried so a run can SAY
	// "compacting at 102400 against a 1000000 window" — the mismatch that was
	// previously invisible until compaction stalled.
	ContextWindow int
	// BudgetTokens is the ceiling on provider-reported tokens this one
	// invocation may spend (see Agent.Budget); 0 means no ceiling. Never
	// hashed — it is an operational limit, like Timeout.
	BudgetTokens int
	// BudgetUSD is the dollar ceiling a CLI agent hands its subprocess (see
	// Budget.USD); 0 means no ceiling. Never hashed, for the same reason
	// BudgetTokens is not.
	BudgetUSD float64
	ToolSpecs []ToolSpec
	// StringOnlyToolChoice, when true, forces a required tool call (see
	// forceRequiredTool in internal/agent) via tool_choice: "required"
	// instead of a named function object — for providers whose
	// OpenAI-compat server rejects the object form. See resolveAgentTarget.
	StringOnlyToolChoice bool
	// Env names the host environment variables this step's commands may see
	// beyond the baseline. See Agent.Env/Step.Env.
	Env []string
	// User is the container user this step's commands run as. See
	// Agent.User/Step.User.
	User string
	// Network is the container network this step's commands join. See
	// Agent.Network/Step.Network.
	Network string
	// Privileged runs this step's container with --privileged. See
	// Agent.Privileged/Step.Privileged.
	Privileged bool
	// Limits caps this step's container CPU/memory. See
	// Agent.Limits/Step.Limits.
	Limits *ContainerLimits
	// Sandbox fences this agent's run_shell/custom tools on the host. From the agents: entry only; see Agent.Sandbox.
	Sandbox bool
	// Image, when non-empty, runs this step's run_shell/custom-tool commands
	// in a container from this image instead of on the host. See
	// Agent.Image/Step.Image.
	Image string
	// Caches are the directories this step's commands keep between runs. See TaskCache.
	Caches []TaskCache
}

ResolvedInvocation is an agent + step reduced to everything needed to hash and run the step: the resolved connection, persona, dials, limits, and the effective (merged) tool set. ResolveAgentInvocation produces it for both planning (merkle hashing) and execution, so both compute identical hashes.

func (ResolvedInvocation) WithSource

func (ri ResolvedInvocation) WithSource(source AgentSource, agent *Agent) (ResolvedInvocation, error)

WithSource returns a copy of ri reaching a different endpoint/model/ credential, leaving everything else — persona, dials, limits, tool grant — exactly as resolved.

It backs agent failover: an outage changes where a step's requests GO, and must change nothing about what the agent is or is allowed to do. It also keeps the failover path out of hashing: the caller hashes the primary invocation and runs this one, so which source actually served a run is availability, not content, and a fallback firing on one run cannot invalidate a cache entry. It takes the whole agent, not just its compact_after_tokens:, because the compaction budget is now derived from two of the agent's own fields and a second positional override would be one more blank to count at every call site — the same argument agentTarget's doc comment makes. A nil agent means "no explicit overrides", which is what the two nil-able knobs it reads already mean individually.

type ResolvedTask

type ResolvedTask struct {
	Name    string
	Run     string
	Fix     *FixSpec
	Inputs  []string
	Outputs []string
	// InputMapping/OutputMapping rename declared inputs/outputs onto plan-
	// artifact names under workspace: isolation (see Step.InputMapping). Always
	// from the step (tasks: entries carry no mapping); empty leaves names
	// unmapped.
	InputMapping  map[string]string
	OutputMapping map[string]string
	// Image, when non-empty, runs this task's run: (and any fix-loop
	// re-runs) in a container from this image instead of on the host. See
	// Task.Image/Step.Image.
	Image string
	// Env names the host environment variables this task's commands may see
	// beyond the baseline. See Task.Env/Step.Env.
	Env []string
	// User is the container user this task's commands run as. See
	// Task.User/Step.User.
	User string
	// Network is the container network this task's commands join. See
	// Task.Network/Step.Network.
	Network string
	// Privileged runs this task's container with --privileged. See
	// Task.Privileged/Step.Privileged.
	Privileged bool
	// Limits caps this task's container CPU/memory. See
	// Task.Limits/Step.Limits.
	Limits *ContainerLimits
	// Sandbox fences this task's host commands. Always the tasks: entry's: a step has no spelling for it, and an inline run: has no entry. See Task.Sandbox.
	Sandbox bool
	// Timeout is a wall-clock deadline per attempt (e.g., "2m", "30s"). Empty
	// means no timeout. Step.Timeout overrides Task.Timeout when set.
	Timeout string
	// Assert, when set, checks the task's captured stdout/exit code (see
	// Assert). It always comes from the step (top-level tasks: entries carry
	// no assert), so a matching assert makes a non-zero-exit task a success.
	Assert *Assert
	// Caches are the directories this task keeps between runs. See TaskCache.
	Caches []TaskCache
}

ResolvedTask is a task step's run/fix, resolved against either the step's own inline fields or a tasks: entry it references by name. Both the merkle planner and the executor call ResolveTask so plan-time hashing and run-time execution stay in lockstep.

type Resource

type Resource struct {
	Name   string         `yaml:"name"`
	Type   string         `yaml:"type"`
	Source map[string]any `yaml:"source"`
	// Env names host environment variables THIS resource's check/in/out may
	// see, ON TOP OF whatever its resource type's own env: already allows —
	// the two lists union at run time, rather than one replacing the other.
	//
	// It exists for the case a type's own env: cannot cover: a resource type
	// is shared by every resource of that type in the pipeline, so its env:
	// names ONE fixed credential variable — but two resources of the same
	// type may need two DIFFERENT-named tokens (two Slack bots in one
	// pipeline), or an operator's secret may already be named something
	// other than the type's documented default. Widening the type's own
	// env: would grant every resource of that type the new name; this grants
	// only the one resource that asked for it, so the type's declared
	// allow-list stays the complete picture of what a shared, possibly
	// third-party type can read.
	//
	// Names only — see validateEnvValues. Meaningful only for an expr:- or
	// shell-backed type; an mcp-backed type authenticates via its
	// mcp_servers: entry and never consults env: at all.
	Env []string `yaml:"env,omitempty"`
	// Tags names the worker this resource's check, in and out run on, for a
	// source only reachable from that machine's network. A get or put step's
	// own tags: overrides it for that step, and it overrides a tag the step
	// would inherit from a job or block; empty runs them here.
	//
	// A deliberate divergence from Concourse, whose resource tags place only
	// the check and must be repeated on every get and put: a get's own
	// version check and its in: have to land on the same machine, and the
	// repetition is the papercut Concourse's docs warn about. Same shape as a
	// step's tags: — one entry, not hashed (see Step.Tags).
	Tags []string `yaml:"tags,omitempty"`
}

Resource is a named instance of a resource type, configured with a source.

func (Resource) CronSource

func (r Resource) CronSource() (CronSource, error)

CronSource decodes this resource's source: strictly, so a misspelled key is a load error rather than a schedule nobody reads.

func (Resource) WebhookSource

func (r Resource) WebhookSource() (WebhookSource, error)

WebhookSource decodes this resource's source: strictly, so a misspelled key is a load error rather than a filter nobody applies.

type ResourceBackend

type ResourceBackend string

ResourceBackend is which way a resource type implements its three lifecycle stages. A type picks exactly one (see validateOneResourceTypeConfig).

const (
	BackendShell ResourceBackend = "shell"
	BackendMCP   ResourceBackend = "mcp"
	BackendExpr  ResourceBackend = "expr"
	// BackendWebhook has no check and no out: each delivery the daemon receives IS a version, and in writes out what it carried.
	BackendWebhook ResourceBackend = "webhook"
	// BackendCron has a check that reads the clock and no out: a version is a moment its expression named.
	BackendCron ResourceBackend = "cron"
	// BackendGitHub calls GitHub's API from this process: two of its types find work and have no out, three publish and have no check (see github.go).
	BackendGitHub ResourceBackend = "github"
)

The backends. These exist so every dispatch over them can be a TAGGED switch, which golangci-lint's `exhaustive` checks — adding a backend then fails the build at each site that has not answered for it, rather than leaving one to silently take a default arm.

config.Step reaches the same place through Step.Kind(), with tools/kindswitch refusing the tagless spelling (`switch { case s.Put != "": }`) that exhaustive cannot see.

type ResourceType

type ResourceType struct {
	Name string `yaml:"name"`
	// Image, when set, runs check/in/out in a fresh `docker run --rm`
	// container from this image instead of on the host. Empty (the default)
	// keeps host execution, byte-identical to before this field existed.
	Image string `yaml:"image,omitempty"`
	// Env names host environment variables check/in/out are allowed to see,
	// on top of the always-allowed baseline (see shell.HostEnv). Names only —
	// see validateEnvValues. This is how a resource type reaches a registry
	// credential or deploy token without it being written into the pipeline.
	Env []string `yaml:"env,omitempty"`
	// User is the container user check/in/out execute as (docker's --user).
	// Empty takes the platform default — see shell's defaultContainerUser.
	User string `yaml:"user,omitempty"`
	// Network is the container network check/in/out join (docker's
	// --network); "none" cuts off egress entirely. Requires Image (except
	// "none" beside Sandbox) — and note
	// that most resource types exist to reach the network, so this is rarely
	// what you want on one.
	Network string `yaml:"network,omitempty"`
	// Privileged runs this command's container with `docker run --privileged`.
	// Mirrors Concourse's privileged: (concourse-ci.org/docs/steps/task/).
	// Container-only, like Network — a host command has nothing to elevate,
	// so it is a load error without image:.
	Privileged bool `yaml:"privileged,omitempty"`
	// Limits caps the container's CPU and memory. Mirrors Concourse's
	// container_limits:; container-only for the same reason Privileged is.
	Limits *ContainerLimits `yaml:"container_limits,omitempty"`
	// Sandbox fences this entity's host commands with the OS sandbox: writes only under the step's workspace and a private TMPDIR, credential directories unreadable, and network: none becomes legal on the host. See internal/config/sandbox.go for the rules and docs/infra.md for the policy.
	Sandbox bool               `yaml:"sandbox,omitempty"`
	Config  ResourceTypeConfig `yaml:"config"`
}

ResourceType defines a resource kind as a set of shell command templates. Its Name is never hashed: a step is keyed by what the type DOES — the rendered template, image, env, user, network, limits — so renaming a type re-runs nothing, and two types that do the same thing share a cache entry, which is what content-addressing means.

func ReadBuiltinResourceType

func ReadBuiltinResourceType(name string) (ResourceType, error)

ReadBuiltinResourceType returns the parsed YAML of a named built-in resource type. name is like "git" — resolves to "resource_types/<name>.yml".

type ResourceTypeConfig

type ResourceTypeConfig struct {
	Check string              `yaml:"check,omitempty"`
	In    string              `yaml:"in,omitempty"`
	Out   string              `yaml:"out,omitempty"`
	MCP   *MCPResourceConfig  `yaml:"mcp,omitempty"`
	Expr  *ExprResourceConfig `yaml:"expr,omitempty"`
	// Webhook marks the built-in webhook type, which no YAML can declare: its versions arrive as deliveries rather than from a check.
	Webhook bool `yaml:"-"`
	// Cron marks the built-in cron type, which no YAML can declare either: its check reads a clock against an expression, and runs nothing.
	Cron bool `yaml:"-"`
	// GitHub names which built-in github-* type this is (see github.go), which no YAML can declare: steps calls the API itself. Keyed, unlike Cron and Webhook, because several share a source: shape and do different things with it.
	GitHub string `yaml:"-"`
}

ResourceTypeConfig holds the check/in/out shell command templates. Templates may reference {{ source.x }} and (for in/out) {{ version.y }}.

MCP, when set, is mutually exclusive with Check/In/Out: this resource type's check/in/out are calls to a configured mcp_servers: entry instead of shell commands (see MCPResourceConfig, validateResourceTypeConfig).

func (ResourceTypeConfig) Backend

func (c ResourceTypeConfig) Backend() ResourceBackend

Backend reports how this resource type is implemented.

Shell is the default rather than a detected state: a type with an empty config: is shell-backed with nothing to run, which is a type that can neither detect nor fetch nor publish, and the errors for that are stage rules (validateResourceGet, validateResourcePut) rather than a fourth backend meaning "none".

type Revision

type Revision struct {
	SHA    string
	Source string
	// Includes is path→content, the path exactly as the pipeline names it: the string is hashed, so a resolved path made ci/app.yml and /repo/ci/app.yml two configurations of the same bytes.
	Includes map[string]string
}

Revision is one configuration, as parsed: the substituted source, the include contents it folded in, and the hash over both.

func (Revision) Recorded

func (r Revision) Recorded() bool

Recorded reports whether this Config was loaded from a source (rather than built in a test), which is what makes its revision worth writing down.

type Rule

type Rule struct {
	// Name is how an export or a tool refers to the rule, and what a refusal and the activity log call it.
	Name string `yaml:"name"`
	// When is an expression over the call — tool, args (after pins and defaults) and who — that must hold for the call to go out. fail("why") refuses with a reason; a plain false says "refused by rule <name>".
	When string `yaml:"when,omitempty"`
	// Accept is the same over what came back, with result (text, content, error) in scope as well, and must hold for the caller to see it.
	Accept string `yaml:"accept,omitempty"`
	// JudgedBy names an agents: entry that is shown the call and answers allow or deny. A judge that fails or answers neither refuses the call.
	JudgedBy string `yaml:"judged_by,omitempty"`
}

Rule is a named check on a call to a tool, written once and attached by name to an export, a tool of an export, or a tool an agent is granted. It can refuse a call before it goes out (When), refuse what comes back before the caller sees it (Accept), and hand the decision to one of the pipeline's own agents (JudgedBy). A refused call tells the caller why and is recorded under the rule's name.

type Step

type Step struct {
	Get     string `kind:"get"               yaml:"get,omitempty"`
	Trigger bool   `yaml:"trigger,omitempty"`
	// Version selects which version(s) a get step fetches: unset/"latest"
	// (default) picks the single latest version; "every" runs the rest of
	// the plan once per version returned by check; a map pins to a specific
	// version. Mirrors Concourse's get.version field, including that a
	// failed version's build does not stop "every" from attempting the
	// remaining versions — see docs/conformance.md and
	// internal/pipeline/conformance_test.go's
	// TestConformanceGetVersionEveryContinuesPastFailure.
	Version any `yaml:"version,omitempty"`
	// Task labels a task step. If Run is also set, the step is inline and
	// Run/Fix below are used as-is. If Run is empty, Task instead names a
	// tasks: entry (see Task) to resolve run/fix from; this step's own Fix,
	// if set, overrides the referenced task's Fix for this step only.
	Task string `kind:"task"          yaml:"task,omitempty"`
	Run  string `yaml:"run,omitempty"`
	// RunFile loads Run's text from a file at a path relative to the pipeline
	// file's directory, instead of writing it inline (see LoadConfig's
	// resolveFileIncludes). Mutually exclusive with Run. Task steps only.
	RunFile string `yaml:"run_file,omitempty"`
	// Fix, on a task step, names an agent to invoke when a run misses the
	// step's own success criteria — its Assert when it declares one, a
	// nonzero exit otherwise. The agent is seeded with that verdict and the
	// captured output and given the task itself as a rerun tool; the command
	// is then re-run and judged by the same rule. A run that already passes
	// never constructs the agent. See FixSpec.
	Fix *FixSpec `yaml:"fix,omitempty"`
	// Put names a resource to run its out command against.
	Put string `kind:"put" yaml:"put,omitempty"`
	// Params are passed through to the resource command this step runs, as
	// {{ .params.x }}: a put's reach out:, a get's reach in:. Mirrors
	// Concourse, where params on a get are how a resource is told HOW to
	// fetch (git's depth:/submodules:, s3's unpack:) — see
	// concourse-ci.org/docs/steps/get/ and docs/conformance.md.
	//
	// A get's params fold into its node hash (see merkle.GetNodeContent):
	// they change what lands in the artifact, so two gets of one version
	// differing in params are two different fetches and must not share a
	// cache entry.
	Params map[string]any `yaml:"params,omitempty"`
	// Agent names an agents: entry this step invokes. Prompt is the task
	// given to the model (not templated — freeform text is likely to contain
	// literal {{ }} that isn't meant as a template). Dir is the step's
	// working directory relative to the job's workspace, since there's no
	// run: string to embed a cd in. Tools selects a subset of the agent's
	// granted tools by name and may add inline custom tools for this task
	// (empty means all of the agent's tools). Attempts overrides the agent's
	// default retry count (attempts: 3 = up to 3 total tries, including the
	// first); 0/unset inherits the agent's default (which itself defaults
	// to 1).
	Agent string `kind:"agent" yaml:"agent,omitempty"`
	// Messages is what this step says to the agent: one user turn per entry.
	//
	// A list rather than a single string because a conversation is a list, and
	// exposing only its first element was an arbitrary restriction rather than
	// a design. The agent runs each message to completion — tool calls and all
	// — before the next one is sent, so a later message is written against an
	// answer that already exists. That is the whole difference from putting
	// both asks in one message, where the model composes its answer to the
	// second while it is still deciding the first.
	//
	// The agent's standing instructions are not here: they are its own
	// system:. These are the per-invocation asks.
	Messages []string `yaml:"messages,omitempty"`
	// MessageFiles supplies Messages from files instead of inline text, one
	// message per file, in either of FileRef's two forms: a scalar path
	// relative to the pipeline file's directory, resolved at load time exactly
	// like RunFile/SystemFile; or a {artifact, path} mapping naming a file
	// inside an artifact this step declares in its inputs:, read at run time
	// once that artifact is fetched. The run-time form exists only here (not
	// for a task's run:, not for an agent's own definition/persona) — see
	// docs/agents.md. It costs no merkle caching: an agent step's chain is
	// already unconditionally unskippable (see internal/merkle's
	// planNonGetNode), so there is nothing to lose by resolving it after plan
	// time.
	//
	// Mutually exclusive with Messages, in BOTH forms. Two ordered lists
	// cannot be interleaved, so a step that declared both would be asking for
	// an order neither field states; refusing is the only answer that does not
	// invent one.
	MessageFiles []*FileRef `yaml:"message_files,omitempty"`
	Dir          string     `yaml:"dir,omitempty"`
	Tools        []ToolSpec `yaml:"tools,omitempty"`
	// Attempts is how many times this step's work is tried before it fails.
	// Unset takes 1 for task/get/put steps and defaultAgentAttempts for agent
	// steps (where a retry is a transport concern, not a re-run). An explicit
	// 0 is a load error — see dials.go for why attempts: sits outside the
	// zero-means-no-limit convention the other dials follow.
	Attempts *int `yaml:"attempts,omitempty"`
	// MaxTurns overrides the agent entry's max_turns for this one step:
	// tool-calling turns per attempt. Unset inherits the agent's value (which
	// itself defaults to defaultMaxAgentTurns); an explicit 0 removes the cap
	// for this step alone. Agent steps only — one long-horizon step can buy
	// more turns without every step of the same agent paying for them.
	MaxTurns *int `yaml:"max_turns,omitempty"`
	// MaxQuestions caps how many ask_user calls this step may make. Unset
	// inherits the agent entry's value (itself defaulting to
	// defaultMaxQuestions); an explicit 0 removes the cap for this step alone,
	// per the zero-means-no-limit convention. Agent steps only.
	//
	// It lives on the STEP, unlike ask_user's other dials, for the reason
	// max_turns: does: how much one step may spend is a property of that step.
	// And it is denominated in questions rather than turns because the thing
	// being spent is a person's attention — a model that asks twenty questions
	// burns somebody's afternoon, not a token budget, and max_turns: cannot
	// say anything about that.
	MaxQuestions *int `yaml:"max_questions,omitempty"`
	// Timeout is a wall-clock deadline per attempt (e.g., "2m", "30s"). Empty
	// means no timeout on a task/get/put step, and the agent entry's timeout:
	// — or the 30-minute package default — on an agent step, which is the one
	// step kind that gets a deadline it never asked for. "0" there is how a
	// long-horizon agent step says it wants none; on any other step kind it
	// is a load error, since omitting the field already says that.
	//
	// Valid on all step kinds (a get step's timeout bounds both its check and
	// in commands); for task/put steps it overrides the referenced
	// task/agent's Timeout.
	Timeout string `yaml:"timeout,omitempty"`
	// Try wraps an inner step so a task-level failure of that step doesn't
	// stop the plan. The wrapper is transparent: the inner step runs exactly
	// as it would unwrapped (its when: guard, hooks and outcome are all real),
	// and everything that observes the outcome — the wrapper's own hooks, its
	// to: route, the recorded node — sees the truth. Only the plan walker is
	// lied to, and only for an outcome.Failed inner step: an infrastructure
	// error or an abort still stops the run (see internal/pipeline's
	// tolerateTryFailure). Composes with attempts:/timeout: on the inner step,
	// and with try: itself (a doubled try: is legal, if pointless).
	//
	// The wrapper is the plan-positioned step, so to:/max_visits: belong on it
	// and are rejected on the step it wraps; verdicts: stays on the agent step
	// being wrapped, since that is what internal/agent reads. Also
	// valid as a hook body, where it tolerates the hook's failure the same way.
	Try *Step `kind:"try" yaml:"try,omitempty"`
	// Do runs several steps one after another, as a single plan step.
	//
	// Sequential execution is what a plan does anyway, so the ordering is not
	// the point — the containment is. A hook on the block observes the whole
	// group's outcome, which is the only way to say "roll back if any of these
	// three failed" without repeating the hook on each step or hoisting it to
	// the job. Mirrors Concourse's do: (concourse-ci.org/docs/steps/do/).
	//
	// The block is a container like in_parallel:/race:: it takes no operation
	// fields of its own (no inputs:, run:, image: — those belong on the steps
	// inside), and it carries hooks, when:, try: and to: as any positioned
	// step does.
	Do []Step `kind:"do" yaml:"do,omitempty"`
	// InParallel runs several steps at the same time instead of one after
	// another. See InParallel.
	InParallel *InParallel `kind:"in_parallel" yaml:"in_parallel,omitempty"`
	// Race runs several steps at once and keeps whichever finishes
	// successfully first. See Race.
	Race *Race `kind:"race" yaml:"race,omitempty"`
	// Ensemble asks several agents the same question and combines their
	// answers into one verdict to route on. See Ensemble.
	Ensemble *Ensemble `kind:"ensemble" yaml:"ensemble,omitempty"`
	// Approval pauses the plan until a person approves or rejects. See
	// Approval.
	Approval *Approval `kind:"approval" yaml:"approval,omitempty"`
	// LoadVar captures a value produced DURING the run — the contents of
	// VarFile, trimmed — into a pipeline var that later steps reference as
	// ((name)). See validateVars.
	LoadVar string `kind:"load_var" yaml:"load_var,omitempty"`
	// VarFile is the workspace-relative file a load_var: step reads.
	VarFile string `yaml:"file,omitempty"`
	// Passed, on a get step, names upstream jobs this version must ALREADY
	// have succeeded in before this job will run against it. See
	// validatePassed.
	Passed []string `yaml:"passed,omitempty"`
	// Across runs this step once per combination of the listed values,
	// substituting {{ .vars.<name> }} into its fields. Expanded at load, so
	// each cell is an ordinary plan step. See AcrossVar.
	Across []AcrossVar `yaml:"across,omitempty"`
	// Budget caps what every cell of an across: block may spend TOGETHER, and
	// unlike the job and agent ceilings it DEGRADES rather than failing: when
	// the matrix has spent its allowance, no further cell is started, the ones
	// already finished keep their work, and the plan carries on.
	//
	// That difference is the point. A job ceiling is a backstop against a
	// runaway, so failing is right. A runtime fan-out is the one step whose
	// cost nobody could know when they wrote the pipeline — its width is
	// decided mid-run, usually by a model — and there, "review eight of the
	// twelve dimensions and publish" beats "spend the same money and publish
	// nothing".
	//
	// Cells that never started are simply not run: they record nothing, so a
	// rerun with a larger ceiling picks up exactly where this one stopped
	// (finished cells are cached, unstarted ones are not).
	//
	// across: steps only, tokens only. Never hashed, like every other
	// operational limit — adding one must not invalidate a cached cell.
	Budget *Budget `yaml:"budget,omitempty"`
	// MaxInFlight bounds how many across: cells run at once. Unset or 1 keeps
	// the serial declaration-order walk this modifier has always had; anything
	// higher runs that many cells concurrently, and a value at or above the
	// cell count is effectively unbounded. Opt-in, unlike in_parallel:'s
	// limit: where an absent value means unbounded — each default matches the
	// contract its own block already had.
	//
	// Cells are one step's clones — same output names, same paths — and each
	// materializes its own directory, so concurrent cells never collide.
	//
	// NOT hashed, unlike in_parallel:'s limit:/fail_fast:. Those change which
	// steps run at all; this only changes how many run at once, and the cell
	// set is identical at any width — so raising it must not re-run a matrix
	// whose cells are all cached.
	MaxInFlight int `yaml:"max_in_flight,omitempty"`
	// Parallelism shards this task step: N copies of one command, run
	// concurrently, each knowing its slot. Sugar for across: over an implicit
	// 1-based index axis — desugared at load (desugarParallelism), so what
	// runs, hashes and caches is an ordinary matrix. What the sugar adds over
	// writing the axis: MaxInFlight defaults to N rather than serial (an
	// authored value wins), and cells render {{ .vars.count }}, the width no
	// hand-written axis can carry. Task steps only; invalid beside across:,
	// which declares its own axes, and on hooks, which never expand one. A
	// pointer like Attempts, so an explicit 0 is a load error rather than a
	// silent single copy. Still set after desugar — the marker (Sharded) the
	// count injection and the parallelism-worded diagnostics key on.
	Parallelism *int `yaml:"parallelism,omitempty"`
	// Inputs/Outputs declare which named artifacts a task/agent/put step
	// draws from and (task/agent only) produces. Each name is either a
	// resource fetched by an earlier get step or an output produced by an
	// earlier task/agent step. Both are optional and default to empty: an
	// absent inputs: means "sees nothing declared" (there is no requirement to
	// declare them). When present, an inputs: naming an artifact nothing
	// produces is caught by workspace.ValidateArtifactFlow. Declarations only
	// change what a step physically sees under a top-level workspace: block
	// (see WorkspaceConfig); without one they are a validated contract, not
	// isolation, and are never folded into a node's hash (see internal/merkle).
	// Inputs is a *InputSpec so an absent key (nil) is distinguishable from an
	// explicit empty list — which matters for ResolveTask's override rule (a
	// step's inputs: override its tasks: entry only when declared) — and so put
	// steps can accept the scalar `inputs: all` (every available artifact) in
	// addition to a sequence of names. Invalid on get steps; Outputs is
	// additionally invalid on put steps.
	Inputs  *InputSpec `yaml:"inputs,omitempty"`
	Outputs []string   `yaml:"outputs,omitempty"`
	// Image, on a task or agent step, overrides the referenced task's/
	// agent's Image for this step only (inherit-only: a non-empty step Image
	// always wins, there is no way to force host execution from a step when
	// the task/agent sets one). Invalid on get/put steps — a put's execution
	// image comes from its resource type, and a get has no task/agent to
	// override.
	Image string `yaml:"image,omitempty"`
	// Env, on a task or agent step, overrides the referenced task's/agent's
	// Env for this step only (declared-wins, like Inputs: an explicit empty
	// list means "nothing beyond the baseline", which is distinguishable from
	// absent). Invalid on get/put steps, matching Image: a put's environment
	// comes from its resource type, and a get has no task/agent to override.
	Env []string `yaml:"env,omitempty"`
	// User, on a task or agent step, overrides the referenced task's/agent's
	// User for this step only (non-empty-wins, like Image). Invalid on
	// get/put steps, for the same reason Image is.
	User string `yaml:"user,omitempty"`
	// Network, on a task or agent step, overrides the referenced task's/
	// agent's Network for this step only (non-empty-wins, like Image).
	// Invalid on get/put steps, for the same reason Image is.
	Network string `yaml:"network,omitempty"`
	// Tags names the worker a step's commands run on: a capability the step
	// needs, mapped to a machine by the invocation (--worker), never by the
	// pipeline. Empty runs on this machine, which is what nearly every step
	// does.
	//
	// One entry. Concourse intersects a step's tags against a pool of workers
	// advertising theirs; there is no pool here, so a second tag would name a
	// second machine and the step would have no home.
	//
	// NOT hashed. Placement decides where a step runs, not what it produces —
	// the same reasoning as timeout: and attempts: — and a tree that crossed a
	// wire digests identically to one that never left (see internal/wire).
	// Retagging a step, or repointing a tag at another machine, therefore does
	// not invalidate work that already succeeded.
	//
	// On a get or put it overrides the resource's own tags: (Resource.Tags)
	// for that step. On a do: or in_parallel: block it places the steps
	// inside instead, the way Job.Tags places a whole plan: a step with none
	// takes its own, else its resource's, else the nearest enclosing block's,
	// else the job's — override, never union. A step's hooks inherit what
	// the step resolved to (see visitPlacements).
	//
	// Invalid on try:, race: and ensemble: steps, on an agent without image:
	// or a CLI agent, on a task with fix: (each would leave half a step on
	// each machine), and on a get or put whose type is not shell-backed —
	// inherited or not.
	Tags []string `yaml:"tags,omitempty"`
	// Privileged, on a task or agent step, overrides the referenced task's/
	// agent's Privileged for this step only. True-wins, like Image's
	// non-empty-wins: there is no way to force UNprivileged from a step when
	// the task/agent asked for it, matching how every other execution setting
	// inherits. Invalid on get/put steps, for the reason Image is.
	Privileged bool `yaml:"privileged,omitempty"`
	// Limits, on a task or agent step, overrides the referenced task's/agent's
	// container_limits: for this step only (set-wins, like Image).
	Limits *ContainerLimits `yaml:"container_limits,omitempty"`
	// Volatile marks a step whose result must never be reused: it reads
	// something the pipeline never declared — a clock, a network endpoint, a
	// path outside its inputs — so a recorded answer is a stale one. Off by
	// default, because a step's declared inputs are what identify its work,
	// which is the contract a task's run: has always had. Valid only on task
	// and agent steps: they are the two kinds the step cache reuses at all.
	Volatile bool `yaml:"volatile,omitempty"`
	// Caches, on a task or agent step, overrides the referenced task's/agent's caches: for this step only (declared-wins, like Env). See TaskCache.
	Caches []TaskCache `yaml:"caches,omitempty"`
	// When, on a task/put/agent step, gates whether the step runs at all: an
	// explicit command whose exit code decides (0 runs, nonzero skips). See
	// WhenSpec. Invalid on get steps — a get fans the remainder of the plan
	// out per version, so a conditional get has no coherent meaning.
	When *WhenSpec `yaml:"when,omitempty"`
	// To, on a task/put/verdict-less agent step, routes to another step in the
	// SAME get-segment based on this step's binary outcome: "success" and
	// "failure" are the only keys, and both are reserved. An open map (not a
	// fixed struct) so a later exit-code routing extension needs no new type.
	// Invalid on get steps, hook steps, and any step declaring Verdicts —
	// which carry their own targets. Absent, the plan falls through in
	// declaration order exactly as before. See validateStepTransitions and
	// internal/pipeline's resolveTransition.
	To map[string]string `yaml:"to,omitempty"`
	// MaxVisits caps how many times THIS step may execute in one run. It is
	// required (LoadConfig) whenever any To target routes backward (a target
	// at or before this step's own position within its segment); 0/unset means
	// unbounded, which is only legal when every To target is strictly forward.
	MaxVisits int `yaml:"max_visits,omitempty"`
	// Verdicts, on an agent step, declares the outcome vocabulary the model
	// emits AND where each outcome sends the plan. Its presence turns on
	// verdict mode: internal/agent synthesizes a required `verdict` tool whose
	// enum is exactly these names, and the model must call it. An entry may be
	// a bare name (record the verdict, carry on) or a `name: target` pair
	// (record it and jump); `failure:` is the reserved catch for a step that
	// errored or emitted nothing. Order is significant — it is the tool enum,
	// and an ensemble's decide: any precedence. Agent-only, and mutually
	// exclusive with To. See VerdictRoute.
	Verdicts []VerdictRoute `yaml:"verdicts,omitempty"`
	// ContextPaths lists files whose contents are injected at conversation
	// start as synthetic read_file tool results — the agent sees the file
	// contents as if it had called read_file itself, without consuming a
	// turn. Paths are relative to the step's working directory and confined
	// to it (ConfinedPath): the step root without Dir, where each input is a
	// directory (["repo/CLAUDE.md"]), and Dir itself with one, so a sibling
	// input is out of reach. Only valid on agent steps. An absolute or
	// escaping spelling is refused at load (checkContextPathConfined); a
	// missing file, or a symlink out, fails the step at preparation, before a
	// token is spent; one merely over MaxContextBytes is truncated, with a
	// note saying so.
	ContextPaths []string `yaml:"context_paths,omitempty"`
	// MaxContextBytes overrides the agent's max_context_bytes: for this step
	// only, capping how much of each ContextPaths file is handed over. Unset
	// (the common case) defers to the agent's, which itself falls back to
	// DefaultMaxContextBytes; an explicit 0 hands the files over whole.
	//
	// It belongs here as well as on the agent because context_paths: is itself
	// a step-level field: two steps sharing one agents: entry routinely hand
	// it different evidence — a 400KB diff to the reviewer, a small manifest
	// to the gatekeeper — and without a step-level ceiling the only way to
	// give them different ones is to duplicate the whole agent under a second
	// name for the sake of one number. (context_window: has no step spelling
	// for the mirror-image reason: it describes the MODEL, and the model is
	// the agent's.) Operational, like the agent's, and likewise never hashed.
	MaxContextBytes *int `yaml:"max_context_bytes,omitempty"`
	// Hooks are the step's on_success/on_failure/on_error/on_abort/ensure
	// reaction steps (see Hooks). Inlined so they sit alongside the step's
	// own fields in YAML.
	Hooks Hooks `yaml:",inline"`
	// Assert, on a task/agent step, checks the step's captured output/exit
	// code (see Assert). A matching assert makes a non-zero-exit task a
	// success; a mismatch fails the step. Invalid on get/put steps.
	Assert *Assert `yaml:"assert,omitempty"`
	// Label is COMPUTED at expansion (never written in YAML): the name this
	// step is KNOWN BY, when that differs from the name it is RESOLVED by.
	//
	// task:/agent:/put: each do two jobs — they identify the step (routing
	// target, recorded node, assert.execution) and they look something up
	// (FindTask, FindAgent, the resource). An across: matrix needs its cells to
	// be distinguishable, so it used to append coordinates to the very field
	// the lookup keys on: a cell over a shared tasks: entry then failed with
	// `no task named "shared [shard=b]"`, and an agent cell could not be
	// renamed at all, since FindAgent has no inline escape the way ResolveTask
	// does. So every agent cell of a matrix answered to one name, which is why
	// context: is still rejected there.
	//
	// Label carries the identity half, leaving the lookup half alone. Empty on
	// an ordinary step, where the two names are the same thing — see stepName.
	Label string `yaml:"-"`
	// OutputSubdir is COMPUTED at expansion (never written in YAML): this
	// cell's coordinates as a path — one segment per axis value, declaration
	// order ("alpha/fast"). A matrix that declares outputs: captures each
	// cell's outputs under it (findings/alpha/fast/...), which is what lets N
	// cells share one declared output name without clobbering each other; see
	// CollectedOutputMapping. Empty on an ordinary step, and ignored on a cell
	// that declares no outputs.
	OutputSubdir string `yaml:"-"`
	// Memory, on an agent step, is the scope it keeps facts under between
	// runs: what that scope holds is handed to the model as it starts, and the
	// remember/forget builtins write to it. See StepMemory and docs/agents.md.
	Memory *StepMemory `yaml:"memory,omitempty"`
	// Context, on an agent or task step, opts into reading named earlier
	// steps' decisions: `context: { from: { <step>: verdict|note|full } }`.
	// Never a hook — see validateContextSteps and contextfrom.go.
	Context *ContextSpec `yaml:"context,omitempty"`
	// NoteRequired is COMPUTED at load (never written in YAML): some other
	// step declared context: { from: { <this step>: note|full } }, so this
	// step's verdict note stops being optional. The obligation lives here
	// rather than on the reader because internal/agent builds the sender's
	// tool set and never sees the reader. See validateContextFrom.
	NoteRequired bool `yaml:"-"`
	// InputMapping/OutputMapping rename a task step's declared inputs/outputs
	// onto plan-artifact names, mirroring Concourse's input_mapping/
	// output_mapping (see docs/conformance.md;
	// TestRunJobIsolatedGetAliasMappingAndPutAll in
	// workspace_integration_test.go; Concourse doc: concourse-ci.org/docs/
	// steps/task/): each entry is {task-config-name: plan-artifact-name}, so
	// a reusable tasks: entry with pinned input names can be pointed at
	// whatever a job actually fetched/produced without editing the task. Keys
	// must be a subset of the resolved task's declared inputs:/outputs:. Task
	// steps only (a load-time error elsewhere); mapping renames a materialized
	// directory. Absent/empty leaves names unmapped.
	InputMapping  map[string]string `yaml:"input_mapping,omitempty"`
	OutputMapping map[string]string `yaml:"output_mapping,omitempty"`
	// Resource, on a get or put step, names the resource to fetch or publish
	// to when it differs from the step's own name. On a get the fetched
	// artifact (and the directory, step name, and to: target) is Get, while the
	// resource whose check/in runs is Resource — mirroring Concourse's
	// get.resource, including that two aliased get steps for the same
	// underlying resource share one version history rather than tracking
	// separately (see docs/conformance.md;
	// TestResourcesAndAffectedJobsResolveGetAlias in
	// internal/trigger/trigger_test.go; Concourse doc: concourse-ci.org/docs/
	// steps/get/). On a put the step name, to: target and execution entry are
	// Put, while the resource whose out: runs is Resource — Concourse's
	// put.resource (concourse-ci.org/docs/steps/put/). This lets one resource
	// appear under a task-friendly name, or several times in a plan under
	// several names. Empty (the default) means the resource name equals Get or
	// Put.
	Resource string `yaml:"resource,omitempty"`
	// Line is the step's source line in the pipeline file, filled in after
	// decoding (see stampLines) so a validation error can point at a place in
	// the file. Never written in YAML and never hashed.
	Line int `yaml:"-"`
}

Step is a flat union of the step kinds this interpreter supports: get, task, put, and agent. Each field that selects a kind is tagged `kind:"<yaml key>"`, which is how tools/kindswitch finds tagless dispatch on it; a new kind field takes the tag too.

func ExpandAcross

func ExpandAcross(label string, step Step) ([]Step, error)

ExpandAcross renders an across: step into its cells: one step per combination of values, with `{{ .vars.<name> }}` substituted into the templatable fields, in row-major order over the axes as declared.

The cells are SIBLINGS, not a sequence. Each is hashed and cached independently under the across block, which is what gives the headline property: changing one value in one axis re-runs only the cells that value appears in. Chaining them instead would mean a changed cell invalidated every cell before it, since a chain is identified by its leaf — which is exactly the whole-matrix re-run this exists to avoid.

Cells run in declaration order rather than concurrently: they commonly share a workspace, and a matrix's value is mostly in not hand-maintaining the copies. Put an in_parallel: inside a cell if a cell's own work should overlap.

func ExpandAcrossValues

func ExpandAcrossValues(label string, step Step, runtime map[string][]string) ([]Step, error)

ExpandAcrossValues is ExpandAcross with the file axes already read: runtime maps each from_file: axis's var name to the values it took this run.

Two entry points rather than one because the two happen at different times. A static matrix expands at load, so a malformed one costs a load rather than a run; a file axis cannot exist until the step that writes it has run, so it expands in internal/pipeline instead. Both share every rule below, which is what keeps a file-driven cell hashing identically to the static cell it is indistinguishable from.

func ResolveArtifactImage

func ResolveArtifactImage(step Step, artifacts map[string]bool, fetched map[string]Fetched) (Step, bool, error)

ResolveArtifactImage returns step with an image: naming an artifact replaced by the reference that artifact's get fetched, reporting whether anything was replaced. A try: is resolved through to the step it wraps, because that is the step that runs and the step its guard runs as.

It returns a COPY and never writes through step: a try: body is cloned before it is resolved. A long-lived process hands one loaded Config to every run, and a digest written into it would be run N's image for run N+1 — stale, and a false cache hit.

The planner and the executor both call this, so they cannot disagree about the image a step hashes under.

func (Step) ContextFrom

func (s Step) ContextFrom() map[string]FromLevel

ContextFrom returns the steps this one reads from, keyed by step name. Nil when it reads from none.

func (Step) DisplayName

func (s Step) DisplayName() string

DisplayName is stepName for other packages: the name a step is KNOWN by, which is what a run's output, its recorded node, and assert.execution should all agree on. Distinct from the task:/agent:/put: value once an across: cell has been named (see Step.Label), and identical to it otherwise.

func (Step) FromSenders

func (s Step) FromSenders() []string

FromSenders returns the demanded step names in sorted order — the order the delivered blocks are rendered in, and the order the hash sees them. Sorted rather than declaration order because a YAML mapping has none, and a reader naming two senders must not hash differently run to run.

func (Step) GetResourceName

func (s Step) GetResourceName() string

GetResourceName is the name of the resource a get step fetches: Resource when set (get: aliases the resource under a different name), else Get itself. The fetched artifact, its directory, and the step's routing name are always Get; only the resource whose check/in runs is GetResourceName.

func (Step) InputNames

func (s Step) InputNames() []string

InputNames returns the explicit input names (nil for an absent or all-form inputs:). It is the []string every existing input consumer expects.

func (Step) InputsAll

func (s Step) InputsAll() bool

InputsAll reports whether the step declared `inputs: all` (put steps only).

func (Step) InputsDeclared

func (s Step) InputsDeclared() bool

InputsDeclared reports whether the step carried an inputs: key at all (nil vs present, including inputs: []). ResolveTask reads this so a step's inputs: override its tasks: entry only when the step actually declared one.

func (Step) Kind

func (s Step) Kind() (kind StepKind, ok bool)

Kind reports which single kind of step s is. ok is false when zero, or more than one, of the kinds is set — a malformed step every call site should reject the same way, rather than each silently picking whichever field its own historical check order happened to test first.

func (Step) PutResourceName

func (s Step) PutResourceName() string

PutResourceName is the name of the resource a put step publishes to: Resource when set, else Put itself. The step's name, to: target and execution entry are always Put; only the resource whose out: runs is PutResourceName.

func (Step) ReadsFrom

func (s Step) ReadsFrom() bool

ReadsFrom reports whether this step demands any upstream outcome.

func (Step) RenderVars

func (s Step) RenderVars(values map[string]string) Step

RenderVars returns s with every ((name)) in its var fields and params: substituted from values, unknown references left in place.

Slices are cloned before rendering, never written through: s is a copy of the config's step, but a slice field still points at the config's own array — so rendering through it would substitute into the LOADED pipeline, and a long-lived process (steps web) hands that same config to every run. The second run would then find no placeholder left, send the first run's values, and — because the substituted text is what gets hashed — hash identically to it: a false cache hit.

func (Step) RouteEntries

func (s Step) RouteEntries() []VerdictRoute

RouteEntries returns every (outcome key, target) pair this step routes by, from whichever of verdicts:/to: it declares — the input to target resolution, which treats both the same way. Bare verdicts are omitted: they name no target to resolve.

func (Step) RouteFor

func (s Step) RouteFor(key string) (target string, ok bool)

RouteFor resolves the target an outcome key sends the plan to, looking in the verdicts: list first and the to: map second (a step has one or the other; validateStepRouting rejects both at once). ok is false when the key routes nowhere — an unmatched outcome, or a bare verdict — and the caller falls through in declaration order.

func (Step) Routes

func (s Step) Routes() bool

Routes reports whether this step can send the plan somewhere other than the next step in declaration order — a to: map, or a verdicts: entry that names a target. A verdicts: list of bare names routes nothing: it records what the model decided and carries on.

func (Step) Sharded

func (s Step) Sharded() bool

Sharded reports whether this step began as parallelism: — the marker the desugared matrix keeps so the count var and the run report can speak the author's word. See the field's comment in step.go.

func (Step) Unwrap

func (s Step) Unwrap() Step

Unwrap returns the step a try: chain ultimately wraps — s itself when s is not a try wrapper. Callers that care about what a plan step actually RUNS (which agent, which verdicts:) go through this, since a try: wrapper carries none of those fields itself.

func (Step) VerdictNames

func (s Step) VerdictNames() []string

VerdictNames returns the vocabulary the model may emit, in declaration order, excluding the reserved failure catch — which is exactly the enum internal/agent puts on the synthesized verdict tool.

func (Step) VerdictRoutes

func (s Step) VerdictRoutes() []VerdictRoute

VerdictRoutes returns the verdict list that governs this step: its own, the one a try: wraps, or an ensemble block's.

The three live in different places for reasons that survive this redesign — a try: wrapper carries no agent fields, and an ensemble's vocabulary belongs to the block because every member votes in it — so every caller that asks "what does this step route on" goes through here rather than reaching for the field.

func (Step) VersionEvery

func (s Step) VersionEvery() bool

VersionEvery reports whether this get declared `version: every` — the one mode that runs the rest of the plan once per version. Every other spelling (unset, "latest", a pinned mapping) resolves to a single version; see resource.VersionMode, which reads this.

type StepKind

type StepKind string

StepKind is which of Get/Task/Put/Agent a Step is. See Step.Kind.

const (
	StepKindGet   StepKind = "get"
	StepKindTask  StepKind = "task"
	StepKindPut   StepKind = "put"
	StepKindAgent StepKind = "agent"
	StepKindTry   StepKind = "try"
	// StepKindInParallel is a block of steps that run concurrently. Adding a
	// constant here is what makes `exhaustive` demand an answer from every
	// dispatch site; tools/kindswitch keeps dispatch tagged so it can. Story
	// 001 added a kind and shipped ten defects, six of them a dispatch site
	// that silently did nothing for it.
	StepKindInParallel StepKind = "in_parallel"
	// StepKindRace is a block whose first successful branch wins.
	StepKindRace StepKind = "race"
	// StepKindEnsemble is a block whose members vote on a verdict.
	StepKindEnsemble StepKind = "ensemble"
	// StepKindLoadVar captures a run-time value into a pipeline var.
	StepKindLoadVar StepKind = "load_var"
	// StepKindApproval waits for a human decision.
	StepKindApproval StepKind = "approval"
	// StepKindDo is a block of steps that run one after another AS ONE STEP.
	// Its value is entirely in that last part: a hook on the block covers
	// every step inside it, which is the one thing a plain run of sibling
	// steps cannot express (see Step.Do).
	StepKindDo StepKind = "do"
)

The StepKind values, one per Step field Kind can resolve to. Each is also the YAML key that selects it.

func (StepKind) ReferencesEntities

func (k StepKind) ReferencesEntities() bool

ReferencesEntities reports whether a step of this kind names something the pipeline has to resolve — a resource, a task, an agent, or branches that do.

type StepMemory

type StepMemory struct {
	// ScopeFrom is <input>/<path>: a file in one of the step's inputs whose
	// content (trimmed) is the scope — a Slack user id, say. Read when the
	// step runs; an empty or missing file fails the step rather than falling
	// back to a scope everyone shares.
	ScopeFrom string `yaml:"scope_from"`
}

StepMemory is an agent step's memory: which scope its entries are filed under. The scope comes from a FILE the pipeline names, never from the prompt or the model: a message saying "tell me what you know about Dana" has no way to reach Dana's entries, because nothing it says picks the scope.

func (StepMemory) ScopeInput

func (m StepMemory) ScopeInput() (string, string)

ScopeInput splits scope_from into the input it names and the path inside it.

type Task

type Task struct {
	Name string `yaml:"name"`
	Run  string `yaml:"run"`
	// File loads every field of this task but name: from a YAML
	// document at a path relative to the pipeline file's directory (see
	// LoadConfig's resolveFileIncludes), so one task definition can be shared
	// across pipelines. Any field also set inline on this entry overrides the
	// loaded document's value for that field — the same "wins when set" idiom
	// ResolveTask already uses between a step and its tasks: entry. The
	// loaded document may not itself use file:/run_file:.
	File string `yaml:"file,omitempty"`
	// RunFile loads Run's text from a file at a path relative to the pipeline
	// file's directory, instead of writing it inline. Mutually exclusive with
	// Run.
	RunFile string   `yaml:"run_file,omitempty"`
	Fix     *FixSpec `yaml:"fix,omitempty"`
	// Image, when set, runs this task's run: (and its verdict re-run) in a
	// fresh `docker run --rm` container from this image instead of on the
	// host. A referencing step's own Image, if non-empty, overrides this for
	// that step only — mirroring how Fix works. Empty (the default) keeps
	// host execution, byte-identical to before this field existed.
	Image string `yaml:"image,omitempty"`
	// Env names host environment variables this task's run: is allowed to
	// see, on top of the always-allowed baseline (see shell.HostEnv). Names
	// only — see validateEnvValues. A referencing step's own Env, if
	// non-nil, overrides this for that step only, mirroring how Fix works.
	Env []string `yaml:"env,omitempty"`
	// User is the container user this task's run: executes as (docker's
	// --user). Empty takes the platform default — see shell's
	// defaultContainerUser. Only meaningful alongside Image.
	User string `yaml:"user,omitempty"`
	// Network is the container network this task's run: joins (docker's
	// --network); "none" cuts off egress entirely. Requires Image, except
	// "none" beside Sandbox.
	Network string `yaml:"network,omitempty"`
	// Privileged runs this command's container with `docker run --privileged`.
	// Mirrors Concourse's privileged: (concourse-ci.org/docs/steps/task/).
	// Container-only, like Network — a host command has nothing to elevate,
	// so it is a load error without image:.
	Privileged bool `yaml:"privileged,omitempty"`
	// Limits caps the container's CPU and memory. Mirrors Concourse's
	// container_limits:; container-only for the same reason Privileged is.
	Limits *ContainerLimits `yaml:"container_limits,omitempty"`
	// Sandbox fences this entity's host commands with the OS sandbox: writes only under the step's workspace and a private TMPDIR, credential directories unreadable, and network: none becomes legal on the host. See internal/config/sandbox.go for the rules and docs/infra.md for the policy.
	Sandbox bool `yaml:"sandbox,omitempty"`
	// Timeout is a wall-clock deadline per attempt (e.g., "2m", "30s"). Empty
	// (default) means no timeout. Inherited by task steps unless overridden.
	Timeout string `yaml:"timeout,omitempty"`
	// Inputs/Outputs are consulted only when a pipeline sets workspace: (see
	// WorkspaceConfig); a referencing step's own Inputs/Outputs, if
	// non-nil, override these for that step only — mirroring how Fix works.
	//
	// Inputs is the same *InputSpec a step carries, so `inputs:` means one
	// thing in the schema rather than two types that happened to share a
	// name. The list form decodes identically; only the scalar `all` differs,
	// and that stays put-only (validateTaskInputsAll) — a reusable task
	// declaring "everything available" would depend on whichever job called
	// it, which is the opposite of reusable.
	Inputs  *InputSpec `yaml:"inputs,omitempty"`
	Outputs []string   `yaml:"outputs,omitempty"`
	// Caches are directories this task keeps between runs on the machine that ran it (see TaskCache). A referencing step's own Caches, if non-nil, override these for that step only.
	Caches []TaskCache `yaml:"caches,omitempty"`
}

Task is a named, reusable command a `task` step can invoke by name instead of carrying its own `run:` inline. A step with `task: <name>` and no `run:` resolves against this list; a step's own `fix:`, if set, overrides the referenced task's Fix for that step. A step that does supply `run:` is always inline and never consults this list, even if a same-named entry exists here.

type TaskCache added in v0.2.0

type TaskCache struct {
	// Path is relative to the step's working directory. It may sit inside an input, which it then shadows, as Concourse's own node_modules example does; it may not be or contain an input, nor touch an output, which would carry the cache into the step's results.
	Path string `yaml:"path"`
}

TaskCache is one directory a step keeps between runs, on the machine that ran it: Concourse's task cache. Scoped to pipeline + job + step name + path, empty on first use and on any other machine, saved whatever the step's exit code. Never hashed: a cache only makes the step faster, so a warm one must not make identical work look different. It is never an input or an output either: not passed to other steps, not captured by the step cache.

type ToolSpec

type ToolSpec struct {
	Builtin     string // set when the entry is a bare builtin name
	Name        string // custom tool: function name exposed to the model
	Description string // custom tool (and sub-agent tool): description shown to the model
	Run         string // custom tool: sh -c template, {{ .args.X }} interpolated
	// Agent, when set, makes this tool a sub-agent delegation: the named
	// agents: entry is exposed to the model as a callable tool taking a single
	// `request` string; each call runs that agent's own fresh tool-calling
	// conversation (its own model/persona/dials/tool grant) in the caller's
	// working directory, and its final text answer is the tool result. A
	// sub-agent tool sets no builtin/name/run and can never be required:
	// (enforced at LoadConfig by validateAgentGraph). Sub-agents may
	// themselves grant sub-agents, up to maxSubAgentDepth, with no cycles.
	Agent string
	// MCP, when set, makes this a passthrough to one or more tools on a
	// configured mcp_servers: entry (MCP names the server). Three forms:
	//   - MCPTool set ({mcp, tool} in YAML): grants that one remote tool;
	//     only this form may also set Description/Required/MaxCalls/Args.
	//   - MCPTools set ({mcp, tools: [...]} in YAML): grants that named
	//     subset, each keeping its own server-advertised description.
	//   - neither set ({mcp} alone): grants every tool the server exposes.
	// Like a sub-agent tool, an MCP grant must live on the agents: entry
	// (or a fix:'s own tools:), never introduced inline on a step
	// (enforced by validateMCPToolGrants/resolveEffectiveTools).
	MCP      string
	MCPTool  string
	MCPTools []string
	// Required marks a custom tool's command as a resource-like action that
	// must succeed: a nonzero exit aborts the agent step (and, once attempts
	// are exhausted, the job) instead of being reported to the model as
	// {"error": ...} data for it to react to. Ignored on builtins; invalid on
	// sub-agent tools.
	Required bool
	// MaxCalls caps how many times this custom tool may be invoked within one
	// attempt's conversation (0/unset = unlimited). The (N+1)th call is
	// rejected as ordinary tool-result data ({"error": "... call budget
	// exhausted ..."}), never an aborted attempt. The counter resets on an
	// attempts: restart (a fresh conversation is a fresh budget). Invalid on
	// builtins and sub-agent tools.
	MaxCalls int
	// Args pins argument values: merged OVER the model's own arguments at
	// call time (pinned always wins), and excluded from the parameter schema
	// shown to the model, so the model can neither see nor override them —
	// "the model chooses when, the machine chooses where." Values are plain
	// strings; not templated. On a custom tool they are keys its run:
	// template may reference. On a single-tool MCP grant each must be a
	// top-level property the server's input schema declares, and is sent as
	// that property's scalar type (internal/agent pinMCPArgs). Invalid on
	// builtins, sub-agent tools, and the tools:/bare MCP forms.
	Args map[string]string
	// Allow narrows the web_fetch builtin to the named hosts: an entry
	// matches its exact hostname and any subdomain of it, and every hop of a
	// redirect chain is re-checked. Empty means any PUBLIC http(s) URL:
	// loopback, link-local, private and multicast addresses are refused at
	// connect time unless an entry names the host, which is the only opt-in
	// (internal/agent webFetchTransport). Invalid on
	// every other tool form (validateWebFetchAllowShape).
	Allow []string
	// AnsweredBy names an agents: entry that answers this grant's ask_user
	// questions BEFORE a person is asked — the first rung of the ladder in
	// docs/agents.md. It is deliberately not the same thing as granting that
	// agent as a sub-agent tool: a sub-agent is delegation the asking model
	// chose and paid for out of its own reasoning, while a responder is an
	// ESCALATION a person can still intercept, and the recorded row says which
	// one answered. Empty parks the question for a person straight away.
	// Invalid on every tool but the ask_user builtin (validateAskUserShape).
	AnsweredBy string
	// Default is the answer ask_user resolves to when its wait expires. The
	// model is told it was a default and that nobody answered — an
	// indistinguishable default is the runtime telling a model a person
	// confirmed something no person saw. With no default declared, an expiry
	// ABORTS the step instead, matching approval: exactly: aborted, not
	// failed, because nobody decided anything. Invalid on every other tool.
	Default string
	// OptionsRequired refuses an answer that is not one of the options the
	// model offered. Off by default, and that default is the design: a wrong
	// option list must never trap the person answering, so the free-text
	// escape is what makes offering options safe. Turn it on for a question
	// whose answer a later step will match on. Invalid on every other tool.
	OptionsRequired bool
	// MaxOutputBytes sets the inline output budget for this one tool
	// (0/unset = the global default, maxToolOutputBytes in internal/agent).
	// An explicit value wins in either direction, bounded above by the spill
	// ceiling (shell.SpillMaxBytes, 10MB).
	//
	// Narrowing is for a tool whose output is known to be mostly noise: a
	// fuzzy search that returns a ranked list where the answer is the first
	// few entries, where the tail costs context on every subsequent turn and
	// buys nothing. Narrowing loses no data — the overflow still spills to a
	// file the model can read back — it only shrinks what lands in the
	// conversation. Widening is the declared trade the other way: a bigger
	// inline result instead of a spill pointer, for a tool whose output the
	// model needs whole.
	//
	// Deliberately NOT valid on a built-in: those carry their own output
	// contract (read_file pages, list_dir counts entries, search_files is
	// bounded by arithmetic), and a second, conflicting bound on top of a
	// designed one is a bug surface rather than a knob. Also invalid on a
	// sub-agent tool, whose result is another agent's considered answer, not
	// a data dump. Valid on custom tools and on all three MCP grant forms.
	MaxOutputBytes int
	// Timeout bounds one call of this tool (empty/unset = no per-call bound;
	// the step's own deadline still holds). An expired call comes back to the
	// model as {"error": "... timed out after ..."} — ordinary data it can
	// react to on its next turn, the same as a nonzero exit — never an
	// aborted attempt: a deadline says how long a tool may take, not that the
	// step is over.
	//
	// Valid on every tool form, unlike max_output_bytes: a deadline collides
	// with no designed per-tool contract, and run_shell — a builtin — is the
	// likeliest thing in the system to hang. It binds only where the tool is
	// GRANTED, except on an inline custom tool, which a step defines rather
	// than selects (see validateToolTimeoutShape). A cli source used to be an
	// exception — a builtin ran as the CLI's own native tool, which the
	// deadline never reached — but since issue #100 every tool a CLI agent
	// calls goes through the bridge, the same impl a hosted agent's deadline
	// already binds to, so a builtin's timeout: on a cli source binds exactly
	// like any other form.
	//
	// On ask_user the deadline is the WAIT, and what a breach resolves to is
	// that tool's own contract: the declared default: with answered: false,
	// or an aborted step when none was declared. That is the one place this
	// word bends, and it bends the way web_fetch's hard 30s cap already
	// established a built-in may.
	//
	// It is enforced through the call's context, so it stops what honors one:
	// shell-backed tools (the sh -c process is killed, though not its own
	// grandchildren — see shell.cancelWaitDelay), MCP calls, sub-agent
	// conversations. The purely local built-ins (read_file, list_dir,
	// search_files, write_file, edit_file) take no context and so run to
	// completion; an over-deadline call there is reported only if it failed
	// on its own, never relabeled from success (see withToolTimeout).
	// web_fetch carries a hard 30s client-side cap of its own, so a timeout:
	// there can only tighten.
	Timeout string
	// Params limits what each argument of this tool may be (see ParamSpec). Keys are the argument names the caller sees; a pinned argument is not one. Valid on a custom tool, a sub-agent tool and a single-tool MCP grant, where the tool is granted.
	Params map[string]ParamSpec
	// Rules names rules: entries that check each call of this tool (see Rule), in order, after any rules of the export it is in. Where the tool is granted.
	Rules []string
}

ToolSpec is one entry in an agent step's tools: list — a built-in tool referenced by name, a custom command-backed tool, or a sub-agent tool (an agents: entry exposed to the model as a callable tool, see Agent below). It implements yaml.Unmarshaler because a tools: list mixes bare scalar entries (builtin names) with mapping entries (custom tool / sub-agent definitions).

func DefaultAgentToolSpecs

func DefaultAgentToolSpecs() []ToolSpec

DefaultAgentToolSpecs is used when an agent grants no tools — the default is the read-only built-ins. Anything that mutates state or reaches the host (run_shell, write_file, edit_file) is a capability the agent must be granted explicitly.

func (*ToolSpec) UnmarshalYAML

func (t *ToolSpec) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML decodes a ToolSpec from either a scalar (builtin name) or a mapping YAML node: {name, description, run, required, max_calls, args} for a custom tool, {agent, description} for a sub-agent tool, {mcp, tool, description, required, max_calls, args} / {mcp, tools: [...]} / {mcp} for an MCP tool grant (see ToolSpec.MCP), or {builtin, description} to reference a builtin by mapping instead of a bare scalar — the only reason to do so is to hit a validation error like max_calls/args on a builtin explicitly (validateToolCallGuardShape), since a bare scalar entry has no room for those fields at all.

type VerdictRoute

type VerdictRoute struct {
	Name   string `json:"name"`
	Target string `json:"target,omitempty"`
}

VerdictRoute is one entry of a verdicts: list: the outcome name the model may emit, and where the plan goes when it does.

Two spellings, because most verdicts do not branch:

verdicts:
  - approve            # Target "": record the verdict, fall through
  - revise: fix-code   # Target "fix-code": record it and jump
  - failure: cleanup   # reserved: the step errored or emitted no verdict

The vocabulary and the routing were two fields (verdicts: and to:) that had to agree, cross-checked at load. Folding them into one list removes the disagreement rather than diagnosing it — and, unlike a to: map, a list keeps its order, which verdict mode needs twice over: the order is the emitted tool enum, and it is the precedence an ensemble's decide: any resolves by.

The json tags are load-bearing: internal/merkle hashes this list as part of a step's identity, since it changes the synthesized tool set.

func (*VerdictRoute) UnmarshalYAML

func (v *VerdictRoute) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML accepts either spelling of an entry: a bare scalar (fall through) or a single-key mapping (route). Anything else — a multi-key mapping, a sequence — is a load error naming both forms, since a two-key entry is an author writing what they think is a map of all their verdicts.

type WebhookSignature

type WebhookSignature struct {
	Header    string `yaml:"header"`
	Prefix    string `yaml:"prefix,omitempty"`
	Encoding  string `yaml:"encoding,omitempty"`
	Algorithm string `yaml:"algorithm,omitempty"`
	Signed    string `yaml:"signed,omitempty"`
	Timestamp string `yaml:"timestamp,omitempty"`
}

WebhookSignature is provider: custom — an HMAC in one header, described rather than coded. Signed and Timestamp are expressions that build strings; whether the signature is valid is only ever decided in Go.

type WebhookSource

type WebhookSource struct {
	Provider  string `yaml:"provider"`
	SecretEnv string `yaml:"secret_env"`
	MaxBody   int64  `yaml:"max_body,omitempty"`
	// Filter, ID and Version are expressions over the verified delivery; see exprlang.Webhook.
	Filter    string            `yaml:"filter,omitempty"`
	ID        string            `yaml:"id,omitempty"`
	Version   map[string]string `yaml:"version,omitempty"`
	Signature *WebhookSignature `yaml:"signature,omitempty"`
}

WebhookSource is a webhook resource's source:, typed. Config knows its shape; the provider table and the expression compiler it names live in internal/webhook and internal/exprlang, which is where resource.Receiver finishes the job.

type WhenSpec

type WhenSpec struct {
	Run string
}

WhenSpec is a step's when: guard — an explicit shell command whose EXIT CODE decides whether the step runs at all: 0 runs it, nonzero skips it. A nonzero exit is a legitimate "false" (a `grep -q` that finds nothing), never a failure; only a runner-level error (the command could not be started at all — a bad image, a docker daemon that isn't up) fails the step, so an infrastructure problem is never silently read as "skip".

A skipped step behaves exactly like a merkle-cached skip: it fires no hooks, records no node or job_run, and does not appear in a job's assert.execution log. The guard runs in the same view the step itself would get — under the step's resolved image, in a directory materialized from the step's declared inputs — so it can read what the step reads.

It implements yaml.Unmarshaler for the same scalar-or-mapping reason FixSpec does: `when: test -f x` is the common case, `when: {run: ...}` the explicit one.

func (*WhenSpec) UnmarshalYAML

func (w *WhenSpec) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML decodes a WhenSpec from either a scalar (the command) or a mapping ({run}) YAML node.

type WorkspaceConfig

type WorkspaceConfig struct {
	// Strategy is "copy", the only one, kept so a pipeline spelling out the default still loads. It uses copy-on-write where the filesystem offers it (APFS clonefile on macOS, reflink on Linux) and a plain recursive copy otherwise.
	Strategy string `yaml:"strategy,omitempty"`
	// Root is where isolated build workspaces are materialized; the system temp directory when unset.
	Root string `yaml:"root,omitempty"`
	// Cache, when set, opts into the cross-build resource cache: a fetched
	// resource version is kept under Root and reused by later builds instead
	// of being re-fetched. Off by default, deliberately — see CacheConfig.
	Cache *CacheConfig `yaml:"cache,omitempty"`
}

WorkspaceConfig tunes the per-step workspace isolation every pipeline runs under: task/agent/put steps materialize a directory built from their own declared inputs:/outputs: (see Step, Task) — a step sees what it declared and nothing else. Isolation is not optional (there is no shared single-directory mode; an absent block simply takes every default here), the block only chooses HOW trees are materialized and where. This is corruption hygiene, not a security sandbox — a step's shell commands can still reach outside the materialized directory via absolute paths.

func (*WorkspaceConfig) CacheEnabled

func (w *WorkspaceConfig) CacheEnabled() bool

CacheEnabled reports whether the cross-build resource cache is on.

func (*WorkspaceConfig) CacheMaxEntries

func (w *WorkspaceConfig) CacheMaxEntries() int

CacheMaxEntries is the configured ceiling, or the default.

Jump to

Keyboard shortcuts

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