flow

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package flow is the declarative action interpreter: it lets an extension express multi-step behaviour as data instead of compiled code. A Flow is an ordered list of steps (http, transform, condition, loop, call, return) the interpreter runs against a scope, where steps reference prior outputs and configuration by path. Values flow as ordinary decoded JSON (nil, bool, float64, string, []any, map[string]any), so a spec the resource store admits is also exactly what runs.

The expression sub-language is deliberately small and side-effect-free: it reads the scope and calls a fixed whitelist of pure functions, and nothing else. It cannot reach the filesystem, spawn a process, or open a connection. The only effects a flow has are its declared http and call steps, both of which go through injected ports the host governs at the dispatch boundary. This file is the expression lexer and parser; eval.go evaluates the resulting tree.

Index

Constants

This section is empty.

Variables

View Source
var DefaultLimits = Limits{
	MaxSteps:          256,
	MaxLoopIterations: 1000,
	TimeoutMillis:     30_000,
	MaxPayloadBytes:   5 << 20,
}

DefaultLimits is the conservative envelope applied when a flow or the interpreter sets no override. The numbers are deliberately modest: a flow that needs more is declaring an unusual shape and should say so explicitly.

Functions

This section is empty.

Types

type AssertAction

type AssertAction struct {
	That    string `json:"that"`
	Message string `json:"message,omitempty"`
}

AssertAction fails the flow when That is not truthy, with an optional templated Message. It is how a flow verifies an outcome and stops with a clear error when the outcome does not hold, rather than continuing on a false assumption.

type CallAction

type CallAction struct {
	Tool  string         `json:"tool"`
	Input map[string]any `json:"input,omitempty"`
}

CallAction invokes another tool or extension by name (templated) with an Input object (deep-templated). The result becomes this step's output.

type ConditionAction

type ConditionAction struct {
	If   string `json:"if"`
	Then []Step `json:"then,omitempty"`
	Else []Step `json:"else,omitempty"`
}

ConditionAction branches: if If is truthy, run Then, else run Else. A branch is itself a list of steps, so conditions nest.

type ConfirmAction

type ConfirmAction struct {
	Message string `json:"message"`
}

ConfirmAction pauses the flow to show Message (templated) and wait for the user to approve before continuing. It is how a flow makes an interactive step visible and gets the operator's go-ahead rather than acting silently, and how a non-interactive run stops with a clear instruction instead of blocking. It produces no output.

type Confirmer

type Confirmer interface {
	Confirm(ctx context.Context, message string) error
}

Confirmer performs the confirm steps of a flow: it shows the user a message and waits for them to approve before the flow continues. The host wires how that is asked (an interactive terminal prompt for a person, a fail-closed instruction for a non-interactive run). Returning a non-nil error stops the flow, so a declined or unanswerable confirmation aborts rather than proceeding without consent.

type CredentialSink

type CredentialSink interface {
	Put(ctx context.Context, sink, ref string, target map[string]string) error
}

CredentialSink performs the secret steps of a flow: it materializes a secret, named by a reference, into a target's secret store (a hosting provider's secrets, a deployed workload's environment). The implementation resolves the reference to its value through the host's secret source and delivers it to the named sink, so the value never enters the flow as data: a step passes only the reference, the sink, and the target. Returning a non-nil error fails the step. Without it, a secret step fails closed.

type DependencyAction

type DependencyAction struct {
	Name string `json:"name"`
}

DependencyAction ensures the named external program is present, provisioning a pinned build when it is missing, and yields {path} to run it. Name is templated.

type DependencyResolver

type DependencyResolver interface {
	Resolve(ctx context.Context, name string) (path string, err error)
}

DependencyResolver performs the dependency steps of a flow: it ensures an external program is present (provisioning a pinned build when missing) and returns the path to run it. The host wires the dependency manager, so a flow declares what it needs without knowing how it is obtained.

type ExecAction

type ExecAction struct {
	Command      string `json:"command"`
	AllowNonzero bool   `json:"allowNonzero,omitempty"`
}

ExecAction runs Command (templated) through the injected runner, which confines it in the sandbox. The step output is {exitCode, output}. A nonzero exit fails the step unless AllowNonzero is set, so a command failure stops the flow by default; set AllowNonzero to inspect the exit code in a later step instead.

