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 ¶
- Variables
- type AssertAction
- type CallAction
- type ConditionAction
- type ConfirmAction
- type Confirmer
- type CredentialSink
- type DependencyAction
- type DependencyResolver
- type ExecAction
- type ExecRequest
- type ExecResult
- type Execer
- type Flow
- type HTTPAction
- type HTTPDoer
- type HTTPRequest
- type HTTPResponse
- type Interpreter
- type Limits
- type LoopAction
- type Observer
- type Op
- type Option
- func WithClock(c clock.Clock) Option
- func WithConfirm(c Confirmer) Option
- func WithCredentialSink(s CredentialSink) Option
- func WithDependencies(d DependencyResolver) Option
- func WithExec(e Execer) Option
- func WithHTTP(d HTTPDoer) Option
- func WithLimits(l Limits) Option
- func WithObserver(o Observer) Option
- func WithTools(c ToolCaller) Option
- type ReturnAction
- type SecretAction
- type Step
- type StepEvent
- type StepPhase
- type ToolCaller
- type TransformAction
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
WithClock sets the time source the timeout cap measures against (default clock.System). Tests pass a Manual clock for determinism.
func WithConfirm ¶
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 ¶
WithExec sets the port that runs exec steps through the sandbox. Without it, an exec step fails closed.
func WithHTTP ¶
WithHTTP sets the port that performs http steps. Without it, an http step fails closed.
func WithObserver ¶
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.
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".