Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hello World Guide

This guide gets you running with Aura in 15 minutes. You will build a simple ping-pong protocol, deploy it locally, and interact with it using the CLI.

Setup

Aura uses Nix for reproducible builds. Install Nix with flakes support.

Enter the development environment:

nix develop

This command activates all required tools and dependencies. The environment includes Rust, development tools, and build scripts.

Cargo and cargo clippy both use the shell's pinned Rust toolchain. The shell dispatches Clippy explicitly because Cargo can otherwise select an older plugin installed in Cargo home before searching PATH. This requires no global toolchain changes. just ci-build-cache-policy tests the dispatch, argument boundaries, and forwarded exit status.

The repository supports the pinned Rust 1.93 toolchain. Workspace packages declare 1.93 as their supported compiler floor, including published packages; the earlier 1.75 metadata did not establish a tested minimum for the current resolved dependency graph. Lower package-specific minimum supported Rust versions require a separate dependency audit and actual checks with that compiler. Dependency metadata alone does not establish lower-toolchain support. The separate host-tool workspaces (toolkit/test-support and toolkit/xtask) declare this floor explicitly; they do not inherit the runtime workspace's metadata. Dylint libraries use the separately pinned nightly compiler because they integrate with compiler-private APIs. Vendored upstream package metadata remains upstream-owned.

Build the project:

just build

The build compiles all Aura components and generates the CLI binary. This takes a few minutes on the first run.

For a deployable terminal binary, run just build-release. This builds the production terminal feature set and installs bin/aura atomically. Use just build-dev for the development feature set, and just build-workspace-release when validating every workspace crate in the release profile. The browser bundle has its own Dioxus build path.

On a clean fixed-commit comparison (a9fabe9a, macOS ARM), the terminal release retained 1,825,040 KiB versus 3,068,204 KiB for the full workspace: 1,243,164 KiB (40.5%) less release output. The terminal compiled 330 packages versus 498 for the workspace. Shared warm caches vary with later web, development and test builds; use the disk report to measure the current checkout.

Repeated LAN test builds should use just e2e-build-terminal, just e2e-build-web, and just e2e-build-harness in the Nix environment on each host. These commands report free space, preview and collect idle Cargo artifacts when needed, and stop their own build if free space reaches the emergency floor. just disk-report, just cache-inventory, and just build-budget-dry-run are read-only. Run the LAN recipes from the same pushed commit on both hosts, in the background and one host at a time. Set CARGO_BUILD_JOBS to half the available cores on the Air. The recipes use nice -n 10. Use AURA_BUILD_TARGET_CAP_GIB, AURA_BUILD_MIN_FREE_GIB, and AURA_BUILD_EMERGENCY_FREE_GIB to adjust Host B thresholds after its own read-only baseline. The 10 GiB per-checkout Cargo target cap is a between-build soft target; it is not a quota during compilation. The scripts preserve .tmp/e2e and failure evidence. Use scripts/dev/retain-e2e-runs.sh prune --dry-run to inspect completed successful run bundles, then --apply when no harness run is active. Failed, pinned, active and unclassified bundles are preserved.

The disk report lists the current checkout and linked Git worktrees separately, including debug incremental and trybuild caches; it never collects another worktree's target. Work 8 debug, test, and Clippy rebuilds also produce Cargo artifacts. Route each of those commands through the same guard, keeping any chosen Cargo profile or explicit incremental override on the command. During an approved live LAN run, use:

AURA_BUILD_PROFILE=debug nice -n 10 bash scripts/dev/build-budget.sh \
  --lane work8-debug --allow-live-harness -- cargo test -p hxrts-aura-agent --lib

Replace the Cargo command after -- for cargo check, cargo clippy, or another package test. Omit --allow-live-harness outside a live run. The guard refuses admission when another builder of the same checkout is active, or when free space minus other admitted builds' reservations is below the configured floor; wait for the current owner to finish and release an idle cache window before retrying. The Cargo development profile disables incremental compilation by default for ordinary Cargo and editor checks; guarded builds also default to CARGO_INCREMENTAL=0. Opt in with CARGO_INCREMENTAL=1 when the saved rebuild time justifies its retained cache. The fixed-source foundation measurement retained 289,240 KiB without incremental compilation versus 552,508 KiB after an incremental source rebuild (47.6% less). The wrapper's measured rebuild times were 16s versus 12s; this is a foundation check, not a whole-workspace performance guarantee. The wrapper and saved disk report log the effective setting. Required cache-policy fixtures verify the wrapper behavior and compile a tiny isolated package using the actual development profile to verify the compiler default and explicit override.

