Aura
A configuration language that reads its own inputs — and never lets an import do the same.
Aura compiles readable manifests into 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.
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 (Deno-style capabilities) |
| "Why is prod on a different port?" | Values are immutable; shadowing requires the explicit shadow keyword |
| Dependency version drift in CI | Versioned imports + aura.lock with a token-stream integrity hash and --frozen mode |
| Dead config fragments live for years | Static analysis: unused variables and imports, --strict for CI |
Example
import as defaults
base_port = 8000 # private computation
is_prod = env(, ) ==
type ServiceMeta
name: String
port: Int
end
name: app_name
tier: tier
managed_by:
end
domain
replicas: is_prod ? 3 : 1 # a property - ends up in JSON
cargo_data = read_file().parse_toml()
app_version = cargo_data.package.version
services = [, , , null]
active = services.compact().uniq()
meta: new ServiceMeta
name: .upper()
port: base_port + 1
end
apps: active.map (name, index) ->
name: name
image:
labels: build_labels(name, ).merge(defaults.global_labels)
end
assert active.len() >= 1,
end
aura eval production_deploy.aura --allow-read=. --allow-env=APP_ENV
Quick start
git clone https://github.com/aura-config/aura-lang && cd aura-lang
cargo build --release
cd examples
../target/release/aura eval production_deploy.aura \
--allow-read=. --allow-env=APP_ENV --registry-dir=registry
Check without evaluating (lex + parse + static analysis):
aura check production_deploy.aura --strict
Tour of the language
Computation and output are different things
Aura's central rule (the same idea as locals vs. 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."
Immutability and explicit shadowing
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).
Schemas
type ServiceMeta
name: String
port: Int
end
meta: new ServiceMeta
name:
port: # E0512: expected Int
end
A missing field is E0511; an extra field is E0513 under --strict.
Int and Float are separate types: byte limits and 64-bit IDs never lose precision.
A field with = default is optional — omit it and the default applies (it may
reference module vars). No nullable fields: optionality never introduces a null.
type Service
name: String
port: = 8080 # optional: omitted -> 8080
tier: =
end
api: new Service
name: # port and tier take their defaults
end
Closed sets with enum
A String field accepts any string, so "backand" used to reach production. An
enum makes the set closed — and a member is still an ordinary string, so the
JSON output does not change:
enum Tier
end
type Service
name: String
tier: Tier
end
svc: new Service
name:
tier: # E0514: did you mean "backend"? members: ...
end
pub enum crosses the module boundary (D12), and members are resolved where the
schema is declared — an imported schema validates against its own module's enum.
Types for your service: aura types
The same schema that validates the config can type the service that consumes its JSON — no hand-written structs to keep in sync:
aura types config.aura --lang rust # or ts | go
enum Scheme
end
type Endpoint
host: String
port: = 443
scheme: =
end
becomes, for TypeScript:
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 plus a named string type and typed constants. Parsing only —
the manifest is never evaluated, so no capabilities are involved.
Functions, lambdas, methods
# def returns an object
app: app
end
up = (s) -> s.upper() end # a lambda expression
xs.compact().uniq().map (item, index) ->
end
Built-in methods: upper lower len trim split replace starts_with
ends_with to_int to_float to_str compact uniq map filter sort
reverse sum min max flatten slice first last get contains
join merge keys values abs parse_toml parse_json parse_yaml
to_json to_yaml to_toml parse_duration format_duration parse_datetime
format_datetime sha256 base64 base64_decode. The registry grows without
touching the parser.
The global range(n) generates [0, 1, ..., n-1] — handy for producing N items
(shards, replicas) instead of listing them by hand:
shards: range(3).map (i, _) ->
name:
replica_of:
end
Multi-way choice with cond
For 3+ branches where nested ternaries get unreadable, cond picks the first
true arm; else is mandatory (a missing one is a parse error):
tier = cond
region == ->
region == ->
else ->
end
The left of each -> must be Bool; the right is any expression. No pattern
destructuring - deliberately simpler than a match.
Multi-line values with text … end
A text … end block is an ordinary string, just multi-line - so any property
takes either a "one-liner" or a block. Interpolation #{} and escapes work as
usual; the common indentation is stripped and lines join with \n:
domain
entrypoint: text
#!/bin/sh
echo
exec ./server --port #{port}
end
end
The closing end sits at the entrypoint: indentation; content is indented
deeper, so an embedded end (a shell/Ruby block) is just text. For many or large
scripts prefer read_file(...) over an inline block.
Time - deterministic only
now() does not exist in Aura and never will (D13) - an unreproducible config
cannot be written by construction. Durations and dates, on the other hand, are
first-class:
ttl = .parse_duration() # -> 5400 (seconds)
refresh: (ttl / 3).format_duration() # -> "30m"
window_end: (.parse_datetime()
+ .parse_duration()).format_datetime() # -> "2026-07-19T02:00:00Z"
If a build timestamp is needed, the host supplies it: env("BUILD_TIME", ...) under --allow-env.
Data access
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/dot - a string
dynamic: loaded.envs..url # a dynamic key
first: loaded.apps[0].name # list index (out of bounds - E0317)
optional: loaded.get(, ) # safe access, no error
A typo in a key is an E0308 error with a position, not a silent null.
Aura as a format converter
Since the language reads TOML/JSON/YAML and writes all three, conversion is a one-liner:
# convert.aura
config: read_file().parse_toml()
aura eval convert.aura --allow-read=. --format yaml # TOML -> YAML
Unlike yq/jq, you can validate against a schema, merge multiple sources,
and add assert checks along the way - conversion with guarantees.
Modules
import github/actions/rust-cache@v1.2 as rust # a version is mandatory
import as defaults
- Cyclic imports are detected with the full chain:
E0401: cyclic import: a.aura -> b.aura -> a.aura. - Every module is loaded, parsed, and evaluated exactly once.
- Exact versions and integrity hashes are pinned in
aura.lock; use--frozenin CI.
Validation
assert active.len() >= 1,
value: broken ? fail() : 42
Security model
By default a manifest can do nothing: no reading files, no environment variables. Capabilities are granted via CLI flags and do not propagate to imported modules.
| Flag | What it allows |
|---|---|
--allow-read=<dir> |
read_file() inside the directory (repeatable; paths are canonicalized, .. cannot escape) |
--allow-env[=A,B] |
env() for the listed variables (no list - all of them) |
--allow-imports-io |
grant imported modules the root's capabilities |
--hermetic |
the opposite: no I/O at all. env() and read_file() are E0505 in every module, and the flag excludes the --allow-* ones |
A call without a grant is an E0310 error with a hint about which flag to
add. An effectful call inside an imported module is additionally caught
statically (W0512).
--hermetic inverts the question: rather than granting rights, it requires that
none are needed. Because E0505 is an analysis error, aura check --hermetic deploy.aura proves a manifest touches nothing at all - no evaluation, no
branch-dependent answer - which makes it a CI gate.
CLI
aura eval <file.aura> [--strict] [--dry-run] [--frozen]
[--allow-read=<dir>] [--allow-env[=A,B]] [--allow-imports-io]
[--hermetic]
[--format json|json-flat|yaml|toml] [-o out.json] [--registry-dir=<dir>]
aura check <file.aura> [--strict] [--hermetic]
aura fmt <files...> [--check]
aura add <path>@vX.Y.Z [--from <file>] [--registry-dir=<dir>]
aura add is the only place Aura ever touches the network: a package is
downloaded (by convention github/<owner>/<repo> -> package.aura at tag
vX.Y.Z), validated, stored in the local cache, and pinned in aura.lock
with an integrity hash. eval always runs offline - the result never depends
on the network, by construction.
| Mode | Behavior |
|---|---|
--strict |
analysis warnings become errors; extra schema fields are forbidden |
--dry-run |
full evaluation, but neither JSON nor aura.lock is written - only a [dry-run] would write ... report |
--frozen |
dependencies resolve strictly via aura.lock (a mismatch is an error), the lock is never rewritten |
--format json-flat |
flattened output: production-eu.metrics.port = 9090 |
--format yaml / toml |
the same result in YAML or TOML (TOML requires an object at the top level) |
Exit codes: 0 - success, 1 - diagnostics, 2 - I/O or argument errors.
Using Aura from other languages
Aura follows the terraform/jq/pandoc pattern: a stable CLI contract is the
API. JSON on stdout, diagnostics with stable codes (E0xxx) on stderr,
exit codes 0/1/2. From any language:
# Python
=
=
// Node.js
const = require;
const config = JSON.;
// Go
out, err := exec.Command("aura", "eval", "app.aura", "--frozen").Output()
if err != nil
var config map[string]any
json.Unmarshal(out, &config)
Recommendations for production: --frozen (a lock file is mandatory),
capabilities only explicit, --format yaml|toml if the consumer prefers
another format. Rust projects can embed Aura directly, without a subprocess,
via aura_lang::facade::eval_file(). Mobile apps are consumers of the
result: a server/CI evaluates the .aura file, the client reads the
resulting JSON. Native bindings (WASM/npm, PyO3) are on the roadmap.
Diagnostics
Errors point to the 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 error has a stable code (E0xxx / W0xxx) - convenient for grepping CI logs.
Architecture
source ──▶ lexer ──▶ parser ──▶ static analysis ──▶ evaluation ──▶ JSON
&'a str Vec<Token> AST Vec<Diagnostic> Value
(zero-copy: tokens and the AST borrow the source's memory)
crates/
├── aura-lang # the library + the `aura` CLI (one publishable crate)
│ ├── lexer/ # zero-copy DFA, newline normalization
│ ├── parser/ # recursive descent + Pratt expressions
│ ├── analysis/ # dead code, undefined vars, shadow rules
│ ├── eval/ # tree-walking interpreter, Environment, method registry
│ ├── vfs/ # FileResolver, cycle detection, aura.lock
│ ├── serialize/ # Value -> serde_json (Int with no precision loss)
│ └── main.rs # the `aura` binary (clap + ariadne); the library stays
│ # clap/ariadne-free and WASM/LSP-ready
└── aura-lsp # language server (ships in the VS Code extension)
Key invariants:
- Zero-copy: neither the lexer nor the parser copies strings - only
&'a strslices. - Determinism: JSON key order = declaration order (
IndexMap); two runs produce byte-identical output. - Immutability: containers live in
Arc, cloning a value is O(1).
Performance
Criterion benchmarks on the reference manifest (cargo bench -p aura-lang):
| Stage | Result |
|---|---|
| Lexer | 258 MiB/s |
| Lexer + parser | 178 MiB/s |
| Lexer + parser + resolver | 71 MiB/s |
| Full pipeline (lex + parse + eval) | 33 µs per manifest |
Measured 2026-07-29 on x86_64 Linux. Numbers from a single machine are worth
what they cost — treat them as an order of magnitude, and re-run
cargo bench -p aura-lang on yours.
Development
cargo test # units, conformance suite, property tests, golden snapshots
cargo bench # lexer, parser, resolver, full-pipeline benchmarks
Coverage-guided fuzzing (lexer, parser, full pipeline) lives in
fuzz/ — nightly + cargo-fuzz, run in a non-blocking CI job.
The recursive-descent parser is DoS-hardened: deeply-nested input yields E0208
instead of a stack overflow.
The full language and architecture specification is SPEC.md. The reference manifest, which must pass the entire pipeline, is examples/production_deploy.aura. Nine themed examples (k8s, a CI matrix, feature flags, a service catalog, i18n, a Telegram bot, a validators package, a capability-model demo) live in examples/.
Status and roadmap
Aura is in working-preview status (v0.1): all six specification phases are implemented.
- Zero-copy lexer and Pratt parser
- Runtime with a capability model and schemas
- Modules, cycle detection,
aura.lock - Static analysis and
--strict/--dry-run - JSON export and a CLI with ariadne diagnostics
- Reading and writing JSON/YAML/TOML (
parse_*/to_*,--format yaml|toml) - Indexing and access to arbitrary keys (
xs[0],obj."eu west",.get) -
aura fmt- indentation canonicalization with a token-stream-preservation guarantee - Deterministic time:
parse_duration/format_duration,parse_datetime/format_datetime;now()is forbidden by construction (D13) - Extended standard-library methods (
sort,split,trim,join,slice,flatten,reverse, ...) - the method reference - LSP server: completion, hover, go-to-definition, references, document
symbols, rename (with
prepare), signature help, and format-on-save - crates/aura-lsp/
Ecosystem and distribution:
- A release pipeline: a
v*tag builds binaries for six targets (Linux gnu/musl, aarch64, macOS Intel and Apple silicon, Windows), each with a.sha256, after checking the tag againstCargo.toml- release.yml - A
setup-auraGitHub Action: resolves the version, verifies the checksum and putsauraonPATH- aura-config/setup-aura. The release workflow'sself-testjob installs from a real release through that published action and runs the result on Linux, macOS and Windows - Publication: crates.io, and
setup-auraextracted into its own repository for the Marketplace - both are done when this repository becomes public - Syntax highlighting: VS Code (TextMate + auto-indent), Vim/Neovim, nano - editors/
- A tree-sitter grammar (Helix, Zed, Neovim, GitHub Linguist)
- A documentation book (docs/book/, with a full Russian translation at docs/book-ru/): a tutorial (6 chapters), guides (security, formats, embedding), a CLI/method reference, and a full error-code catalog; built in CI, deployed to Pages - after the repository is published
- A browser playground - playground/: the real compiler as
WebAssembly, multi-file, with
aura fmtand diagnostics; nothing is installed and nothing leaves the page. Deployed to Pages together with the book, after the repository is published - Usage from other languages: the subprocess pattern (Python/Node/Go) - see the section above. The WebAssembly module the playground runs on is built and CI-tested (crates/aura-wasm/); publishing it to npm, then PyO3 and a C ABI, is on demand
- Cross-platform coverage: a
cargo checkmatrix (freebsd, aarch64-linux, musl, wasm32) in CI; macOS in the test matrix
v1.3 - the package ecosystem:
- D12:
pub def/pub type- exporting functions and schemas from modules (pkg.fn(...),new pkg.Schema ... end); exported functions run with their own module's capabilities, not the caller's -
aura add <pkg>@vX.Y.Z- installing a package (the network is touched only here;evalalways runs offline), validation before install, integrity pinned inaura.lock
Contributing
Issues and PRs are welcome. Before opening a PR: cargo test --workspace,
cargo fmt --all, cargo clippy --workspace --all-targets -- -D warnings,
and aura fmt --check on any changed .aura files - CI runs the same checks.
Code comments are in English (see SPEC.md for the formal language
specification).
License
Aura is distributed under your choice of MIT or Apache License 2.0.
Unless you explicitly state otherwise, any contribution (issue/PR) intentionally submitted for inclusion in this project shall be licensed as above, without any additional terms or conditions.