Skip to content

Runtime and security

Runtime, executions, and execution security

1. Reference runtime (t0-runtime)#

  • Executes a validated artifact's entry function 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> }. CallContext gives 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.
  • print goes to an Output sink 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 print from 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 marked cancellable (backtest.wait, market.candles, UI requests). Every job it started is stopped with it. The execution ends Cancelled — also when it stopped on an error after the cancellation was asked for — never Failed.
  • One job — Stop job in the window, backtest.cancel. The engine stops at its next event boundary; the program goes on and reads StoppedByUser.

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's RunJob, 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 becomes Failed{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 with E-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).