Documentation
¶
Overview ¶
Package interp implements stage 4 (references) of the manifest's fixed seven-stage validation pipeline (design.md D-H): a hand-written scanner over a CLOSED interpolation grammar (design.md D-L), plus the typed Value carrier that makes secrecy a structural property rather than a runtime discipline.
This is deliberately NOT a general expression evaluator. design.md D-L rejected text/template (supports function calls and reflective field traversal), a CEL/expr library (arbitrary-expression evaluation), and os.Expand ($VAR leaks the process environment) in favor of a small, closed, fully-static grammar:
placeholder := "${{" ws ref ws "}}"
ref := "variables." name
| "secrets." name
| "steps." name ".output"
name := [A-Za-z_][A-Za-z0-9_-]*
Anything else is a parse error at Scan, never a fallback to literal text — there is no evaluation step to attack.
Stage 7 (value binding, "Secret reference in a non-secret-typed field is a stage-7 validation error") is NOT implemented here: it requires a provider's declared WithSchema field kinds, which do not exist until internal/workflow/providers (Phase 7). This package ships the primitive that stage 7 depends on (Reference.StaticKind) — see reference_test.go for the exact boundary.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Kind ¶
type Kind int
Kind identifies what a Value carries. It is deliberately a closed, small enum matching design.md D-L's type sketch exactly — no provider-specific kinds, no open extension point.
type Namespace ¶
type Namespace int
Namespace identifies which of the three closed grammar prefixes a Reference belongs to (design.md D-L's ref production).
type Reference ¶
Reference is one resolved "${{ ... }}" placeholder body. Name is set for NamespaceVariables and NamespaceSecrets; StepID is set for NamespaceSteps. Every Reference produced by Scan is guaranteed to match the closed grammar exactly — there is no reference shape Scan can return that is not one of the three ref alternatives.
func (Reference) StaticKind ¶
StaticKind reports the Value Kind a Reference is GUARANTEED to resolve to, when that is determinable from the grammar and manifest schema alone, or ok=false when the eventual Kind is not yet knowable at this stage.
secrets.* is unconditionally KindSecret by construction (only NewSecret ever produces one). variables.* is unconditionally KindString because the manifest schema types spec.variables as map[string]string (internal/workflow/manifest.Spec.Variables) — there is no way for a variables.* reference to resolve to anything else.
steps.<id>.output's Kind is fixed by whichever capability produced step <id>, which is not known until stage 6 provider resolution (internal/workflow/providers, Phase 7, design.md D-I's WithSchema). StaticKind reports ok=false rather than guessing, so a later stage cannot silently skip the provider-resolution step its OWN correctness actually depends on. This is the primitive tasks.md 5.3 ships: full stage-7 forbidPlaintext enforcement (comparing a field's declared kind against the reference's eventual kind) is Phase 7's job, and it MUST call StaticKind for variables./secrets. references and fall back to the resolved provider's WithSchema kind for steps.<id>.output references — this package has no provider schema to compare against, so it cannot and does not implement that comparison itself.
type Token ¶
Token is one scanned unit: either a literal text run or a resolved reference. Scan emits []Token with no evaluation step — there is nothing further for a caller to interpret except look each Reference up (design.md D-L: "there is no evaluation step to attack").
func Scan ¶
Scan parses s against the closed interpolation grammar (design.md D-L). Every placeholder in s MUST match exactly one of the three ref alternatives; anything else — an operator, a function call, a nested placeholder, an unknown namespace, a trailing path segment, or a malformed delimiter — is a parse error. Scan never returns a non-nil token slice alongside a non-nil error: a rejected form is a hard failure, never a partial or best-effort literal-text fallback (tasks.md 5.1).
type Value ¶
type Value struct {
// contains filtered or unexported fields
}
Value is the typed result of resolving an interpolation reference (design.md D-L, the mechanically-enforced secret rule). Its shape matches design.md's type sketch exactly:
type Value struct {
kind Kind
str string // never set when kind == KindSecret
secret *dagger.Secret // only set when kind == KindSecret
}
Secrecy is a STRUCTURAL property, not a runtime discipline: str is never populated by any constructor when kind == KindSecret (see NewSecret), and no exported accessor returns a secret's payload as a Go string (see String, and the reflection-based proof in security_test.go). This is a stronger guarantee than "any policy that merely forbids substitution" — there is no code path that COULD produce a plaintext secret string, because the string-producing path cannot hold a secret value at all.
func NewBool ¶
NewBool constructs a KindBool Value, following the same single-str-field representation as NewInt.
func NewInt ¶
NewInt constructs a KindInt Value. Value's shape (design.md D-L) has a single str field carrying every non-secret kind's textual representation — there is no separate numeric field to invent, and no consumer in this change needs int arithmetic on a Value (that belongs to a later phase's execution engine, if it ever needs it).
func NewSecret ¶
NewSecret constructs a KindSecret Value wrapping handle. This is the ONLY constructor that ever sets the secret field, and it deliberately never touches str — a KindSecret Value's str is always the zero value, by construction, never by convention.
func Render ¶
Render combines tokens into a single Value, resolving each reference via resolve.
A field containing exactly one Reference token and no literal text resolves directly to that reference's Value — so a field whose entire content is `${{ secrets.tok }}` resolves end-to-end to a KindSecret Value, exactly as the workflow-execution spec requires ("Secret Interpolation Resolves To Typed Handles, Never Plaintext").
Any field mixing more than one token — literal text with a reference, or multiple references — must produce a single concatenated string, which requires calling String() on every resolved Value. Value's String() reports ok=false for KindSecret (design.md D-L: there is no accessor that returns a secret as a string), so Render rejects any such combination containing a secret reference BEFORE ever attempting to build the concatenated string — it never emits a partial result containing just the literal portion (tasks.md 5.4).
func (Value) Kind ¶
Kind reports the Value's kind. This is always safe to call and never exposes a secret's payload.
func (Value) Secret ¶
Secret returns v's *dagger.Secret handle and reports whether v actually carries one. It is the ONLY accessor that can observe a KindSecret Value's payload, and it never exposes a plaintext string — callers receive the same opaque handle type every other secret-consuming surface in this codebase already uses (pkg/shipwright's capability interfaces, internal/pipelines/shared/docker.go's client.SetSecret pattern).
func (Value) String ¶
String returns v's textual representation and reports whether one exists. It reports ok=false for a KindSecret Value — there is no accessor on Value that returns a secret as a string (design.md D-L). This check is explicit, not merely incidental to str being unset: even if a future refactor accidentally populated str alongside a secret, String() would still refuse to return it.