Aura
A configuration language that reads its own inputs — and never lets an import do the same.
Manifests compile to JSON, YAML or TOML, with schemas, enums, assertions that fail
the build instead of the deploy, and a capability model in which env() and
read_file() are granted per run — and the grant does not reach imported modules.
One binary, no runtime to install, 16 keywords, a 1.7 MB download.
Try it in your browser · Documentation · Русская версия
[!NOTE] Aura is a working preview (
0.x). All six specification phases are implemented, but nothing has run in production anywhere yet. Until 1.0 a minor release may still change the language, and the syntax is frozen only at 1.0. The changelog lists every change that needs an edit.
Install
Nothing at all
Open the playground. The real compiler, compiled to WebAssembly. Nothing is installed and nothing leaves the page.
A binary
cargo install aura-lang
Or take a build from Releases:
Linux (gnu and static musl), macOS (Intel and Apple silicon), Windows — each
with a .sha256.
In CI
- uses: aura-config/setup-aura@v1
- run: aura check deploy.aura --strict
Resolves the version, verifies the checksum, puts aura on PATH.
In a Rust program
[]
= { = "0.4", = false }
Without the default cli feature the library is 38 packages instead of 96,
and no C dependencies at all.
[!TIP] On x86_64 Linux prefer the musl build — it is what
setup-aurainstalls. The gnu build requiresGLIBC_2.34and will not start on Ubuntu 20.04, Debian 11, CentOS 8 or Amazon Linux 2. The static build has no such floor, and starts measurably faster.
Sixty seconds
# deploy.aura
base_port = 8000 # `=` computes, stays private
is_prod = env(, ) == # capability-gated
# `:` exports — everything under here ends up in the output
api:
port: base_port + 1
replicas: cond
is_prod -> 6
else -> 1
end
end
assert base_port > 1024,
$ aura eval deploy.aura --allow-env=APP_ENV
{
"api": {
"port": 8001,
"replicas": 1
}
}
Note what is not in the output: base_port and is_prod. That is the whole
rule — : exports, = does not.
Why Aura
| Problem | Aura's answer |
|---|---|
| YAML breaks from one stray space | No significant indentation: structure comes from line breaks and an explicit end |
| Configs "work on my machine" | Deterministic by construction: without explicit flags a manifest has no access to files or environment variables |
A module pulled from the internet reads /etc/passwd |
Imported modules are isolated from I/O, and cannot borrow the caller's rights |
| "Why is prod on a different port?" | Values are immutable; shadowing requires the explicit shadow keyword |
| Dependency version drift in CI | Versioned imports and aura.lock with a token-stream integrity hash, plus --frozen |
| Dead config fragments live for years | Static analysis finds unused variables and imports; --strict makes them fail CI |
A generated config.json nobody can trace |
The manifest is the source, and --dry-run reports what it read and would write |
The language
Aura's central rule — the same idea as locals versus outputs in Terraform:
tmp = base * 2 # = a private variable: does NOT end up in JSON
port: tmp + 1 # : a property: ends up in JSON
This is what makes dead-code analysis precise: an unused = variable is always
genuine cruft (W0501), never "maybe someone needs this output".
path =
domain
path = # E0302: shadowing requires the keyword
shadow path = # OK - the intent is explicit
end
Reassigning a name within the same scope is always an error (E0301).
enum Tier
end
type Service
name: String
tier: Tier
port: = 8080 # optional: omit it and the default applies
end
api: new Service
name:
tier: # E0514: did you mean "backend"? members: ...
end
A missing field is E0511, a type mismatch E0512, an extra field E0513 under
--strict. Int and Float are separate types, so byte limits and 64-bit IDs
never lose precision. An enum member stays an ordinary string — the JSON output
does not change, only what is accepted.
Optionality never introduces a null: a default is a value, not an absence.
The schema that validates the manifest can also type the program reading its JSON, so there are no hand-written structs to keep in sync:
aura types config.aura --lang rust # or ts | go
export type Scheme = "https" | "http";
export interface Endpoint {
host: string;
port: number;
scheme: Scheme;
}
Rust gets a serde struct plus an enum with #[serde(rename)]; Go gets a struct
with json: tags and typed constants. Parsing only — the manifest is never
evaluated, so no capabilities are involved, and the output is already canonical
for rustfmt, gofmt and prettier.
# a def body is an object
app: app
end
up = (s) -> s.upper() end # a lambda
xs.compact().uniq().map (item, index) ->
end
Sixty methods across String, Int, Float, Bool, List and Object — the
full list is in the method reference.
range(n) produces [0 … n-1], which is how you generate N shards or replicas
without listing them by hand.
cond takes the first true arm, and the else is mandatory — no branch yields
nothing:
tier = cond
region == ->
region == ->
else ->
end
A text … end block is an ordinary multi-line string. Interpolation works;
braces inside do not, which is what makes generating nginx configs practical:
entrypoint: text
#!/bin/sh
echo
exec ./server --port #{port}
end
now() does not exist and never will — an unreproducible config cannot be
written. Durations and dates are first-class:
ttl = .parse_duration() # -> 5400 seconds
refresh: (ttl / 3).format_duration() # -> "30m"
If a build timestamp is needed the host supplies it as an input:
env("BUILD_TIME", …).
Dot for fields, brackets only for list indices — one operator per operation:
loaded = read_file().parse_json()
version: loaded.package.version # ordinary keys
port: loaded.servers..port # a key with a space
first: loaded.apps[0].name # list index (out of bounds is E0317)
optional: loaded.get(, ) # safe access
A typo in a key is E0308 with a position, not a silent null.
import github/actions/rust-cache@v1.2 as rust # a version is mandatory
import as defaults
Cyclic imports are reported with the full chain. Every module is loaded, parsed
and evaluated exactly once. Exact versions and integrity hashes live in
aura.lock, and --frozen makes CI refuse anything else.
aura add is the only place Aura touches the network. eval always runs
offline, so a result never depends on what a registry served today.
The language reads TOML, JSON and YAML and writes all three, so conversion is one line:
config: read_file().parse_toml()
aura eval convert.aura --allow-read=. --format yaml
Unlike yq or jq you can validate against a schema, merge several sources and
add assert checks along the way — conversion with guarantees.
Security model
By default a manifest can do nothing: no files, no environment variables. Rights are granted per run, and they do not propagate.
| Flag | What it allows |
|---|---|
--allow-read=<dir> |
read_file() inside that directory (repeatable; paths are canonicalised, .. cannot escape). A relative path in read_file() is read from the manifest's folder, as an import is (D37) |
--allow-env[=A,B] |
env() for the listed variables — with no list, all of them |
--allow-imports-io |
extends the root's rights to imported modules |
--hermetic |
the opposite: no I/O at all, E0505 everywhere; excludes the --allow-* flags |
[!IMPORTANT] Grants belong to the root manifest. A module you import cannot call
env()orread_file()even when the root holds those rights, and an exported function runs with the capabilities of the module it came from — so isolation cannot be borrowed by persuading you to call something innocent-looking.
A call without a grant is E0310; a grant that does not cover the path is
E0311. An effectful call inside an imported module is caught statically as
W0512, before anything runs.
--hermetic inverts the question. Rather than granting rights it requires that
none are needed — and because E0505 is an analysis error, this is settled
without evaluating anything, including for branches a given run would not take:
$ aura check --hermetic deploy.aura
[E0505] Error: env() is not allowed in hermetic mode
Command line
aura eval <file.aura> [--strict] [--dry-run] [--frozen] [--hermetic]
[--allow-read=<dir>] [--allow-env[=A,B]] [--allow-imports-io]
[--max-steps=N] [--max-memory=MB] [--stats]
[--format json|json-flat|yaml|toml] [-o out.json] [--registry-dir=<dir>]
aura check <file.aura> [--strict] [--hermetic]
aura fmt <files...> [--check]
aura types <file.aura> --lang rust|ts|go [--out <file>]
aura docs --agent [-o <file>]
aura add <path>@vX.Y.Z [--from <file>] [--registry-dir=<dir>]
| Mode | Behaviour |
|---|---|
--strict |
analysis warnings become errors; extra schema fields are forbidden |
--dry-run |
a full evaluation, but nothing is written — you get a report of what it read and would write |
--frozen |
dependencies resolve strictly via aura.lock; a mismatch is an error and the lock is never rewritten |
--hermetic |
settles, statically, that the manifest performs no I/O |
Exit codes: 0 success, 1 diagnostics, 2 an I/O or argument error.
Working with an AI assistant
aura docs --agent prints the complete language reference — syntax, standard
library, every diagnostic code — assembled from the compiler's own definitions,
at roughly four thousand tokens. Small enough to hand over whole, and it always
describes the binary that produced it. One line in your repository's AGENTS.md
is enough:
Before writing or editing .aura files, run `aura docs --agent` for the complete
language reference.
The same text is published at /llms.txt.
Using Aura from other languages
The CLI contract is the API, in the tradition of terraform, jq and
pandoc: JSON on stdout, stable E0xxx codes on stderr, exit codes 0/1/2.
=
=
const = require;
const config = JSON.;
out, err := exec.Command("aura", "eval", "app.aura", "--frozen").Output()
if err != nil
var config map[string]any
json.Unmarshal(out, &config)
For production: --frozen with a committed lock file, capabilities only
explicit, and --format yaml|toml if the consumer prefers another shape.
use Script;
let cfg: MyConfig = builder
.file
.allow_read // the files it reads, beside it
.env_values // exactly these, and no others
.strict
.load?
.output?; // what `aura eval` prints, typed
There is no intermediate file: the application reads the manifest itself, and
aura types --lang rust writes the structs for its schemas.
A manifest's pub def is also a callable value, so rules can live in a .aura
file and the application can call them per request, changing behaviour without a
rebuild:
use Script;
let rules = load?; // hermetic: proves it reads nothing
let route: Route = rules.call?;
Every call has a budget in steps and bytes, memory per call is constant, and one
Script serves every thread. A script can call functions the application gives
it - import "host:geo" as geo - and be reloaded when its file changes, held to
every check the first load passed. See the
embedding guide;
cargo run --example checkout shows all of it in one service.
Mobile applications consume the result: a server or CI evaluates the manifest, the client reads the JSON. A WebAssembly build exists and is CI-tested — it is what the playground runs on — and npm, PyO3 and a C ABI are on demand.
Diagnostics
Errors carry a file, line and column, highlight the code, and suggest a fix:
[E0302] Error: 'global_file_path' shadows an outer variable
╭─[ production_deploy.aura:24:3 ]
24 │ global_file_path = "/var/log/aura.log"
│ ─────────┬────────
│ ╰── add `shadow`
│
│ Help: write `shadow global_file_path = ...` to make the shadowing explicit
────╯
Every code is stable and documented in the catalogue — a test keeps that list and the compiler in step, in both directions.
Architecture
flowchart LR
S["source<br/>&'a str"] --> L["lexer<br/>Vec<Token>"]
L --> P["parser<br/>AST"]
P --> A["analysis<br/>Vec<Diagnostic>"]
A --> E["evaluation<br/>Value"]
E --> O["JSON · YAML · TOML"]
Tokens and the AST borrow the source's memory — nothing is copied along the way.
crates/
├── aura-lang # the library and the `aura` CLI, one publishable crate
│ ├── lexer/ # zero-copy DFA, newline normalisation
│ ├── parser/ # recursive descent + Pratt expressions
│ ├── analysis/ # dead code, undefined names, shadow rules
│ ├── eval/ # tree-walking interpreter, environments, method registry
│ ├── vfs/ # resolvers, cycle detection, aura.lock
│ └── serialize/ # Value -> JSON/YAML/TOML, Int without precision loss
└── aura-lsp # the language server, built on aura-lang
Invariants. Zero-copy: neither lexer nor parser copies a string.
Determinism: JSON key order is declaration order, and two runs are
byte-identical. Immutability: containers live in Arc, so cloning a value is
O(1).
Performance
cargo bench -p aura-lang, on the reference manifest:
| Stage | Result |
|---|---|
| Lexer | 258 MiB/s |
| Lexer + parser | 178 MiB/s |
| Lexer + parser + resolver | 71 MiB/s |
| Full pipeline (lex + parse + evaluate) | 33 µs per manifest |
Measured 2026-07-29 on x86_64 Linux. Numbers from one machine are worth what they cost — treat them as an order of magnitude, and re-run them on yours.
How this is verified
| Tests | the whole suite, on Linux, macOS and Windows |
| Conformance | every example in examples/ driven through the real binary, output diffed against a pinned expectation |
| End to end | packaging/e2e.sh asserts the documented claims – exit codes, capability refusals, hermetic mode, output formats, and byte-identical output across two runs |
| Containers | on a tag, those artefacts run on debian:12, ubuntu:22.04, alpine, ubuntu:20.04, and aarch64 under emulation |
| Miri | the source arena, under both Stacked Borrows and Tree Borrows |
| Fuzzing | a cargo-fuzz target per stage: lexer, parser, pipeline, formatter, codegen, resolver, and the JSON/YAML/TOML readers. ClusterFuzzLite runs them nightly with a corpus kept between runs, and on every pull request that touches the compiler |
| Cross-platform | cargo check for freebsd, aarch64-linux, musl and wasm32 |
| Documentation | snippets must be canonical aura fmt and name nothing the language lacks; the diagnostic catalogue, the method reference and the decision log must match the compiler, in both languages |
The recursive-descent parser is DoS-hardened: deeply nested input yields E0208
rather than a stack overflow, and that is checked in release builds, where there
is no thread with a large stack to hide behind.
cargo test --workspace # units, conformance, property tests, snapshots
cargo bench -p aura-lang # lexer, parser, resolver, full pipeline
The formal specification, including the numbered design decisions the code refers to by name, is SPEC.md.
How Aura is built
My name is Ilya Brin. I came up with Aura and I develop it, working on the language together with Claude, an AI model by Anthropic.
The idea is an old one. It goes back to when I was writing my first language, in Go. That was when I came across Starlark and thought that computed configuration is a genuinely useful thing. For Go, though, it was overkill: everything compiles fast anyway, and a YAML file or a few constants in the code are enough. With Rust it became clear that I would have to write such a language myself. I have several growing projects and production services that need it. I trusted Claude with writing the code, and I read everything it writes. That way I am also learning Rust from real code rather than from textbook examples.
We have different responsibilities. I am responsible for what the Aura language is: why it exists, what must never be in it, and which rules it keeps. There are only a few:
- no magic;
- the same thing always behaves the same way;
- code reads one way only;
- the developer's experience matters more than anything else.
Claude:
- writes the code;
- tests ideas in practice;
- looks for places where the documentation has drifted from what the language actually does.
The decisions are mine, and no change reaches the project without my agreement.
Along the way we settled into habits that have proven themselves:
- One task, one change. Each comes with an explanation: not only what changed, but why we chose this path.
- We don't take text at its word, we check. If the documentation says the language can do something, we run it and see. That is how we found things you don't notice by reading: the reference was missing half the methods, one example did not compile, and three decisions were mentioned everywhere but written down nowhere.
- Every bug we find gets a test. And we make sure that without the fix the test really fails. Otherwise it only looks like protection.
- Hints in error messages are checked too. If the compiler suggests how to fix your code, a test runs that suggestion and makes sure it works.
Commits written by Claude carry a Co-Authored-By trailer, starting with the one
that added this section. Earlier ones don't: back then Aura was an internal tool for
my own projects.
Status
All six specification phases are implemented. The syntax is still allowed to change, and is frozen at 1.0.
The language and its tooling
- Zero-copy lexer and Pratt parser
- Runtime with a capability model, schemas and
enum - Modules, cycle detection,
aura.lockwith a token-stream hash - Static analysis,
--strict,--dry-run,--hermetic - JSON, flattened JSON, YAML and TOML output
-
aura fmt— canonical formatting with a token-stream guarantee -
aura types— Rust, TypeScript and Go from a manifest's schemas -
aura docs --agent— the whole language for a coding assistant - Embedding in Rust:
Scriptwith a budget per call (D33, D34), functions the host provides (D35), reload while the service runs (D36), a whole manifest read into your own types - Deterministic time;
now()forbidden by construction - Language server: completion, hover, go-to-definition, references, symbols, rename, signature help, format-on-save
Ecosystem and distribution
- Published to crates.io, with binaries for six targets on every tag
-
aura-config/setup-aura@v1for GitHub Actions - A browser playground running the real compiler as WebAssembly
- A documentation book, with a full Russian translation
- Syntax highlighting for VS Code, Vim/Neovim and nano — editors/
- A tree-sitter grammar (Helix, Zed, GitHub Linguist)
- npm, PyO3 and a C ABI — on demand rather than ahead of it
Towards 1.0. Three criteria remain: a frozen syntax, a promise of backward compatibility, and real users whose configs must not break.
Contributing
Issues and pull requests are welcome. Before opening one:
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all --check
cargo build -p aura-lang && find examples -name '*.aura' -print0 | xargs -0 ./target/debug/aura fmt --check
CI runs exactly those four. The last is the language formatting its own examples. Code comments and commit messages are in English. AGENTS.md records where each fact has its single home — worth reading before adding a second copy of one.
License
MIT (LICENSE-MIT) or Apache 2.0 (LICENSE-APACHE), at your option.
Unless you state otherwise, any contribution you intentionally submit for inclusion shall be licensed as above, with no additional terms.