Skip to content

Exec

The exec provider runs an external verifier or helper program and asserts on its exit code and output. It is the bridge to logic Tales deliberately does not build in — hashing tools, format validators, custom CLIs — while keeping the DSL generic.

Use it for

  • Running an external verifier over a downloaded artifact and parsing its JSON.
  • Invoking a project script or CLI as part of an end-to-end flow.
  • Asserting a tool’s exit code, stdout / stderr, or structured output.

Don't use it for

  • Anything needing strong isolation — the process sandbox is not a security boundary (see below).
  • Deterministic, seed-replayable data — exec output and duration are inherently non-deterministic.
  • Shell pipelines — Tales calls the program directly; there is no shell.
step "exec" "verify_certificate" {
command = "./bin/artifact-verify"
args = [
"--certificate", result.download_certificate.path,
"--format", "json",
]
env = {
VERIFY_STRICT = "1"
}
timeout = "30s"
sandbox {
mode = "process"
workdir = "scenario"
env = "minimal"
network = false
}
expect {
exit_code = 0
stdout_json = {
valid = true
hash_algorithm = "SHA512"
}
}
capture {
merkle_root = stdout.json.merkle.root
}
}

Run a script through an interpreter by naming the interpreter as the command:

step "exec" "verify_with_python" {
command = "python3"
args = ["${project.dir}/scripts/verify_proof.py", result.download.path]
timeout = "30s"
expect { exit_code = 0 }
}

Tales calls the executable directly with its args. command = "bash", args = ["script.sh"] runs bash with that argument; command = "bash -c ..." is not split or interpreted — it is treated as a single program name and will fail to resolve. There is no variable expansion, globbing, or piping.

FieldNotes
commandRequired. The program to run (see command resolution below).
argsOptional list(string).
envOptional map(string) overlaid on the base environment.
stdinOptional string piped to the program’s stdin.
timeoutOptional duration (default 30s). On timeout the process is killed and the step fails.
sandboxOptional. See below.
FormResolution
Bare name (python3)Looked up on PATH.
Relative (./bin/tool)Resolved under project.dir; must stay within it.
Absolute (${project.dir}/bin/tool)Allowed only within the scenario workdir or the project dir.

Absolute paths outside those roots (/usr/bin/python3, /tmp/tool) are rejected — reference system interpreters by bare name so resolution stays deterministic across machines.

The response exposes:

FieldNotes
exec.exit_codeProcess exit status.
exec.duration_msWall-clock duration (non-deterministic).
stdout.rawCaptured stdout (capped at 1 MiB).
stdout.jsonParsed stdout JSON, or null when stdout is not JSON.
stdout.truncatedtrue when stdout exceeded the cap.
stderr.rawCaptured stderr (capped at 1 MiB).
stderr.truncatedtrue when stderr exceeded the cap.
expect {
exit_code = 0
stdout = contains("ok")
stderr = ""
stdout_json = { valid = true }
}

exit_code, stdout, stderr (matched against the raw streams) and stdout_json (matched against the parsed JSON) are all optional. When stdout_json is declared but stdout is not valid JSON, the step fails clearly.

When a stream exceeds 1 MiB it is truncated, truncated is set, and the truncated bytes are still written to the artifact.

Every exec step writes, under scenario.workdir/exec/<step-name>/:

FileContents
stdout.txtCaptured stdout (possibly truncated).
stderr.txtCaptured stderr (possibly truncated).
metadata.jsoncommand, args count, exit_code, duration_ms, timed_out, stdout/stderr paths, truncation flags, workdir, sandbox mode, network.
stdout.jsonParsed stdout, when it is valid JSON.

metadata.json never contains environment values or argument values, so secrets passed via env or args are not persisted. The console / JSONL report is metadata-only and never dumps raw stdout / stderr.

sandbox {
mode = "process" # process (default) | docker (reserved)
workdir = "scenario" # scenario (default) | project | <custom path>
env = "minimal" # minimal (default) | inherit
network = false # advisory in process mode
}
  • workdir: scenario runs in scenario.workdir, project in project.dir, any other value is a custom path resolved under the scenario workspace.
  • env: minimal exposes only PATH, a scenario-local TMPDIR, HOME (set to the workdir) and your env entries. inherit starts from the host environment. The host environment is never fully leaked by default.
  • network: advisory in process mode (recorded in metadata, not enforced).

Unlike Tales’ generators, exec is not seed-replayable: a program reads real files, the clock, and possibly the network, and exec.duration_ms is real wall-clock time. Pin flakiness with timeout and assert only on stable output.