type ExecRequest

type ExecRequest struct {
	Command string
}

ExecRequest is one command an exec step asks the host to run. It is a value type, free of os/exec, so the interpreter never spawns a process itself: the host runs the command through the sandbox, where confinement, egress policy, and timeouts live.

type ExecResult

type ExecResult struct {
	ExitCode int
	Output   string
}

ExecResult is the outcome of a command: its exit code and combined output.

type Execer

type Execer interface {
	Exec(ctx context.Context, req ExecRequest) (ExecResult, error)
}

Execer performs the exec steps of a flow by running a command through the sandbox. Without it, an exec step fails closed, so a flow can never reach a process except through a host that confines it.

type Flow

type Flow struct {
	Steps  []Step  `json:"steps"`
	Limits *Limits `json:"limits,omitempty"`
	// contains filtered or unexported fields
}

Flow is a declarative procedure: an ordered list of steps the interpreter runs to produce a result. It decodes from the JSON an extension surface carries, so the stored spec is exactly what executes.

func Decode

func Decode(raw []byte) (Flow, error)

Decode parses a Flow from JSON, validates it, and compiles it: every expression, template, and templated body is parsed exactly once here, and execution reuses the parsed form. A flow that does not compile is never returned, so the interpreter only ever runs well-formed flows.

func (Flow) Validate

func (f Flow) Validate() error

Validate checks a flow's structure: every step's Op matches exactly one action block, ids are unique, control-flow bodies recurse, and every expression and template parses. It is static admission: a flow that passes never fails later for a structural reason, only for a runtime value error or a resource cap.

type HTTPAction

type HTTPAction struct {
	Method  string            `json:"method,omitempty"` // default GET
	URL     string            `json:"url"`
	Headers map[string]string `json:"headers,omitempty"`
	Query   map[string]string `json:"query,omitempty"`
	Body    json.RawMessage   `json:"body,omitempty"`
}

HTTPAction is a single request. Method, URL, and the values of Headers and Query are templated; Body is deep-templated (every string leaf), so a request is built from prior outputs and config without code.

type HTTPDoer

type HTTPDoer interface {
	Do(ctx context.Context, req HTTPRequest) (HTTPResponse, error)
}

HTTPDoer performs the http steps of a flow. The host's implementation is where egress confinement, credential injection, rate limiting, and retries live, so the interpreter never reaches the network itself.

type HTTPRequest

type HTTPRequest struct {
	Method  string
	URL     string
	Headers map[string]string
	Query   map[string]string
	Body    []byte
}

HTTPRequest is one outbound request the interpreter asks the host to perform. It is a value type, free of net/http, so the interpreter stays transport-agnostic and fully testable: the host adapts its governed transport to this port.

type HTTPResponse

type HTTPResponse struct {
	Status  int
	Headers map[string]string
	Body    any
	Raw     []byte
}

HTTPResponse is the result of a request, decoded for use by later steps. Body is the parsed JSON value when the payload is JSON, otherwise the raw text; Raw is the unparsed bytes, against which the payload cap is checked.

type Interpreter

type Interpreter struct {
	// contains filtered or unexported fields
}

Interpreter runs flows. It holds the injected effect ports and the default limits; it is safe for concurrent use because Run keeps all mutable run state local.

func New

func New(opts ...Option) *Interpreter

New builds an interpreter. With no ports, it can still run pure flows (transform, condition, loop, return); http and call steps require their ports.

func (*Interpreter) Run

func (in *Interpreter) Run(ctx context.Context, f Flow, config map[string]any) (any, error)

Run executes a flow against the given configuration and returns its result. The result is whatever a return step yields; a flow that falls off the end with no return yields nil. config is exposed to expressions as "config"; step outputs accumulate under "steps".

type Limits

type Limits struct {
	// MaxSteps caps total step executions across the whole run (loop bodies included).
	MaxSteps int `json:"maxSteps,omitempty"`
	// MaxLoopIterations caps total loop iterations across the whole run.
	MaxLoopIterations int `json:"maxLoopIterations,omitempty"`
	// TimeoutMillis caps wall-clock duration, measured against the injected clock.
	TimeoutMillis int `json:"timeoutMillis,omitempty"`
	// MaxPayloadBytes caps a single http response body; a larger response fails closed.
	MaxPayloadBytes int `json:"maxPayloadBytes,omitempty"`
}

