Development Patterns and Workflows
This document covers practical patterns and workflows for developing Aura systems.
Effects vs Coordination
A critical distinction guides where code belongs in the architecture.
Single-Party Operations
aura-effects implements single-party operations that are stateless and context-free. Each operation takes input and produces output without maintaining state or coordinating with other handlers.
Examples:
sign(key, msg) → Signature- One device, one cryptographic operationstore_chunk(id, data) → Ok(())- One device, one writeRealCryptoHandler- Self-contained cryptographic operationsMockNetworkHandler- Simulated peer communication for testing
Single-party operations are reusable in any context. They work in unit tests, integration tests, and production equally well.
Multi-Party Coordination
aura-protocol implements multi-party coordination where multiple handlers orchestrate together. Operations are stateful and context-specific.
Examples:
execute_anti_entropy(...)- Orchestrates sync across multiple partiesCrdtCoordinator- Manages state of multiple CRDT handlersGuardChain- Coordinates authorization checks across sequential operations
Multi-party coordination requires a context. It assumes multiple handlers are involved and state is maintained across operations.
The distinction is critical for understanding where code belongs. Single-party operations go in aura-effects. Multi-party coordination goes in aura-protocol.
Code Location Decision Matrix
Use these questions to classify code and determine the correct crate.
| Pattern | Answer | Location |
|---|---|---|
| Implements single effect trait method | Stateless and single operation | aura-effects |
| Coordinates multiple effects or handlers | Stateful and multi-handler | aura-protocol |
| Multi-party coordination logic | Distributed state and orchestration | aura-protocol |
| Domain-specific types and semantics | Pure logic without handlers | Domain crate or aura-mpst |
| Complete reusable protocol | End-to-end without UI | Feature/protocol crate |
| Assembles handlers and protocols | Runtime composition | aura-agent or aura-simulator |
| User-facing application | Has main() entry point | aura-terminal or app-* |
Boundary Questions for Edge Cases
Is it stateless or stateful?
Stateless and single operation go in aura-effects. Stateful and coordinating go in aura-protocol.
Does it work for one party or multiple?
Single-party code goes in aura-effects. Multi-party code goes in aura-protocol.
Is it context-free or context-specific?
Context-free code (works anywhere) goes in aura-effects. Context-specific code (requires orchestration) goes in aura-protocol.
Does it coordinate multiple handlers?
No coordination goes in aura-effects. Multiple handlers being orchestrated goes in aura-protocol.
Typical Workflows
Adding a New Cryptographic Primitive
- Define the type in
aura-corecrypto module - Implement
aura-coretraits for the type's semantics - Add a single-operation handler in
aura-effectsthat implements the primitive - Use the handler in feature crates or protocols through the effect system
Adding a New Distributed Protocol
- Write the choreography in
aura-mpstusing session types or DSL syntax withaura-macros - Use annotation syntax for security:
Role[guard_capability = "...", flow_cost = N] -> Target: Message - Create the protocol implementation in
aura-protocolor a feature crate - Implement the coordination logic using handlers from
aura-effects - Wire the protocol into
aura-agentruntime with appropriate leakage budget policies - Expose the protocol through CLI or application interfaces
Writing a New Test
- Create test fixtures in
aura-testkit - Use mock handlers from
aura-effectsfor reproducibility - Configure appropriate leakage budget policies for the test scenarios
- Drive the agent from the test harness
- Compose protocols using
aura-simulatorfor deterministic execution
Type Consolidation and Single Source of Truth
ProtocolType
The canonical definition of ProtocolType lives in aura-core. All other crates re-export and use this canonical definition.
Variants:
Dkd- Deterministic Key DerivationCounter- Counter reservation protocolResharing- Key resharing for threshold updatesLocking- Resource locking protocolRecovery- Account recovery protocolCompaction- Ledger compaction protocol
Usage is consistent across aura-protocol and aura-simulator.
SessionStatus
The canonical definition of SessionStatus lives in aura-core. Variants represent the session lifecycle.
Lifecycle order:
Initializing- Session initializing before executionActive- Session currently executingWaiting- Session waiting for participant responsesCompleted- Session completed successfullyFailed- Session failed with errorExpired- Session expired due to timeoutTimedOut- Session timed out during executionCancelled- Session was cancelled
All crates that track session state use the canonical definition from aura-core.
Capability System Layering
The capability system intentionally uses multiple architectural layers. Each layer serves legitimate purposes.
- Canonical types in
aura-coreprovide lightweight references - Authorization layer (
aura-wot) adds policy enforcement features - Storage layer (
aura-store) implements capability-based access control
Clear conversion paths enable inter-layer communication without confusion.
Effect Handler Patterns
Stateless Handler Pattern
Effect handlers follow a consistent pattern. Each handler implements one or more effect traits from aura-core.
A handler is stateless. It receives input, performs a single operation, and returns output. No state is maintained between calls.
Example:
#![allow(unused)] fn main() { pub struct RealCryptoHandler; impl CryptoEffects for RealCryptoHandler { async fn sign(&self, key: &SecretKey, msg: &[u8]) -> Signature { // Single operation: sign the message } async fn verify(&self, key: &PublicKey, msg: &[u8], sig: &Signature) -> bool { // Single operation: verify the signature } } }
Mock handlers follow the same pattern but use deterministic or simulated implementations:
#![allow(unused)] fn main() { pub struct MockCryptoHandler; impl CryptoEffects for MockCryptoHandler { async fn sign(&self, key: &SecretKey, msg: &[u8]) -> Signature { // Deterministic mock signature for testing } async fn verify(&self, key: &PublicKey, msg: &[u8], sig: &Signature) -> bool { // Always returns true in testing mode } } }
Multi-Party Coordination Pattern
Coordination logic in aura-protocol manages multiple handlers working together.
Coordination functions are async and stateful. They orchestrate handlers to accomplish multi-party goals.
Example:
#![allow(unused)] fn main() { pub async fn execute_anti_entropy( coordinator: CrdtCoordinator, // Coordinates multiple CRDT handlers adapter: AuraHandlerAdapter, // Coordinates choreography and effects guards: GuardChain, // Coordinates authorization ) -> Result<SyncResult> { // Orchestrates distributed sync across parties } }
Coordination functions typically accept multiple handlers or a composed system. They maintain state across multiple operations. They coordinate between different concerns like authorization, storage, and transport. They return results that depend on the combined state.
Guard Chain Execution Pattern
The guard chain coordinates authorization, flow budgets, and journal effects in strict sequence. Guards themselves are pure: evaluation runs synchronously over a prepared GuardSnapshot and yields EffectCommand items that an async interpreter executes. This keeps guard logic deterministic and prevents observable side effects from failed authorization attempts.
#![allow(unused)] fn main() { async fn send_storage_put( bridge: &BiscuitAuthorizationBridge, guards: &GuardChain, interpreter: &dyn EffectInterpreter, ctx: ContextId, peer: AuthorityId, token: Biscuit, payload: PutRequest, ) -> Result<()> { // Phase 1: Authorization via Biscuit + policy (async, cached) let auth_result = bridge.authorize(&token, "storage_write", &payload.scope())?; if !auth_result.authorized { return Err(AuraError::permission_denied("Token authorization failed")); } // Phase 2: Prepare snapshot (async) and evaluate guards (sync) let snapshot = prepare_guard_snapshot(ctx, peer, &auth_result.cap_frontier).await?; let outcome = guards.evaluate(&snapshot, &payload.guard_request()); if outcome.decision.is_denied() { return Err(AuraError::permission_denied("Guard evaluation denied")); } // Phase 3: Execute commands (async) - charge, record leakage, commit journal, send transport for cmd in outcome.effects { interpreter.exec(cmd).await?; } Ok(()) } }
This pattern implements the guard chain guarantee: snapshot preparation happens before synchronous guard evaluation, and no transport observable occurs until the interpreter executes the resulting commands in order.
Security-First Design Philosophy
Privacy Budget Enforcement
The leakage tracking system implements security by default with backward compatibility.
#![allow(unused)] fn main() { // Secure by default - denies undefined budgets let tracker = LeakageTracker::new(); // UndefinedBudgetPolicy::Deny // Legacy compatibility mode let tracker = LeakageTracker::legacy_permissive(); // UndefinedBudgetPolicy::Allow // Configurable policy let tracker = LeakageTracker::with_undefined_policy( UndefinedBudgetPolicy::DefaultBudget(1000) ); }
The default policy is to deny access to undefined budgets. This prevents accidental privacy violations. Legacy mode is available for existing code that needs to operate without strict budget enforcement.
Annotation Parsing
Robust syn-based validation prevents malformed choreographies from compiling. Proper error messages guide developers toward secure patterns. All placeholders have been replaced with complete implementations for deployment readiness.
The choreography compiler validates annotations at compile time. Invalid or missing annotations are rejected with helpful error messages that explain the requirement.
Creating a New Domain Service
Domain crates define stateless handlers that take effect references per-call. The agent layer wraps these with services that manage RwLock access.
Step 1: Create the Domain Handler
In the domain crate (e.g., aura-chat/src/service.rs):
#![allow(unused)] fn main() { /// Stateless handler - takes effect reference per-call pub struct MyHandler; impl MyHandler { pub fn new() -> Self { Self } pub async fn my_operation<E>( &self, effects: &E, // <-- Per-call reference param: SomeType, ) -> Result<Output> where E: StorageEffects + RandomEffects + PhysicalTimeEffects { // Use effects for side effects let uuid = effects.random_uuid().await; // ... domain logic } } }
Step 2: Create the Agent Service Wrapper
In aura-agent/src/handlers/my_service.rs:
#![allow(unused)] fn main() { pub struct MyService { handler: MyHandler, effects: Arc<RwLock<AuraEffectSystem>>, } impl MyService { pub fn new(effects: Arc<RwLock<AuraEffectSystem>>) -> Self { Self { handler: MyHandler::new(), effects, } } pub async fn my_operation(&self, param: SomeType) -> AgentResult<Output> { let effects = self.effects.read().await; // <-- Acquire lock self.handler .my_operation(&*effects, param) .await .map_err(Into::into) } } }
Step 3: Expose via Agent API
In aura-agent/src/core/api.rs:
#![allow(unused)] fn main() { impl AuraAgent { pub fn my_service(&self) -> MyService { MyService::new(self.runtime.effects()) } } }
Benefits
- Domain crate stays pure: No tokio/RwLock dependency
- Testable: Pass mock effects directly in unit tests
- Consistent: Same pattern across all domain crates
- Safe: RwLock managed automatically at agent layer
See docs/106_effect_system_and_runtime.md section 13 for more details.
Implementing Aura in a New Environment
When building an Aura application for a new platform (mobile, web, embedded, or custom infrastructure), use the AgentBuilder API to assemble the runtime with appropriate effect handlers.
Choosing a Builder Strategy
Aura provides three paths for creating agents:
| Strategy | Use Case | Compile-Time Safety |
|---|---|---|
| Platform preset | Standard platforms (CLI, iOS, Android, Web) | Configuration validation |
| Custom preset | Full control over all effects | Typestate enforcement |
| Effect overrides | Preset with specific customizations | Mixed |
Using Platform Presets
Platform presets provide sensible defaults for common environments.
CLI Preset
The CLI preset is the simplest path for terminal applications:
#![allow(unused)] fn main() { use aura_agent::AgentBuilder; let agent = AgentBuilder::cli() .data_dir("~/.aura") .testing_mode() .build() .await?; }
The CLI preset wires:
RealCryptoHandlerfor cryptographic operationsFilesystemStorageHandlerfor persistent storagePhysicalTimeHandlerfor wall-clock timeRealRandomHandlerfor secure randomnessRealConsoleHandlerfor terminal outputTcpTransportHandlerfor network transport
Mobile Presets
Mobile presets require platform-specific feature flags:
#![allow(unused)] fn main() { // iOS (requires --features ios) let agent = AgentBuilder::ios() .app_group("group.com.example.aura") .keychain_access_group("com.example.aura") .data_protection(DataProtectionClass::CompleteProtection) .build() .await?; // Android (requires --features android) let agent = AgentBuilder::android() .application_id("com.example.aura") .use_strongbox(true) .require_user_authentication(Some(300)) // 5 minutes .build() .await?; }
Web Preset
The web preset targets browser environments:
#![allow(unused)] fn main() { // Web/WASM (requires --features web) let agent = AgentBuilder::web() .storage_prefix("aura_") .use_session_storage(false) .build() .await?; }
Custom Preset with Typestate
When you need explicit control over all effects, use the custom preset. The Rust type system enforces that all required effects are provided before build() is available.
#![allow(unused)] fn main() { use std::sync::Arc; use aura_agent::AgentBuilder; use aura_effects::{ RealCryptoHandler, FilesystemStorageHandler, PhysicalTimeHandler, RealRandomHandler, RealConsoleHandler, }; // All five required effects must be provided let agent = AgentBuilder::custom() .with_crypto(Arc::new(RealCryptoHandler::new())) .with_storage(Arc::new(FilesystemStorageHandler::new("~/.aura".into()))) .with_time(Arc::new(PhysicalTimeHandler::new())) .with_random(Arc::new(RealRandomHandler::new())) .with_console(Arc::new(RealConsoleHandler::new())) .testing_mode() .build() .await?; }
Attempting to call build() without providing all required effects results in a compile error:
#![allow(unused)] fn main() { // This will not compile - missing effects let agent = AgentBuilder::custom() .with_crypto(Arc::new(RealCryptoHandler::new())) .build() // Error: method not found for this type .await?; }
Required vs Optional Effects
Core Required Effects
Every agent requires these five effects:
| Effect | Purpose | Trait |
|---|---|---|
| Crypto | Signing, verification, encryption | CryptoEffects |
| Storage | Persistent data storage | StorageEffects |
| Time | Wall-clock timestamps | PhysicalTimeEffects |
| Random | Cryptographically secure randomness | RandomEffects |
| Console | Logging and output | ConsoleEffects |
Optional Effects
These effects have defaults or are derived from required effects:
| Effect | Default Behavior |
|---|---|
TransportEffects | TCP transport (can be customized) |
LogicalClockEffects | Derived from storage |
OrderClockEffects | Derived from random |
ReactiveEffects | Default reactive handler |
JournalEffects | Derived from storage + crypto |
BiometricEffects | Fallback no-op handler |
Implementing Custom Effect Handlers
To support a new platform, implement the core effect traits:
#![allow(unused)] fn main() { use aura_core::effects::{CryptoEffects, Signature, SecretKey, PublicKey}; pub struct MyPlatformCrypto { // Platform-specific state } #[async_trait] impl CryptoEffects for MyPlatformCrypto { async fn sign(&self, key: &SecretKey, msg: &[u8]) -> Result<Signature> { // Platform-specific signing implementation } async fn verify(&self, key: &PublicKey, msg: &[u8], sig: &Signature) -> Result<bool> { // Platform-specific verification } // ... other required methods } }
Then use it with the custom builder:
#![allow(unused)] fn main() { let agent = AgentBuilder::custom() .with_crypto(Arc::new(MyPlatformCrypto::new())) .with_storage(Arc::new(MyPlatformStorage::new())) .with_time(Arc::new(MyPlatformTime::new())) .with_random(Arc::new(MyPlatformRandom::new())) .with_console(Arc::new(MyPlatformConsole::new())) .build() .await?; }
Testing Custom Implementations
Use mock handlers from aura-testkit for testing:
#![allow(unused)] fn main() { use aura_testkit::{MockCryptoHandler, MockStorageHandler}; #[tokio::test] async fn test_custom_agent() { let agent = AgentBuilder::custom() .with_crypto(Arc::new(MockCryptoHandler::new())) .with_storage(Arc::new(MockStorageHandler::new())) .with_time(Arc::new(MockTimeHandler::new())) .with_random(Arc::new(MockRandomHandler::seeded(42))) .with_console(Arc::new(MockConsoleHandler::new())) .testing_mode() .build() .await .expect("Agent should build"); // Test agent operations } }
Feature Flags
Platform-specific presets require feature flags:
[dependencies]
aura-agent = { version = "0.1", features = ["ios"] }
# or
aura-agent = { version = "0.1", features = ["android"] }
# or
aura-agent = { version = "0.1", features = ["web"] }
The default feature set includes CLI support. Multiple platform features can be enabled simultaneously for cross-platform codebases.
Platform Implementation Checklist
When implementing Aura for a new platform:
- Identify platform-specific APIs for crypto, storage, time, random, and console
- Implement the five core effect traits using platform APIs
- Create a preset builder (optional but recommended)
- Add feature flags for platform-specific dependencies
- Write integration tests using mock handlers
- Document platform-specific security considerations
- Consider transport layer requirements (WebSocket, BLE, etc.)