Run just ci-dry-run only after Cargo, Dioxus and LAN harness consumers have stopped. Its startup and per-step preflight refuses active consumers, checks free space, and can collect only idle compiler caches. It preserves .tmp/e2e and current CI logs. A complete run needs about 40 GiB free at start. To exclude a step explicitly, set AURA_CI_DRY_RUN_SKIP to comma-separated step names (for example "Tests + Protocol Compat"); excluded steps are announced at the start of the run. If stale native release variants dominate after the LAN run ends, preview bash scripts/dev/prune-inactive-lane.sh --lane release --dry-run and then use --apply to reset that whole idle cache. The command refuses active builders, harness processes and open release files. It leaves the installed bin/aura alone; the next release build recompiles its dependencies.

The normal guarded build sequence on each host is:

just disk-report
just build-budget-dry-run
just e2e-build-terminal
just e2e-build-web
just e2e-build-harness
just disk-report

To compare the clean disk cost of the deployable terminal build with the full workspace release gate, run scripts/dev/compare-release-scopes.sh --dry-run first. On an idle host with at least 40 GiB free, use --apply. The Bash script builds both scopes from one commit in separate temporary worktrees, copies the disk reports to artifacts/disk-budget/comparisons/, and removes each temporary target before starting the next build. By default it refuses to run while a build or LAN harness consumer is active. For a reserved window with a live LAN harness but no Cargo/Dioxus builders, use --check --allow-live-harness and then --apply --allow-live-harness. This mode still refuses another builder, uses four low-priority Cargo jobs by default, and touches only the temporary worktree target. Override the job count with AURA_COMPARE_CARGO_JOBS when the host needs a lower limit. The comparison uses the budget wrapper's --no-prune mode because each temporary target is removed after measurement; its free-space admission and emergency stop remain active. If a separate builder starts before the second scope, the script preserves the completed first-scope measurements and stops. Rerun with --resume artifacts/disk-budget/comparisons/<run-directory> from a later no-builder window; it verifies the fixed commit and skips successful scopes.

The tracked LAN entry point is scripts/harness/lan/build.sh; its lane is terminal, terminal-live, terminal-dev, web, web-live, harness, or harness-live. Use the live variants during an approved active LAN run; they preserve release and web caches and only collect an idle WASM debug lane when the budget requires cleanup. It prints the host and commit before building, enters nix develop when needed, and accepts AURA_EXPECT_COMMIT=<full-hash> to reject a host on a different revision or a checkout with uncommitted changes. Use its --dry-run flag to preview the selected recipe. Use scripts/harness/lan/build.sh all for the complete terminal, web and harness build sequence. It requires a clean checkout, pins the initial commit through every step, and stops at the first failure. Run this sequence on one host at a time; it does not start an E2E run or modify the other host. LAN browser startup requires the prebuilt harness-enabled bundle and fails if it is missing or stale. It cannot quietly start an unguarded Dioxus build or clear the serving cache; stop the run and repeat the guarded web build first. On Host B, the macOS application firewall may need the newly signed bin/aura authorized again after a rebuild, as described in work/8.md. The build helper does not change firewall settings.

Run just e2e-build-terminal-dev separately when a test explicitly needs the development feature set. Each successful guarded build also saves its own disk report under artifacts/disk-budget/. For one-off Work 8 Cargo test, check, or Clippy rebuilds, wrap the Cargo command too. For example, inside nix develop:

AURA_BUILD_PROFILE=debug AURA_BUILD_FEATURES=work8-test \
  nice -n 10 bash scripts/dev/build-budget.sh \
  --lane work8-debug --allow-live-harness -- \
  cargo test -p hxrts-aura-app

