Help us shape the future of Go! Take the 2026 Go Developer Survey

external

package
v0.31.2 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 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) compiler.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