Limits bound a flow's resource use so a runtime-authored flow cannot wedge or amplify. A zero field takes the interpreter's default; DefaultLimits documents those. They are enforced as terminal faults: exceeding one stops the flow, it is never retried into the same wall.

type LoopAction

type LoopAction struct {
	Over    string `json:"over,omitempty"`
	Count   int    `json:"count,omitempty"`
	As      string `json:"as,omitempty"`
	Body    []Step `json:"body,omitempty"`
	Collect string `json:"collect,omitempty"`
}

LoopAction repeats Body. Over is an expression yielding a list to iterate; when empty, Count gives a fixed iteration count. As names the element binding (default "item"); the zero-based index is always bound as "index". When Collect is set, it is evaluated each iteration and the results are gathered into this step's output list, so a loop can build a result.

type Observer

type Observer interface {
	Step(StepEvent)
}

Observer watches a flow execute, step by step. The interpreter reports each observable step as it begins (StepBegin) and after it ends (StepEnd), so a host can show progress while a long-running procedure (provisioning a tool, deploying an app) happens rather than only seeing the final result. A nil Observer disables reporting. Implementations must not block or retain the event past the call.

type Op

type Op string

Op names one action an interpreter can execute. The set is closed and small: it is the entire vocabulary a declarative flow has, chosen so a runtime-authored spec can express useful behaviour without any path to arbitrary code.

const (
	// OpHTTP performs one HTTP request through the injected transport.
	OpHTTP Op = "http"
	// OpTransform reshapes a value (extract, project, filter, map) using expressions.
	OpTransform Op = "transform"
	// OpCondition branches on a boolean expression.
	OpCondition Op = "condition"
	// OpLoop repeats a body over a list or a fixed count.
	OpLoop Op = "loop"
	// OpCall invokes another tool or extension through the injected caller.
	OpCall Op = "call"
	// OpReturn yields the flow's result and stops.
	OpReturn Op = "return"
	// OpAssert checks a condition and fails the flow when it is false, so a flow can
	// verify an outcome (a resource exists, a status is healthy) before continuing.
	OpAssert Op = "assert"
	// OpExec runs a command through the injected runner, which confines it in the
	// sandbox, so a flow can drive an external command-line program.
	OpExec Op = "exec"
	// OpDependency ensures an external program is present, provisioning a pinned build
	// when it is missing, and yields the path to run it.
	OpDependency Op = "dependency"
	// OpConfirm pauses the flow to show the user a message and wait for them to approve
	// before continuing, so an interactive step (a browser login, a destructive action)
	// is surfaced and consented to rather than happening silently.
	OpConfirm Op = "confirm"
	// OpSecret materializes a secret into a target's secret store (a provider's secrets,
	// a deployed workload's environment) by reference. The step names only the reference,
	// the sink, and the target; the host resolves the reference to its value and delivers
	// it, so the secret value never enters the flow as data, a rendered command, or a log.
	OpSecret Op = "secret"
)

type Option

type Option func(*Interpreter)

Option configures an Interpreter.

func WithClock

func WithClock(c clock.Clock) Option

WithClock sets the time source the timeout cap measures against (default clock.System). Tests pass a Manual clock for determinism.

func WithConfirm

func WithConfirm(c Confirmer) Option

WithConfirm sets the port that asks the user to approve confirm steps. Without it, a confirm step fails closed.

func WithCredentialSink

func WithCredentialSink(s CredentialSink) Option

WithCredentialSink sets the port that materializes secret steps into a target's secret store. Without it, a secret step fails closed.

func WithDependencies

func WithDependencies(d DependencyResolver) Option

WithDependencies sets the port that resolves dependency steps. Without it, a dependency step fails closed.

func WithExec

func WithExec(e Execer) Option

WithExec sets the port that runs exec steps through the sandbox. Without it, an exec step fails closed.

func WithHTTP

func WithHTTP(d HTTPDoer) Option

WithHTTP sets the port that performs http steps. Without it, an http step fails closed.

func WithLimits

func WithLimits(l Limits) Option

WithLimits overrides the default resource caps.

func WithObserver

func WithObserver(o Observer) Option

