interp

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

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.

const (
	KindString Kind = iota
	KindInt
	KindBool
	KindSecret
)

func (Kind) String

func (k Kind) String() string

String renders a Kind's name for error messages and diagnostics. This is safe for a KindSecret value itself (it only ever names the KIND, it never touches a Value's payload) — do not confuse this with Value.String(), which is the one that must never leak a secret.

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).

const (
	NamespaceVariables Namespace = iota
	NamespaceSecrets
	NamespaceSteps
)

type Reference

type Reference struct {
	Namespace Namespace
	Name      string
	StepID    string
}

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

func (r Reference) StaticKind() (Kind, bool)

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

type Token struct {
	Kind    TokenKind
	Literal string
	Ref     Reference
}

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

func Scan(s string) ([]Token, error)

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 TokenKind

type TokenKind int

TokenKind identifies what a Token carries.

const (
	// TokenLiteral is a run of ordinary text with no interpolation.
	TokenLiteral TokenKind = iota
	// TokenReference is a resolved "${{ ... }}" placeholder.
	TokenReference
)

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

func NewBool(b bool) Value

NewBool constructs a KindBool Value, following the same single-str-field representation as NewInt.

func NewInt

func NewInt(n int64) Value

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

func NewSecret(handle *dagger.Secret) Value

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 NewString

func NewString(s string) Value

NewString constructs a KindString Value.

func Render

func Render(tokens []Token, resolve func(Reference) (Value, error)) (Value, error)

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

func (v Value) Kind() Kind

Kind reports the Value's kind. This is always safe to call and never exposes a secret's payload.

func (Value) Secret

func (v Value) Secret() (handle *dagger.Secret, ok bool)

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

func (v Value) String() (s string, ok bool)

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.

Jump to

Keyboard shortcuts

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