SCUA

How-to

Write tools for AI agents

SCUA is a strong target for one-shot tools an agent (or a shell harness) generates and runs: default-deny I/O, fuel/memory kill-switches, exact money, and a predictable stdout / stderr / exit contract. This page is the full shape of that contract.

For plain positional args without a tool envelope, see Read command-line arguments.

#The contract

Channel Role
stdin JSON payload (or empty)
stdout Exactly one JSON result
stderr Diagnostics only
exit status 0 success · 1 clean negative · 2 usage / bad args · other = crash
import sys

record Args {
  a: int where a in 0:1000000,
  b: int where b in 0:1000000,
}

let args = sys.parse_args(Args)
sys.emit({ ok = true, sum = args.a + args.b })
$ echo '{"a":2,"b":3}' | scua tool.scua
{"ok":true,"sum":5}

$ scua tool.scua --a 2 --b 3
{"ok":true,"sum":5}

$ scua tool.scua --a 1
{"error":{…missing field "b"…}}   # exit 2

Runnable end-to-end: examples/agent_tool.scua.

#Building the argument record

sys.parse_args(RecordType)

Declares the tool's interface once. The same record:

  1. validates CLI + stdin,
  2. runs every where refinement,
  3. fills defaults,
  4. is what scua schema turns into JSON Schema for callers (MCP inputSchema, etc.).

RecordType is a type name, not a value: write sys.parse_args(Args), never Args().

Sources of fields (CLI overlays stdin):

Source Shape
stdin one JSON object (if non-empty)
CLI --key value, --key=value, or --flag (→ true)

Positionals without -- are ignored by parse_args. A boundary violation (missing required field, wrong type, failed where) never runs the tool body — it calls sys.fail(…, 2).

#Lower-level pieces

Call Use when
sys.stdin() You want the raw UTF-8 payload yourself
sys.args() Positional strings only
sys.args_table() Named flags as a loose table, no record type

#Emitting a result or a failure

Call stdout stderr exit
sys.emit(value) one JSON value (via json.encode) 0
sys.fail(reason) {"error": …} 1
sys.fail(reason, code) {"error": …} code (0–255)
sys.exit(code) flushes prior prints code

sys.emit exits. Code after it never runs. For streaming / multi-line output use print and a final sys.exit.

decimal and money encode as lossless strings (never JSON numbers):

import sys
sys.emit({ total = 19.99 USD })
$ scua tool.scua
{"total":"19.99 USD"}

A fault (bug) is different from sys.fail: faults print a traceback and are for the author, not a chosen tool outcome.

#JSON Schema for callers

$ scua schema tool.scua --type Args

Emits JSON Schema draft 2020-12 for the decidable subset of the record (where ranges, membership enums, s.len() bounds, nested records via $defs). Arbitrary where predicates stay authoritative in SCUA; the schema says so in $comment and field descriptions.

#Teaching a model SCUA

$ scua pack --repo=. --tier=compact --out=-
$ scua pack --repo=. --tier=both

Builds a markdown teach-pack: compact grammar, the I/O paragraph above, collapsing primitives from the stdlib, few-shots from examples/, reserved words, and common pitfalls. Token estimates go to stderr. Prefer compact in a system prompt; regenerate after language changes.

#Sandbox the run

Agent-generated code should not get ambient authority:

$ scua --max-ops=10m --max-mem=64M --allow-fs=./scratch tool.scua < in.json
  • --max-ops / --max-mem — kill-switches the script cannot disarm
  • --allow-fs=DIR — rooted filesystem only (.. cannot climb out)
  • no --allow-net / --allow-env unless the tool needs them

See Read and write files and Read environment variables.