WithObserver sets the port that watches each observable step as it runs, so a host can show progress while a flow executes. Without it, a flow still runs; nothing is reported.

func WithTools

func WithTools(c ToolCaller) Option

WithTools sets the port that performs call steps. Without it, a call step fails closed.

type ReturnAction

type ReturnAction struct {
	Value json.RawMessage `json:"value,omitempty"`
}

ReturnAction yields the flow result. Value is deep-templated, so it can be a literal shape with holes or a single expression yielding a typed value.

type SecretAction

type SecretAction struct {
	Ref    string            `json:"ref"`
	Sink   string            `json:"sink"`
	Target map[string]string `json:"target,omitempty"`
}

SecretAction materializes the secret named by Ref into Sink's secret store, with Target carrying the sink's parameters (for a hosting provider: the app and the key name). Ref, Sink, and every Target value are templated, so the reference and target are built from config and prior steps. The host resolves Ref to its value and hands it to the sink, so the value never appears in the flow's expressions, the rendered command, or a log: a step declares WHICH secret goes WHERE, never the secret itself. It produces no output.

type Step

type Step struct {
	ID  string `json:"id,omitempty"`
	Op  Op     `json:"op"`
	Doc string `json:"doc,omitempty"`

	HTTP       *HTTPAction       `json:"http,omitempty"`
	Transform  *TransformAction  `json:"transform,omitempty"`
	Condition  *ConditionAction  `json:"condition,omitempty"`
	Loop       *LoopAction       `json:"loop,omitempty"`
	Call       *CallAction       `json:"call,omitempty"`
	Return     *ReturnAction     `json:"return,omitempty"`
	Assert     *AssertAction     `json:"assert,omitempty"`
	Exec       *ExecAction       `json:"exec,omitempty"`
	Dependency *DependencyAction `json:"dependency,omitempty"`
	Confirm    *ConfirmAction    `json:"confirm,omitempty"`
	Secret     *SecretAction     `json:"secret,omitempty"`
}

Step is one action in a flow. ID names the step so later steps can reference its output by path (steps.<id>). Exactly one action field is set, and it must match Op; Validate enforces that pairing so a malformed step is rejected before it runs.

type StepEvent

type StepEvent struct {
	// Phase is whether the step is beginning or has ended.
	Phase StepPhase
	// Op is the step's op (OpExec or OpDependency).
	Op Op
	// Detail is the human-readable subject: the rendered command for an exec step, the
	// dependency name for a dependency step.
	Detail string
	// ExitCode is the command's exit code, set on StepEnd for an exec step.
	ExitCode int
	// Output is the command's combined output, set on StepEnd for an exec step.
	Output string
	// Err is set on StepEnd when the step failed.
	Err error
}

StepEvent describes one observable step of a running flow. It is reported to an Observer as the step begins and again as it ends, so a host can show progress while a flow runs instead of only seeing the final result. Only steps with an external effect worth watching are reported, the command and dependency steps; pure steps (transform, condition, loop, return) are not, because they have nothing to watch.

type StepPhase

type StepPhase int

StepPhase marks whether a StepEvent is reported as a step's effect begins or after it ends.

const (
	// StepBegin is reported just before a step's external effect runs.
	StepBegin StepPhase = iota
	// StepEnd is reported after a step's external effect completes, carrying its outcome.
	StepEnd
)

type ToolCaller

type ToolCaller interface {
	Call(ctx context.Context, tool string, input json.RawMessage) (any, error)
}

ToolCaller performs the call steps of a flow: it invokes another tool or extension by name. The host gates the call at the dispatch boundary, so a flow cannot call anything the run is not authorised for.

type TransformAction

type TransformAction struct {
	Source string            `json:"source,omitempty"`
	Value  string            `json:"value,omitempty"`
	Select map[string]string `json:"select,omitempty"`
	Filter string            `json:"filter,omitempty"`
	Map    string            `json:"map,omitempty"`
}

TransformAction reshapes Source (an expression yielding the input value) into a new value. Exactly one mode applies:

  • Value: yield the expression's result directly (extract / rename).
  • Select: build an object from outKey -> expression, each evaluated against the source bound as "it".
  • Filter (lists): keep elements where the expression is truthy, element as "it".
  • Map (lists): replace each element with the expression's result, element as "it".

Jump to

Keyboard shortcuts

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