Substitute the required Cargo subcommand and package selector. Start only when no other Cargo or Dioxus build is active. The live option preserves release/web/Dylint caches while a LAN run is active; the disk monitor still stops only its own command at the emergency floor. When the LAN owner permits a terminal build during a live run, use just e2e-build-terminal-live. That recipe skips global Cargo sweeping and may collect only an idle WASM debug lane. The ordinary just e2e-build-terminal recipe enforces the full idle-lane cache policy; run it after the harness stops. Web and harness-tool rebuilds also require the harness to stop before cache collection. On Host B, pass its own --root ending in /artifacts/runs to the retention script; it never prunes the other host remotely. When only an unused debug lane can be released, inspect it with just prune-inactive-lane wasm-debug --dry-run and then use --apply. The cleanup checks for active compilers and open files in that lane. Global Cargo sweeping also skips a target with open files, such as proc-macro libraries loaded by rust-analyzer; it then considers only fully inactive whole lanes. The 10 GiB per-checkout target is a soft between-build goal, so a lane held open by another process may remain above it until that process exits. just prune-inactive-lane debug-incremental --dry-run previews the complete incremental cache independently of sibling debug dependencies. With --apply, it refuses active compilers, open incremental files and symlinked parents, and preserves release outputs and loaded debug libraries. The build and CI guards consider this lane before the complete debug lane.

just prune-inactive-lane trybuild --dry-run separately previews the complete target/tests/trybuild compile-fail cache. Its apply mode requires idle compilers, no open files in that cache, and ordinary parent directories. Build and CI guards can reclaim it while preserving editor-loaded sibling debug libraries. The next compile-fail gate rebuilds this cache; its test sources and expected diagnostics remain in the repository.

For an explicit browser release cache reset, wasm-release selects target/wasm32-unknown-unknown/wasm-release and wasm-host-release selects its host-side build cache at target/wasm-release. Preview each with just prune-inactive-lane <lane> --dry-run. Apply refuses active compilers, harness consumers, open files, and symlinked paths. These release lanes are manual cleanup choices; automatic guards preserve them. Installed browser bundles and run evidence stay outside these compiler cache paths.

To reproduce the debug cache measurement, run bash scripts/dev/compare-debug-incremental.sh --dry-run first, then --apply inside Nix during an idle build window. It checks the actual foundation crate with incremental compilation enabled and disabled, measuring a clean check, a warm check and a harmless source rebuild. It uses a fixed-commit source archive and sequential disposable targets, retains logs under artifacts/disk-budget/debug-comparisons, and retains its source snapshot on failure. It leaves the main checkout's source, target and E2E evidence intact. just ci-build-cache-policy runs the isolated Bash safety fixtures, including default/override inheritance, active-build refusal, symlink containment, fixed-commit sequencing, evidence retention and atomic installation. Both GitHub Fast CI and ci-dry-run require this gate; it performs no real build or shared-cache deletion.

Creating an Agent

Aura provides platform-specific builder presets for creating agents. The CLI preset is the simplest path for terminal applications.

#![allow(unused)]
fn main() {
use aura_agent::AgentBuilder;

// CLI preset - simplest path for terminal applications
let agent = AgentBuilder::cli()
    .data_dir("~/.aura")
    .testing_mode()
    .build()
    .await?;
}

The CLI preset provides sensible defaults for command-line tools. It uses file-based storage, real cryptographic operations, and TCP transport.

For custom environments that need explicit control over effect handlers, use AgentBuilder::custom() with typestate enforcement. This requires providing all five core effects (crypto, storage, time, random, console) before build() is available.

Platform-specific presets are available for iOS (AgentBuilder::ios()), Android (AgentBuilder::android()), and Web/WASM (AgentBuilder::web()). These require feature flags to enable. See Effects and Handlers Guide for detailed builder examples.

See Project Structure for details on the 8-layer architecture and effect handler organization.

Ownership Declaration Before You Add New Parity-Critical Code

Before adding a new parity-critical module or workflow, declare its ownership category in the crate ARCHITECTURE.md.

Use Pure for reducers, validators, and typed contracts. Use MoveOwned for handles, owner tokens, and ownership transfer. Use ActorOwned for long-lived mutable async state and coordinators. Use Observed for rendering, harness reads, and diagnostics.

Also declare which capability gates parity-critical mutation and publication, which module owns terminal lifecycle, and which timeout and backoff policy the owner consumes. If those points are not explicit, the new module is not ready to land.

Hello World Protocol

Create a simple ping-pong choreography. This protocol demonstrates basic message exchange between two devices.

