external

package
v0.20.3 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package external provides three actions for calling code that lives outside the Flow process: exec (shell), http.request (HTTP client), and wasm (Wazero WASI modules). This is the polyglot escape hatch — any repository can expose a Bundle that wraps its own logic through one of these.

Typical use:

external.exec @{ cmd: "python3 enhancer.py", input: .payload }
http.request @{ url: "https://api.example/v1/items", method: "GET" }
external.wasm @{ path: "plugins/transform.wasm", input: .data }

Index

Constants

View Source
const ID = "external"

Variables

View Source
var Exec = action.New("external.exec", func(ctx context.Context, in map[string]any) (ExecResult, error) {
	command := readStringField(in, "cmd", "command")
	if command == "" {
		return ExecResult{}, xerr.BadRequest("exec: 'cmd' parameter is required")
	}

	cmd := shellCommand(ctx, command)

	if input, ok := in["input"]; ok && input != nil {
		cmd.Stdin = bytes.NewReader(inputBytes(input))
	}

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	runErr := cmd.Run()
	result := ExecResult{
		Stdout:   stdout.String(),
		Stderr:   stderr.String(),
		ExitCode: 0,
		OK:       runErr == nil,
		Output:   decodeJSON(stdout.String()),
	}

	if runErr == nil {
		return result, nil
	}

	var exitErr *exec.ExitError
	if !asExitError(runErr, &exitErr) {
		return result, xerr.Internal("exec " + command + ": " + runErr.Error())
	}
	result.ExitCode = exitErr.ExitCode()

	return result, xerr.Internal(fmt.Sprintf(
		"exec %q failed with exit code %d: %s",
		command, result.ExitCode, strings.TrimSpace(result.Stderr)))
}).
	Description("Execute external shell/CLI command").
	Tag("external", "exec", "os").
	Build()
View Source
var HTTPRequest = action.New("http.request", func(ctx context.Context, in map[string]any) (HTTPResult, error) {
	reqURL := readStringField(in, "url")
	if reqURL == "" {
		return HTTPResult{}, xerr.BadRequest("http.request: 'url' parameter is required")
	}

	method := strings.ToUpper(readStringField(in, "method"))
	if method == "" {
		method = http.MethodGet
	}

	body, err := buildBody(in["body"])
	if err != nil {
		return HTTPResult{}, xerr.BadRequest("http.request: invalid body: " + err.Error())
	}

	req, err := http.NewRequestWithContext(ctx, method, reqURL, body)
	if err != nil {
		return HTTPResult{}, xerr.Internal("http.request: build request", err)
	}

	if headers, ok := in["headers"].(map[string]any); ok {
		for k, v := range headers {
			req.Header.Set(k, fmt.Sprint(v))
		}
	}

	resp, err := httpClient.Do(req)
	if err != nil {
		return HTTPResult{}, xerr.Unavailable(fmt.Sprintf("http.request: %s %s failed", method, reqURL), err)
	}
	defer func() { _ = resp.Body.Close() }()

	raw, err := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
	if err != nil {
		return HTTPResult{}, xerr.Internal("http.request: read body", err)
	}

	result := HTTPResult{Status: resp.StatusCode, Body: decodeBody(raw)}

	if resp.StatusCode >= 400 {
		return result, errorFromStatus(resp.StatusCode, method, reqURL)
	}
	return result, nil
}).
	Description("Send HTTP request and decode result").
	Tag("external", "http", "network").
	Build()

HTTPRequest performs an HTTP request and maps the status to a classified error kind (429 → TooManyRequests, 5xx → Unavailable, …). The response body is always returned, even on error, so a fallback pipeline can inspect it.

View Source
var WASM = action.New("external.wasm", func(ctx context.Context, in map[string]any) (ExecResult, error) {
	path := readStringField(in, "path")
	if path == "" {
		return ExecResult{}, xerr.BadRequest("wasm: 'path' parameter is required")
	}

	entry, err := getOrCompileWASM(ctx, path)
	if err != nil {
		return ExecResult{}, err
	}

	var stdin []byte
	if input, ok := in["input"]; ok && input != nil {
		stdin = inputBytes(input)
	}

	var stdout, stderr bytes.Buffer
	moduleConfig := wazero.NewModuleConfig().
		WithStdin(bytes.NewReader(stdin)).
		WithStdout(&stdout).
		WithStderr(&stderr).
		WithName("")

	mod, runErr := entry.runtime.InstantiateModule(ctx, entry.compiled, moduleConfig)
	if mod != nil {
		defer mod.Close(ctx)
	}

	result := ExecResult{
		Stdout: stdout.String(),
		Stderr: stderr.String(),
		OK:     runErr == nil,
		Output: decodeJSON(stdout.String()),
	}

	if runErr == nil {
		return result, nil
	}

	exitErr := &sys.ExitError{}
	if errors.As(runErr, &exitErr) {
		result.ExitCode = int(exitErr.ExitCode())
		result.OK = false
		return result, xerr.Internal(fmt.Sprintf(
			"wasm %q exited with code %d: %s",
			path, result.ExitCode, strings.TrimSpace(result.Stderr)))
	}

	return result, xerr.Internal(fmt.Sprintf("wasm %q: %v", path, runErr))
}).
	Description("Execute sandboxed WebAssembly module with stdin/stdout JSON bridging").
	Tag("external", "wasm", "sandbox").
	Build()

WASM executes a WebAssembly module compiled with WASI. The first call compiles the module and caches it; every call instantiates a fresh instance so module state does not leak between invocations.

Functions

func Bundle

func Bundle(_ map[string]string) core.Bundle

func Library

func Library() action.Library

Types

type ExecResult

type ExecResult struct {
	Stdout   string `json:"stdout"`
	Stderr   string `json:"stderr"`
	ExitCode int    `json:"exit_code"`
	OK       bool   `json:"ok"`
	Output   any    `json:"output,omitempty"`
}

ExecResult is the shape returned by Exec and WASM. It carries both the raw streams and, when stdout parses as JSON, the structured Output. Ok is a convenience flag for exit_code == 0.

type HTTPResult

type HTTPResult struct {
	Status int `json:"status"`
	Body   any `json:"body"`
}

HTTPResult carries the status and the decoded body. JSON responses are decoded into any; other bodies are returned as a string.

Jump to

Keyboard shortcuts

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