Runtime and security
Runtime, executions, and execution security
1. Reference runtime (t0-runtime)#
- Executes a validated artifact's
entryfunction by walking the IR. - Values:
Unit, Bool, Int, Float, Decimal, String, Price, Quantity, Money, Ratio, Currency, Timestamp, Duration, List, Struct, Variant, Handle, Strategy; lists, structs and strings are reference-counted with copy-on-write, giving value semantics without copying on every assignment. - Host calls go through
trait Host { fn invoke(&self, call: &HostCall, ctx: &CallContext) -> Result<HostValue, HostFailure> }.CallContextgives the host the execution id, the cancellation token and a progress sink. Results are converted to runtime values and type-checked against the operation's declared return type. printgoes to anOutputsink provided by the caller.
2. Limits (defaults; the host that starts an execution may change them, a program never can)#
Every limit says how it holds: strict — enforced inside the stated subsystem, no way around it there; delegated — enforced by the service that does the work; needs isolation — not achievable in-process, waits for the worker process of ADR-0009.
| limit | default | error | scope |
|---|---|---|---|
| fuel (evaluation steps: one per statement and per expression node) | 200 000 000 | E-RT-FUEL |
strict, interpreter |
| call depth | 256 | E-RT-DEPTH |
strict, interpreter |
live bytes of the program's strings, lists and records (max_memory) |
512 MiB | E-RT-LIMIT-MEMORY |
strict for those values (t0_runtime::budget); see below for what it does not cover |
| elements in one list / bytes in one string | 10 000 000 / 64 MiB | E-RT-LIMIT-COLLECTION |
strict, interpreter and standard library (checked before building) |
| wall time of an execution | 30 min | E-RT-TIMEOUT |
strict at the interpreter's checks (every 1 024 steps); a host operation in progress finishes first |
lines / bytes print may write (max_output_lines, max_output_bytes) |
10 000 / 4 MiB | none: one [output truncated: …] line, the rest counted in output_dropped |
strict, interpreter; a value's text is built only up to the room left |
| host operations one execution may call | 100 000 | E-RT-LIMIT-HOST-CALLS |
strict, interpreter |
nesting of expressions, blocks, types, patterns, else if chains |
128 | E-SYN-TOO-DEEP |
strict, parser (every later recursive pass inherits it) |
candles per market.candles / trades per backtest.trades / features per research.market_state |
200 000 / 200 000 / 64 | E-MKT-TOO-MANY / E-BT-TOO-MANY / E-RS-FEATURE |
strict, host (t0-host), before the data is built: a candle range is sized from its extent before reading; a journal is read as a stream that stops past the limit (pages: backtest.trades_page); the research replay streams its events and keeps only the requested features |
| running backtests per program / per session / per terminal | 4 / 6 / cores − 1 (1…8) | E-BT-CAPACITY (refused, never queued) |
strict, host (t0-host::jobs), shared by every session of the process |
| a backtest's own resources (events, orders, memory of the engine) | TTT's BacktestConfig limits |
the run's own Failed* status |
delegated to TTT's engine |
| memory of the whole process, CPU time, file descriptors | — | — | needs isolation |
What the memory budget covers, exactly#
Counted, as live bytes: every string (its bytes) and every list or record
(its element storage by capacity, plus the box each boxed element owns),
from the moment it is built until its last reference drops — including
strings and lists a host returned, charged on conversion. Released by
Drop on every path (success, error, cancellation, a caught panic), so
nothing leaks (property test the_budget_always_equals_what_live_values_pay).
Refused before allocation whenever the size is known (list.range, joins,
replacements, copies, list literals, a push that would reallocate);
otherwise checked after every evaluation step, so one operation can
overshoot by at most its own result, which the length limits bound.
Not counted: scalars and boxed values outside collections (bounded by call depth × frame size), the interpreter's frames, the compiler's work, a host's transient buffers before conversion (each operation bounds its own result, table above), the backtest engine's memory (delegated), and allocator overhead. It is accounting inside the process, not a memory limit the operating system enforces: it stops a program from growing its values past the budget; it cannot stop other code in the terminal from allocating, and a defect in the runtime itself could still exhaust memory. A hard bound needs the worker process (ADR-0009).
Reviewed 2026-10-10 and measured the same day
(docs/benchmarks/2026-10-10-resource-policy.md): the defaults below
stay. Why these values:
- Fuel bounds CPU per execution independently of wall time (a busy machine must not change a program's result from "completed" to "timeout" at the same step count). 2·10⁸ steps is minutes of interpretation — more than any analysis we ship needs.
- Depth 256 with a 256 MiB reserved stack leaves a large margin even in debug builds.
- Memory 512 MiB holds several real price series and their
indicators (a 10⁶-element
List<Option<Float>>costs about 80 MB) on a terminal machine of 8–16 GB that also runs the market feed and charts. - Nesting 128 bounds every later recursive pass (checker, lowering,
validator, interpreter, formatter, highlighter) once, in the parser. It
was 256 at first; that overflowed a 2 MiB thread in a debug build, and
the editor parses on the UI thread for suggestions. 128 parses on a
2 MiB thread with room to spare (test
nesting_is_bounded_and_safe_on_a_small_stack). - Output 10 000 lines / 4 MiB keeps a looping
printfrom filling memory; the terminal's window keeps the latest 2 000 lines, counts what it let go (output_trimmed), and copies them to the window only when they changed. - Host calls 100 000 stops a loop hammering the host (UI queue, dataset reads) long before it matters to the terminal.
- Backtests 4 / 6 / cores − 1: each job is one engine thread replaying a dataset from disk; the terminal-wide gate keeps a core for the terminal's own frame loop and does not grow with the number of sessions.
Cancellation#
Two different things are stopped (ADR-0010 §5):
- The program — Stop program in the window, an interrupt of
t0 run, the terminal closing. Its token is checked every 1 024 steps, before every host call, and inside host operations markedcancellable(backtest.wait,market.candles, UI requests). Every job it started is stopped with it. The execution endsCancelled— also when it stopped on an error after the cancellation was asked for — neverFailed. - One job — Stop job in the window,
backtest.cancel. The engine stops at its next event boundary; the program goes on and readsStoppedByUser.
Interruptible within an operation (2026-10-10): the research replay
(every 1 024 events), reading a run's journal (every 4 096 events), a
candle read (between partitions), waiting for a backtest or the UI, and
the window indicators of the standard library (ind.sma,
ind.rolling_stdev, every ~4·10⁶ element reads). Not interruptible: the
opening and verification of a dataset before a replay or a read, one
list.sort, and the other standard-library calls, which are linear in
their input (at most 10⁷ elements); cancellation is observed when they
return. Measured latencies: docs/benchmarks/2026-10-10-resource-policy.md.
3. Executions#
Execution::spawn(request) → thread "t0-exec-<id>"
Compiling → (Refused | Running) → (Completed | Failed | Cancelled)ExecutionHandle { id, snapshot(), cancel(), is_finished(), try_take() -> Outcome }— the same shape as TTT'sRunJob, polled by the UI once per frame, never blocking it.Outcome = Completed{ value } | Failed{ code, message, span } | Cancelled | Refused{ diagnostics }, plus metadata: artifact body hash, versions, contracts used, steps used.- A panic inside the thread is caught (
catch_unwind) and becomesFailed{E-RT-INTERNAL}. - Compilation and execution always run on a thread with a 256 MiB reserved
stack (
t0_exec::EXECUTION_STACK), whoever calls them. The parser and the interpreter recurse, and the language's own depth limit must be what stops a deep program — found when 256 nested calls overflowed a 2 MiB test thread and aborted the process instead of failing withE-RT-DEPTH. Reserved, not committed: untouched pages cost nothing.
4. Security model#
Threat: a .tz program (written by a user, generated by Fathom, or
received from someone else) or an IR artifact that was altered.
| control | where |
|---|---|
| no ambient authority: no file, network, process, clock or environment access in the language | t0-runtime has no such builtins |
| every effect is a declared, versioned host operation | compiler (E-CAP-UNDECLARED), validator (E-IR-CAPABILITY) |
| permissions granted per session by the user, never persisted | host session (ADR-0008) |
| permission re-checked on every host call | host dispatch |
| artifacts revalidated before execution, whatever produced them | t0-ir::validate |
| host return values type-checked | t0-runtime |
| resource limits | t0-runtime |
| UI mutations only via the host queue, on semantic handles | ADR-0003 |
no live trading in v0: no contract offers orders.submit |
host API 0.1 |
| diagnostics never carry credentials or account data | host errors are built from fixed text plus values the program itself supplied |
Trust model#
| component | trusted for | not trusted for |
|---|---|---|
| the person at the terminal | granting permissions, choosing files, pressing Run | — |
.tz source (theirs, generated, or received) |
nothing: it is checked and runs with what is granted | anything it claims about itself: it cannot grant itself permissions, and a requires line is a request, not a grant |
| an IR artifact | nothing: re-validated (structure, hash, capabilities, versions) before every execution, whoever built it | — |
| the compiler | producing diagnostics | its own artifacts are validated like anyone's |
the reference runtime (t0-runtime) |
enforcing types, limits and permissions; memory-safe Rust (unsafe_code = "forbid" for the whole workspace), no ambient authority |
isolation from the terminal: it shares the process (ADR-0002) |
the host (t0-host inside TTT) |
executing the operations it declares, honestly | its return values are type-checked against the contract anyway |
t0 lsp / the editor services |
reading text | they never run a program or grant anything |
| native code (future) | — | must run in a separate process (ADR-0009) |
Permissions are never written to disk (the session holds them in memory; a new terminal starts with none), never expanded by "fixing" a program, and checked on every host call, so revoking one mid-run stops the next call.
Out of scope for v0 and recorded as risks: memory the budget does not count (above) inside the terminal's process (ADR-0002, ADR-0010); a malicious host (the host is trusted by definition); timing side channels.
5. Observability#
Each execution records: execution id, source hash, artifact body hash,
language/compiler/IR versions, host name and API version, contract versions
checked against, grants at start, steps used, host calls (operation,
duration, outcome code), and outcome. The session keeps recent executions in
memory; tracing logs one line per execution start/end and per failed host
call (target t0).