#![allow(unused)]
fn main() {
use aura_macros::tell;
use aura_core::effects::{ConsoleEffects, NetworkEffects, TimeEffects};
use aura_core::time::PhysicalTime;
use serde::{Serialize, Deserialize};

/// Sealed supertrait for ping-pong effects
pub trait PingPongEffects: ConsoleEffects + NetworkEffects + TimeEffects {}
impl<T> PingPongEffects for T where T: ConsoleEffects + NetworkEffects + TimeEffects {}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ping {
    pub message: String,
    pub timestamp: PhysicalTime,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pong {
    pub response: String,
    pub timestamp: PhysicalTime,
}

tell! {
    #[namespace = "hello_world"]
    protocol HelloWorld {
        roles: Alice, Bob;

        Alice[guard_capability = "hello_world:send_ping", flow_cost = 10]
        -> Bob: SendPing(Ping);

        Bob[guard_capability = "hello_world:send_pong", flow_cost = 10, journal_facts = "pong_sent"]
        -> Alice: SendPong(Pong);
    }
}
}

The choreography defines a global protocol. Alice sends a ping to Bob. Bob responds with a pong. Guard capabilities control access and flow costs manage rate limiting. Outside the choreography DSL boundary, first-party Rust code should use typed capability families or capability_name! rather than hand-written capability strings.

Implement the Alice session:

#![allow(unused)]
fn main() {
pub async fn execute_alice_session<E: PingPongEffects>(
    effects: &E,
    ping_message: String,
    bob_device: aura_core::DeviceId,
) -> Result<Pong, HelloWorldError> {
    let ping = Ping {
        message: ping_message,
        timestamp: effects.current_timestamp().await,
    };

    let ping_bytes = serde_json::to_vec(&ping)?;
    effects.send_to_peer(bob_device.into(), ping_bytes).await?;

    let (peer_id, pong_bytes) = effects.receive().await?;
    let pong: Pong = serde_json::from_slice(&pong_bytes)?;

    Ok(pong)
}
}

Alice serializes the ping message and sends it to Bob. She then waits for Bob's response and deserializes the pong message. See Effect System for details on effect-based execution.

Local Deployment

Initialize a local Aura account:

just quickstart init

This command creates a 2-of-3 threshold account configuration. The account uses three virtual devices with a threshold of two signatures for operations.

Check account status:

just quickstart status

The status command shows account health, device connectivity, and threshold configuration. All virtual devices should show as connected.

Run quickstart smoke checks:

just quickstart smoke

This command runs a local end-to-end smoke flow (init, status, and threshold-signature checks) across multiple virtual devices.

CLI Interaction

The aura CLI operates on the same account as the TUI. It reads the account from --data-dir, falling back to the TUI's default location ($AURA_PATH/.aura, or ~/.aura). Run aura --help or aura COMMAND --help to see the available commands.

View the account:

aura --data-dir ~/.aura status

This prints the account's authority, nickname, threshold, device count and contact count.

Inspect and use chat, contacts, invitations and homes. Channels are named by name or id:

aura chat list
aura chat send general "hello"
aura chat history general --limit 20
aura contact list
aura invite create --invitee AUTHORITY      # prints a shareable code
aura invite import --code CODE
aura invite accept --invitation-id ID
aura home create --name Home
aura home invite AUTHORITY
aura slash "/topic welcome" --channel general

The other command groups follow the TUI's screens: account create|refresh, profile nick, settings show|mfa, contact list|rename|remove|whois|read-receipts, friend send|accept|decline|revoke, chat dm|join|close|members|retry|mark-read, home enter, neighborhood create|add|link, mod kick|ban|unban|mute|unmute|pin|unpin|op|deop|admit, access set, peer list|discover, device threshold|remove, guardians set, rotation status|cancel, ota list|status|publish|recommend|stage, budget and notifications list. aura account create --nickname NAME creates an account the same way the TUI does, so the TUI opens it afterwards.

Every account command runs the same aura_app::ui::workflows functions as the TUI and web, through one typed request model.

Driving a node from a program

aura rpc keeps one runtime online and reads one JSON request per line on stdin; aura serve keeps it online without a client:

aura rpc
{"id":1,"method":"chat_send","params":{"channel":"general","message":"hi"}}
{"id":2,"method":"subscribe","params":{"topics":["messages"]}}

A running node, the TUI or aura serve, also listens on an owner-only local socket (mode 0600, same-user connections only, no network listener). Unix socket paths are short, so the socket lives in the user runtime directory ($XDG_RUNTIME_DIR, else $TMPDIR, else /tmp) as aura-<hash>.sock, and the node records its path in <data-dir>.sock-path beside the data directory. A node that cannot host its socket fails to start. Every aura account command and aura rpc first try that socket, so they work while the TUI holds the account. With no node running, the CLI opens the account's production runtime itself under the profile's exclusive lease.

The protocol schema is published at crates/aura-terminal/schema/aura-rpc-v2.json, generated from the request and response types and kept in sync by just ci-rpc-schema.

RPC response counts use unsigned 64-bit integers. Chat history and search limits use unsigned 32-bit integers, checked before conversion to the workflow's local collection limit. These ranges are the same on native and browser-sized targets. Subscription event payloads use serde_json::Value, whose numeric representation already bounds their counts independently of pointer width.

The first line written is a hello line with the protocol version, methods and event topics. Each response carries the request's id and the same result or error that aura --json prints. After subscribe, event lines ({"type":"event","topic":"messages",...}) interleave with responses; a subscriber that falls behind gets one resync event with a fresh snapshot. The session ends on EOF or {"method":"shutdown"}.

Command output is printed plainly. Add -v to also print runtime diagnostics.

For scripts, the global flags make every command machine-readable:

aura --json status            # {"ok":true,"result":{...}} on stdout
aura --yes chat leave general  # confirm a destructive command without a prompt
aura --timeout 30 sync once --peers PEER

Under --json, stdout carries exactly one JSON document, either {"ok":true,"result":...} or {"ok":false,"error":{"code":...,"message":...}}; diagnostics go to stderr. Destructive commands prompt on a terminal and fail without --yes otherwise. The exit code classifies failures: 0 success, 1 failed, 2 invalid input, 3 not found, 4 permission denied, 5 timeout, 6 unavailable.

Testing Your Protocol

Create a test script for the hello world protocol:

#![allow(unused)]
fn main() {
use aura_macros::aura_test;
use aura_testkit::*;
use aura_agent::runtime::AuraEffectSystem;
use aura_agent::AgentConfig;

#[aura_test]
async fn test_hello_world_protocol() -> aura_core::AuraResult<()> {
    // Create test fixture with automatic tracing
    let fixture = create_test_fixture().await?;

    // Create deterministic test effect systems
    let alice_effects = AuraEffectSystem::simulation_for_named_test_with_salt(
        &AgentConfig::default(),
        "test_hello_world_protocol",
        0,
    )?;
    let bob_effects = AuraEffectSystem::simulation_for_named_test_with_salt(
        &AgentConfig::default(),
        "test_hello_world_protocol",
        1,
    )?;

    // Get device IDs for routing
    let alice_device = fixture.create_device_id();
    let bob_device = fixture.create_device_id();

    let ping_message = "Hello Bob!".to_string();

    // Run protocol sessions concurrently
    let (alice_result, bob_result) = tokio::join!(
        execute_alice_session(&alice_effects, ping_message.clone(), bob_device),
        execute_bob_session(&bob_effects, ping_message.clone())
    );

    assert!(alice_result.is_ok(), "Alice session failed");
    assert!(bob_result.is_ok(), "Bob session failed");

    let pong = alice_result?;
    assert!(pong.response.contains(&ping_message));

    Ok(())
}
}

This test creates deterministic, seeded effect systems for Alice and Bob using simulation_for_named_test_with_salt(...). The identity + salt pair makes failures reproducible. For comprehensive testing approaches, see Testing Guide.

Run the test:

cargo test test_hello_world_protocol

The test validates protocol correctness without requiring network infrastructure. Mock handlers provide deterministic behavior for testing.

Understanding System Invariants

System invariants are defined in System Architecture and Theoretical Model. Key invariants include Charge-Before-Send, CRDT Convergence, Context Isolation, and Secure Channel Lifecycle.

See Project Structure for traceability details. When developing, ensure your protocols respect these invariants to maintain system integrity.

Next Steps

You now have a working Aura development environment. The hello world protocol demonstrates basic choreographic programming concepts.

Continue with Effects and Handlers Guide to learn about effect systems, platform implementation, and handler patterns. Learn choreographic programming in Choreography Guide. For session type theory, see MPST and Choreography.

Explore testing and simulation in Testing Guide and Simulation Guide.