Skip to Content
Internals

Internals

Kora is a tree-walking interpreter built around durable execution, information-flow labels, and token budgets — not a generic scripting VM. This page traces the pipeline from source text to a running agent, then the runtime mechanisms behind Decisions that don’t show up in a typical language.

Compiler pipeline

crates/kora-syntax
lexer.rs
source text → tokens (token.rs)
→
crates/kora-syntax
parser.rs
tokens → AST (ast.rs)
→
crates/kora-types
typecheck
AST → diagnostics + symbol and local type facts
→
crates/kora-runtime
interp.rs
tree-walk execution
→
crates/kora-cli
main.rs
driver, CLI surface

Crate map

kora-syntaxlex + parse + AST
kora-typesname, local type, and direct model-flow checking
kora-runtimeinterpreter, budgets, journal, labels, telemetry
kora-clirun / repl / driver
kora-lspeditor language server
kora-dapdebug adapter (debug.rs)
kora-mcpMCP tool exposure
kora-pkgpackage manager

The unique parts

Journal — durable execution by replay

No stack snapshot. Every effect (model call, tool result, write, human answer, clock read) is appended to .kora/runs/<id>.jsonl, one line each. Resuming = re-running from the top, effects served from the journal instead of re-executed, fast-forwarding to where it stopped.

Writes are recorded twice on purpose: an attempt before the call, its outcome after. A resume that finds an attempt with no outcome stops and names the call, because a process killed in that gap leaves nobody able to say whether the row was written.

Why: a Rust call stack can’t be serialized — Temporal / Restate solve durability the same way.

Labels — information flow tracking

classified values carry a secrecy tag (Public / Classified) and a trust tag (Unverified / Trusted). Labels are transitive: survive slicing, formatting, arithmetic, container insert, return values.

Why: one line of laundering breaks the whole model — this killed Perl taint mode and Ruby $SAFE.

Declassify blocks — auditable release

audit.rs statically enumerates every declassify … for sink: site in a program, its imports, and its packages — exhaustively, not via grep.

Why: makes “what data reached the model” answerable to a reviewer, not best-effort.

Budgets — tokens as the unit of control

Budgets are denominated in tokens, the thing the runtime can actually measure, not money. They nest: a child scope can only tighten a parent’s limit. Exhaustion is a returned value, never an exception.

Why: partial agent work should survive running out of budget, not crash.

Cassettes — record/replay for model calls

Three modes: Live, Record (call + append to disk), Replay (never call — a missing entry is an error). Keys cover call site, model, prompt, and resolved input.

Why: deterministic, free CI without touching a real provider.

Portable values — the agent isolation boundary

Runtime values use Rc for cheap in-agent sharing, so they can’t cross threads as-is. Portable is an explicit deep, Send-safe copy used only when values move between agents.

Why: parallel workers share no live values. Isolation is paid for explicitly at the thread boundary, not assumed for every agent call.

Images as values

fs.image(path) returns a first-class value like fs.read does for text, and flows into analyze(). Type is sniffed from magic bytes, never the file extension; size is checked at read time, not at the provider.

Why: most real agent work is receipts, screenshots, and forms — not plain text.

Telemetry — labels guard the exporter too

Spans are emitted by the scheduler itself (it already owns every model call, budget meter, declassification) as raw OTLP/HTTP JSON, no SDK. A classified value cannot become a span attribute.

Why: prompt leakage into observability vendors is a live incident category.

Modules — one file, one namespace

A module is one Kora file (use ”./lib/tax.ko” as tax). Its top-level names live in their own namespace, so two files can reuse a name without colliding, and a function always reads the file it was written in. Import paths resolve relative to the importing file, never the working directory.

Why: a program stays a directory that can be moved or vendored whole.

Notes — a durable run’s own scratch space

A key-value store scoped to one durable run (.kora/notes/<run-id>.json), for state a plain variable can’t hold: it survives a crash, and a different kora invocation can read it. A classified value written through notes.write keeps its label on the way back out.

Why: long tool loops already keep a plan and partial results in locals — those shouldn’t die with the interpreter.

Debugger — a zero-cost hook

The interpreter knows nothing about DAP. It keeps a frame stack and asks a Debugger whether to stop before each statement — one Option check, no snapshot, when nothing is attached. The editor-facing protocol lives entirely in kora-dap.

Why: debuggability shouldn’t tax every run that isn’t being debugged.

Config - present files fail closed

kora.toml actively configures models, sinks, HTTP, telemetry, MCP, Python, output redaction, and installs. An unreadable file, invalid TOML, or malformed model entry stops execution; unknown top-level sections remain forward-compatible.

Why: a typo cannot silently change a model or data policy, while later versions can still add top-level sections.

Package security

Capabilities — no ambient authority

A dependency reaches the network, filesystem, a database, the environment, a Python worker, or an MCP server only where the importing program granted it (net, fs, …) — and it can never pass on more than it holds itself.

Why: in most package ecosystems a dependency runs with the program’s full rights, so one compromised package compromises everything.

Checksum log — trust-on-first-use, permanently

A lockfile can’t protect the fetch that creates it. The first time a commit is seen, its hash is recorded forever in two logs — the project’s committed kora.sums and a machine-wide log under the user’s home directory — and every later fetch is checked against both.

Why: closes publish-a-backdoor-then-delete-it, which leaves no trace in a lockfile alone.

Install flow, animated

yes —already lockednohash mismatchtag/commit mismatchkora.toml [dependencies]kora-pkgscan.rsfind every “use”kora-pkgresolve.rspick version → tagkora-pkgfetch.rsgit clone at tagkora-pkghash.rshash contents, not tarballkora-pkgsumlog.rsseen this hash before?kora-pkglock.rstag + commit + hashkora-runtimegrants.rscapabilities onlyREFUSEinstall stops here
normal path (flowing)shortcut when already verifiedrefusal on mismatch

A fresh install walks the teal path top to bottom. If the commit is already in kora.lock, fetch.rs takes the amber shortcut straight to lock.rs. Any hash or commit mismatch at sumlog.rs or lock.rs takes a red path and refuses the install — it never continues quietly on a changed tag or a rewritten repo.

AttackStopped by
Maintainer force-pushes a taglock.rs: commit/hash mismatch → refuse
Repo history rewrittenhash.rs: content hash won’t follow it
Publish backdoor, then delete itsumlog.rs: hash recorded permanently, deletion doesn’t erase the record
First-time fetch, nothing local to check againstsumlog.rs machine-wide log: another project already recorded this commit
Compromised dep tries to phone home / read diskgrants.rs: no capability granted, no ambient authority
Compromised dep re-exports secrets to its own depsgrants.rs: can’t pass on more capability than it holds

Editor tooling

kora-typesone index answers diagnostics, hover, and go-to-definition — so they can’t disagree. Not a full type checker; fast enough to run on every keystroke.
kora-lsplanguage server built on that same index
kora-dapdebug adapter protocol; talks to the interpreter’s Debugger hook only

Standard library surface

fs / globfiles, paths, images
http / json / csvnetwork + structured data
sqlqueries against a database
re / time / envregex, clock, process env
notesdurable per-run scratch space

Journal replay, visually

RUN 1 (crashes / pauses after step 3)RUN 2 (resume)model calltool resulthuman answerstopjournal.jsonl (on disk)replayedreplayedreplayednew work
model calltool resulthuman answerfaded = served from journal, not re-executed

Label flow, visually

classifiedvalueslice / formatstill classifiedclassifiedvaluedeclassifyfor model:publicvaluemodel/ sinkany direct path from classified → sink is a compile-time error
Last updated on